From ceb3e14dccc2a357c87a266029e8c2fedc9e326b Mon Sep 17 00:00:00 2001 From: nf-core-bot Date: Wed, 30 Apr 2025 12:28:44 +0000 Subject: [PATCH 001/258] Template update for nf-core/tools version 3.2.1 --- .github/workflows/awsfulltest.yml | 41 ++++++++----------------------- .github/workflows/ci.yml | 1 + .nf-core.yml | 2 +- nextflow.config | 2 +- ro-crate-metadata.json | 14 +++++------ 5 files changed, 20 insertions(+), 40 deletions(-) diff --git a/.github/workflows/awsfulltest.yml b/.github/workflows/awsfulltest.yml index 1bc712a2..e34b4e6d 100644 --- a/.github/workflows/awsfulltest.yml +++ b/.github/workflows/awsfulltest.yml @@ -4,44 +4,23 @@ 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/stableexpression' && 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/stableexpression' && github.event.review.state == 'approved' && (github.event.pull_request.base.ref == 'master' || github.event.pull_request.base.ref == 'main') || github.event_name == 'workflow_dispatch' 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 @@ -52,12 +31,12 @@ jobs: workspace_id: ${{ secrets.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/stableexpression/work-${{ github.sha }} + revision: ${{ steps.revision.outputs.revision }} + workdir: s3://${{ secrets.AWS_S3_BUCKET }}/work/stableexpression/work-${{ steps.revision.outputs.revision }} parameters: | { "hook_url": "${{ secrets.MEGATESTS_ALERTS_SLACK_HOOK_URL }}", - "outdir": "s3://${{ secrets.AWS_S3_BUCKET }}/stableexpression/results-${{ github.sha }}" + "outdir": "s3://${{ secrets.AWS_S3_BUCKET }}/stableexpression/results-${{ steps.revision.outputs.revision }}" } profiles: test_full diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28d5dde7..942f78ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,5 +83,6 @@ jobs: uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 - name: "Run pipeline with test data ${{ matrix.NXF_VER }} | ${{ matrix.test_name }} | ${{ matrix.profile }}" + continue-on-error: ${{ matrix.NXF_VER == 'latest-everything' }} run: | nextflow run ${GITHUB_WORKSPACE} -profile ${{ matrix.test_name }},${{ matrix.profile }} --outdir ./results diff --git a/.nf-core.yml b/.nf-core.yml index 634b8049..970e9716 100644 --- a/.nf-core.yml +++ b/.nf-core.yml @@ -1,4 +1,4 @@ -nf_core_version: 3.2.0 +nf_core_version: 3.2.1 repository_type: pipeline template: author: Olivier Coen diff --git a/nextflow.config b/nextflow.config index 36731990..d301a1f8 100644 --- a/nextflow.config +++ b/nextflow.config @@ -249,7 +249,7 @@ manifest { // 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.2.0' // Validation of pipeline parameters and creation of an input channel from a sample sheet } validation { diff --git a/ro-crate-metadata.json b/ro-crate-metadata.json index e2a4dff2..39afdcd8 100644 --- a/ro-crate-metadata.json +++ b/ro-crate-metadata.json @@ -22,7 +22,7 @@ "@id": "./", "@type": "Dataset", "creativeWorkStatus": "InProgress", - "datePublished": "2025-01-27T14:48:52+00:00", + "datePublished": "2025-04-30T12:28:35+00:00", "description": "

\n \n \n \"nf-core/stableexpression\"\n \n

\n\n[![GitHub Actions CI Status](https://github.com/nf-core/stableexpression/actions/workflows/ci.yml/badge.svg)](https://github.com/nf-core/stableexpression/actions/workflows/ci.yml)\n[![GitHub Actions Linting Status](https://github.com/nf-core/stableexpression/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/stableexpression/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/stableexpression/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/stableexpression)\n\n[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23stableexpression-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/stableexpression)[![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/stableexpression** 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/stableexpression \\\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/stableexpression/usage) and the [parameter documentation](https://nf-co.re/stableexpression/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/stableexpression/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/stableexpression/output).\n\n## Credits\n\nnf-core/stableexpression was originally written by Olivier Coen.\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 `#stableexpression` channel](https://nfcore.slack.com/channels/stableexpression) (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", "hasPart": [ { @@ -99,7 +99,7 @@ }, "mentions": [ { - "@id": "#a0fe9e6c-006a-48d7-a58b-40a948e2f69c" + "@id": "#bd0c9113-aeeb-419b-8f3c-aacdc4b6b98e" } ], "name": "nf-core/stableexpression" @@ -128,9 +128,9 @@ } ], "dateCreated": "", - "dateModified": "2025-01-27T14:48:52Z", + "dateModified": "2025-04-30T12:28:35Z", "dct:conformsTo": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE/", - "keywords": ["nf-core", "nextflow", "expression", "qpcr-analysis"], + "keywords": ["nf-core", "nextflow", "expression", "housekeeping-genes", "qpcr-analysis"], "license": ["MIT"], "maintainer": [ { @@ -160,11 +160,11 @@ "version": "!>=24.04.2" }, { - "@id": "#a0fe9e6c-006a-48d7-a58b-40a948e2f69c", + "@id": "#bd0c9113-aeeb-419b-8f3c-aacdc4b6b98e", "@type": "TestSuite", "instance": [ { - "@id": "#5c048b32-7ead-409d-946b-97aff873336e" + "@id": "#2fd9e749-3841-494f-9e8e-4a65cd0d7f7c" } ], "mainEntity": { @@ -173,7 +173,7 @@ "name": "Test suite for nf-core/stableexpression" }, { - "@id": "#5c048b32-7ead-409d-946b-97aff873336e", + "@id": "#2fd9e749-3841-494f-9e8e-4a65cd0d7f7c", "@type": "TestInstance", "name": "GitHub Actions workflow for testing nf-core/stableexpression", "resource": "repos/nf-core/stableexpression/actions/workflows/ci.yml", From 4aef76c173e00fe15fb040c07dae2d7bd88840a0 Mon Sep 17 00:00:00 2001 From: nf-core-bot Date: Tue, 3 Jun 2025 11:02:35 +0000 Subject: [PATCH 002/258] Template update for nf-core/tools version 3.3.1 --- .editorconfig | 37 -- .github/CONTRIBUTING.md | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 4 +- .github/actions/get-shards/action.yml | 69 +++ .github/actions/nf-test/action.yml | 113 +++++ .github/workflows/awsfulltest.yml | 4 +- .github/workflows/awstest.yml | 2 +- .github/workflows/ci.yml | 88 ---- .github/workflows/clean-up.yml | 2 +- .github/workflows/download_pipeline.yml | 20 +- .../{fix-linting.yml => fix_linting.yml} | 4 +- .github/workflows/linting.yml | 15 +- .github/workflows/linting_comment.yml | 4 +- .github/workflows/nf-test.yml | 142 ++++++ .github/workflows/release-announcements.yml | 2 +- ...mment.yml => template-version-comment.yml} | 2 +- .nf-core.yml | 17 +- .pre-commit-config.yaml | 26 +- .prettierrc.yml | 5 + CITATIONS.md | 4 - README.md | 9 +- assets/schema_input.json | 2 +- conf/base.config | 5 +- conf/igenomes.config | 440 ------------------ conf/igenomes_ignored.config | 9 - conf/modules.config | 4 - conf/test.config | 3 +- conf/test_full.config | 4 +- docs/output.md | 14 - docs/usage.md | 3 +- main.nf | 13 - modules.json | 5 - modules/nf-core/fastqc/environment.yml | 5 - modules/nf-core/fastqc/main.nf | 64 --- modules/nf-core/fastqc/meta.yml | 67 --- modules/nf-core/fastqc/tests/main.nf.test | 309 ------------ .../nf-core/fastqc/tests/main.nf.test.snap | 392 ---------------- nextflow.config | 29 +- nextflow_schema.json | 44 +- nf-test.config | 24 + ro-crate-metadata.json | 16 +- .../main.nf | 39 -- tests/.nftignore | 8 + tests/default.nf.test | 35 ++ tests/nextflow.config | 12 + workflows/stableexpression.nf | 9 - 46 files changed, 512 insertions(+), 1614 deletions(-) delete mode 100644 .editorconfig create mode 100644 .github/actions/get-shards/action.yml create mode 100644 .github/actions/nf-test/action.yml delete mode 100644 .github/workflows/ci.yml rename .github/workflows/{fix-linting.yml => fix_linting.yml} (96%) create mode 100644 .github/workflows/nf-test.yml rename .github/workflows/{template_version_comment.yml => template-version-comment.yml} (95%) delete mode 100644 conf/igenomes.config delete mode 100644 conf/igenomes_ignored.config delete mode 100644 modules/nf-core/fastqc/environment.yml delete mode 100644 modules/nf-core/fastqc/main.nf delete mode 100644 modules/nf-core/fastqc/meta.yml delete mode 100644 modules/nf-core/fastqc/tests/main.nf.test delete mode 100644 modules/nf-core/fastqc/tests/main.nf.test.snap create mode 100644 nf-test.config create mode 100644 tests/.nftignore create mode 100644 tests/default.nf.test create mode 100644 tests/nextflow.config diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 6d9b74cc..00000000 --- a/.editorconfig +++ /dev/null @@ -1,37 +0,0 @@ -root = true - -[*] -charset = utf-8 -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true -indent_size = 4 -indent_style = space - -[*.{md,yml,yaml,html,css,scss,js}] -indent_size = 2 - -# These files are edited and tested upstream in nf-core/modules -[/modules/nf-core/**] -charset = unset -end_of_line = unset -insert_final_newline = unset -trim_trailing_whitespace = unset -indent_style = unset -[/subworkflows/nf-core/**] -charset = unset -end_of_line = unset -insert_final_newline = unset -trim_trailing_whitespace = unset -indent_style = unset - -[/assets/email*] -indent_size = unset - -# ignore python and markdown -[*.{py,md}] -indent_style = unset - -# ignore ro-crate metadata files -[**/ro-crate-metadata.json] -insert_final_newline = unset diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index ac31ba3a..bdd34869 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -78,7 +78,7 @@ If you wish to contribute a new step, please use the following coding standards: 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`. +8. If applicable, add a new test in the `tests` directory. 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`. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 07b5000a..2c2997d7 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -8,14 +8,14 @@ These are the most common things requested on pull requests (PRs). Remember that PRs should be made against the dev branch, unless you're preparing a pipeline release. -Learn more about contributing: [CONTRIBUTING.md](https://github.com/nf-core/stableexpression/tree/master/.github/CONTRIBUTING.md) +Learn more about contributing: [CONTRIBUTING.md](https://github.com/nf-core/stableexpression/tree/main/.github/CONTRIBUTING.md) --> ## 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/stableexpression/tree/master/.github/CONTRIBUTING.md) +- [ ] If you've added a new tool - have you followed the pipeline conventions in the [contribution docs](https://github.com/nf-core/stableexpression/tree/main/.github/CONTRIBUTING.md) - [ ] If necessary, also make a PR on the nf-core/stableexpression _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 `). diff --git a/.github/actions/get-shards/action.yml b/.github/actions/get-shards/action.yml new file mode 100644 index 00000000..34085279 --- /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@v1 + 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 00000000..243e7823 --- /dev/null +++ b/.github/actions/nf-test/action.yml @@ -0,0 +1,113 @@ +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: + - name: Setup Nextflow + uses: nf-core/setup-nextflow@v2 + with: + version: "${{ env.NXF_VERSION }}" + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.13" + + - name: Install nf-test + uses: nf-core/setup-nf-test@v1 + with: + version: "${{ env.NFT_VER }}" + install-pdiff: true + + - name: Setup apptainer + if: contains(inputs.profile, 'singularity') + uses: eWaterCycle/setup-apptainer@main + + - 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@505e6394dae86d6a5c7fbb6e3fb8938e3e863830 # v3 + with: + auto-update-conda: true + conda-solver: libmamba + conda-remove-defaults: true + + # TODO Skip failing conda tests and document their failures + # https://github.com/nf-core/modules/issues/7017 + - name: Run nf-test + shell: bash + env: + NFT_DIFF: ${{ env.NFT_DIFF }} + NFT_DIFF_ARGS: ${{ env.NFT_DIFF_ARGS }} + 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 e34b4e6d..a73aa7a8 100644 --- a/.github/workflows/awsfulltest.yml +++ b/.github/workflows/awsfulltest.yml @@ -14,7 +14,7 @@ jobs: run-platform: name: Run AWS full tests # 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/stableexpression' && github.event.review.state == 'approved' && (github.event.pull_request.base.ref == 'master' || github.event.pull_request.base.ref == 'main') || github.event_name == 'workflow_dispatch' + if: github.repository == 'nf-core/stableexpression' && 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: Set revision variable @@ -40,7 +40,7 @@ jobs: } profiles: test_full - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: Seqera Platform debug log file path: | diff --git a/.github/workflows/awstest.yml b/.github/workflows/awstest.yml index 76e01b25..174c3ef3 100644 --- a/.github/workflows/awstest.yml +++ b/.github/workflows/awstest.yml @@ -25,7 +25,7 @@ jobs: } profiles: test - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: Seqera Platform debug log file path: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 942f78ad..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,88 +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/stableexpression') }}" - 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 }}" - continue-on-error: ${{ matrix.NXF_VER == 'latest-everything' }} - 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 0b6b1f27..ac030fd5 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@5bef64f19d7facfb25b37b414482c7164d639639 # v9 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 ab06316e..999bcc38 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 @@ -52,9 +44,9 @@ jobs: - name: Disk space cleanup uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 - - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: - python-version: "3.12" + python-version: "3.13" architecture: "x64" - name: Setup Apptainer @@ -120,6 +112,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 +125,10 @@ jobs: else echo "The pipeline can be downloaded successfully!" fi + + - name: Upload Nextflow logfile for debugging purposes + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + 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 similarity index 96% rename from .github/workflows/fix-linting.yml rename to .github/workflows/fix_linting.yml index be68e1d5..133936ad 100644 --- a/.github/workflows/fix-linting.yml +++ b/.github/workflows/fix_linting.yml @@ -32,9 +32,9 @@ jobs: GITHUB_TOKEN: ${{ secrets.nf_core_bot_auth_token }} # Install and run pre-commit - - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: - python-version: "3.12" + python-version: "3.13" - name: Install pre-commit run: pip install pre-commit diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index dbd52d5a..f2d7d1dd 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] @@ -17,9 +14,9 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Set up Python 3.12 - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: - python-version: "3.12" + python-version: "3.13" - name: Install pre-commit run: pip install pre-commit @@ -36,13 +33,13 @@ jobs: - name: Install Nextflow uses: nf-core/setup-nextflow@v2 - - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: - python-version: "3.12" + python-version: "3.13" architecture: "x64" - 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 @@ -74,7 +71,7 @@ jobs: - name: Upload linting log file artifact if: ${{ always() }} - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: linting-logs path: | diff --git a/.github/workflows/linting_comment.yml b/.github/workflows/linting_comment.yml index 95b6b6af..7e8050fb 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@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8 + uses: dawidd6/action-download-artifact@4c1e823582f43b179e2cbb49c3eade4e41f992e2 # v10 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@52423e01640425a022ef5fd42c6fb5f633a02728 # v2 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 00000000..f03aea0c --- /dev/null +++ b/.github/workflows/nf-test.yml @@ -0,0 +1,142 @@ +name: Run nf-test +on: + push: + paths-ignore: + - "docs/**" + - "**/meta.yml" + - "**/*.md" + - "**/*.png" + - "**/*.svg" + 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.2" + 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=$-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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + 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=$-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: + - "24.04.2" + - "latest-everything" + env: + NXF_ANSI_LOG: false + TOTAL_SHARDS: ${{ needs.nf-test-changes.outputs.total_shards }} + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + fetch-depth: 0 + + - name: Run nf-test + uses: ./.github/actions/nf-test + env: + NFT_DIFF: ${{ env.NFT_DIFF }} + NFT_DIFF_ARGS: ${{ env.NFT_DIFF_ARGS }} + NFT_WORKDIR: ${{ env.NFT_WORKDIR }} + with: + profile: ${{ matrix.profile }} + shard: ${{ matrix.shard }} + total_shards: ${{ env.TOTAL_SHARDS }} + confirm-pass: + needs: [nf-test] + if: always() + runs-on: # use self-hosted runners + - runs-on=$-confirm-pass + - runner=2cpu-linux-x64 + steps: + - name: One or more tests failed + 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::" + + - name: Clean Workspace # Purge the workspace in case it's running on a self-hosted runner + if: always() + run: | + ls -la ./ + rm -rf ./* || true + rm -rf ./.??* || true + ls -la ./ diff --git a/.github/workflows/release-announcements.yml b/.github/workflows/release-announcements.yml index 76a9e67e..4abaf484 100644 --- a/.github/workflows/release-announcements.yml +++ b/.github/workflows/release-announcements.yml @@ -30,7 +30,7 @@ jobs: bsky-post: runs-on: ubuntu-latest steps: - - uses: zentered/bluesky-post-action@80dbe0a7697de18c15ad22f4619919ceb5ccf597 # v0.1.0 + - uses: zentered/bluesky-post-action@4aa83560bb3eac05dbad1e5f221ee339118abdd2 # v0.2.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 similarity index 95% rename from .github/workflows/template_version_comment.yml rename to .github/workflows/template-version-comment.yml index 537529bc..beb5c77f 100644 --- a/.github/workflows/template_version_comment.yml +++ b/.github/workflows/template-version-comment.yml @@ -14,7 +14,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Read template version from .nf-core.yml - uses: nichmor/minimal-read-yaml@v0.0.2 + uses: nichmor/minimal-read-yaml@1f7205277e25e156e1f63815781db80a6d490b8f # v0.0.2 id: read_yml with: config: ${{ github.workspace }}/.nf-core.yml diff --git a/.nf-core.yml b/.nf-core.yml index 970e9716..263a8a4e 100644 --- a/.nf-core.yml +++ b/.nf-core.yml @@ -1,4 +1,16 @@ -nf_core_version: 3.2.1 +lint: + files_exist: + - conf/igenomes.config + - conf/igenomes_ignored.config + - conf/igenomes.config + - conf/igenomes_ignored.config + files_unchanged: + - assets/nf-core-stableexpression_logo_light.png + - .github/PULL_REQUEST_TEMPLATE.md + nextflow_config: + - params.input + schema_lint: false +nf_core_version: 3.3.1 repository_type: pipeline template: author: Olivier Coen @@ -9,4 +21,7 @@ template: name: stableexpression org: nf-core outdir: . + skip_features: + - igenomes + - fastqc version: 1.0dev diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1dec8650..9d0b248d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,10 +4,24 @@ repos: hooks: - id: prettier additional_dependencies: - - prettier@3.2.5 - - - repo: https://github.com/editorconfig-checker/editorconfig-checker.python - rev: "3.1.2" + - prettier@3.5.0 + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 hooks: - - id: editorconfig-checker - alias: ec + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] + exclude: | + (?x)^( + .*ro-crate-metadata.json$| + modules/nf-core/.*| + subworkflows/nf-core/.*| + .*\.snap$ + )$ + - id: end-of-file-fixer + exclude: | + (?x)^( + .*ro-crate-metadata.json$| + modules/nf-core/.*| + subworkflows/nf-core/.*| + .*\.snap$ + )$ diff --git a/.prettierrc.yml b/.prettierrc.yml index c81f9a76..07dbd8bb 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/CITATIONS.md b/CITATIONS.md index d9de22cf..741ce1ed 100644 --- a/CITATIONS.md +++ b/CITATIONS.md @@ -10,10 +10,6 @@ ## 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]. - - [MultiQC](https://pubmed.ncbi.nlm.nih.gov/27312411/) > 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. diff --git a/README.md b/README.md index d6d02a46..208e9b38 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,14 @@ [![GitHub Actions Linting Status](https://github.com/nf-core/stableexpression/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/stableexpression/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/stableexpression/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%A524.04.2-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-3.3.1-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/3.3.1) [![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/stableexpression) -[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23stableexpression-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/stableexpression)[![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%23stableexpression-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/stableexpression)[![Follow on Bluesky](https://img.shields.io/badge/bluesky-%40nf__core-1185fe?labelColor=000000&logo=bluesky)](https://bsky.app/profile/nf-co.re)[![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 @@ -28,8 +29,8 @@ --> -1. Read QC ([`FastQC`](https://www.bioinformatics.babraham.ac.uk/projects/fastqc/))2. Present QC for raw reads ([`MultiQC`](http://multiqc.info/)) + workflows use the "tube map" design for that. See https://nf-co.re/docs/guidelines/graphic_design/workflow_diagrams#examples for examples. --> +2. Present QC for raw reads ([`MultiQC`](http://multiqc.info/)) ## Usage diff --git a/assets/schema_input.json b/assets/schema_input.json index 3e9c28e7..4e192cde 100644 --- a/assets/schema_input.json +++ b/assets/schema_input.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/nf-core/stableexpression/master/assets/schema_input.json", + "$id": "https://raw.githubusercontent.com/nf-core/stableexpression/main/assets/schema_input.json", "title": "nf-core/stableexpression pipeline - params.input schema", "description": "Schema for the file provided with params.input", "type": "array", diff --git a/conf/base.config b/conf/base.config index bf07fd33..f217ab8b 100644 --- a/conf/base.config +++ b/conf/base.config @@ -15,7 +15,7 @@ process { 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) ? 'retry' : 'finish' } maxRetries = 1 maxErrors = '-1' @@ -59,4 +59,7 @@ process { errorStrategy = 'retry' maxRetries = 2 } + withLabel: process_gpu { + ext.use_gpu = { workflow.profile.contains('gpu') } + } } diff --git a/conf/igenomes.config b/conf/igenomes.config deleted file mode 100644 index 3f114377..00000000 --- a/conf/igenomes.config +++ /dev/null @@ -1,440 +0,0 @@ -/* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Nextflow config file for iGenomes paths -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Defines reference genomes using iGenome paths. - Can be used by any config that customises the base path using: - $params.igenomes_base / --igenomes_base ----------------------------------------------------------------------------------------- -*/ - -params { - // illumina iGenomes reference file paths - genomes { - 'GRCh37' { - fasta = "${params.igenomes_base}/Homo_sapiens/Ensembl/GRCh37/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Homo_sapiens/Ensembl/GRCh37/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Homo_sapiens/Ensembl/GRCh37/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Homo_sapiens/Ensembl/GRCh37/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Homo_sapiens/Ensembl/GRCh37/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Homo_sapiens/Ensembl/GRCh37/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Homo_sapiens/Ensembl/GRCh37/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Homo_sapiens/Ensembl/GRCh37/Annotation/README.txt" - mito_name = "MT" - macs_gsize = "2.7e9" - blacklist = "${projectDir}/assets/blacklists/GRCh37-blacklist.bed" - } - 'GRCh38' { - fasta = "${params.igenomes_base}/Homo_sapiens/NCBI/GRCh38/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Homo_sapiens/NCBI/GRCh38/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Homo_sapiens/NCBI/GRCh38/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Homo_sapiens/NCBI/GRCh38/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Homo_sapiens/NCBI/GRCh38/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Homo_sapiens/NCBI/GRCh38/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Homo_sapiens/NCBI/GRCh38/Annotation/Genes/genes.bed" - mito_name = "chrM" - macs_gsize = "2.7e9" - blacklist = "${projectDir}/assets/blacklists/hg38-blacklist.bed" - } - 'CHM13' { - fasta = "${params.igenomes_base}/Homo_sapiens/UCSC/CHM13/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Homo_sapiens/UCSC/CHM13/Sequence/BWAIndex/" - bwamem2 = "${params.igenomes_base}/Homo_sapiens/UCSC/CHM13/Sequence/BWAmem2Index/" - gtf = "${params.igenomes_base}/Homo_sapiens/NCBI/CHM13/Annotation/Genes/genes.gtf" - gff = "ftp://ftp.ncbi.nlm.nih.gov/genomes/all/GCF/009/914/755/GCF_009914755.1_T2T-CHM13v2.0/GCF_009914755.1_T2T-CHM13v2.0_genomic.gff.gz" - mito_name = "chrM" - } - 'GRCm38' { - fasta = "${params.igenomes_base}/Mus_musculus/Ensembl/GRCm38/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Mus_musculus/Ensembl/GRCm38/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Mus_musculus/Ensembl/GRCm38/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Mus_musculus/Ensembl/GRCm38/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Mus_musculus/Ensembl/GRCm38/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Mus_musculus/Ensembl/GRCm38/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Mus_musculus/Ensembl/GRCm38/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Mus_musculus/Ensembl/GRCm38/Annotation/README.txt" - mito_name = "MT" - macs_gsize = "1.87e9" - blacklist = "${projectDir}/assets/blacklists/GRCm38-blacklist.bed" - } - 'TAIR10' { - fasta = "${params.igenomes_base}/Arabidopsis_thaliana/Ensembl/TAIR10/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Arabidopsis_thaliana/Ensembl/TAIR10/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Arabidopsis_thaliana/Ensembl/TAIR10/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Arabidopsis_thaliana/Ensembl/TAIR10/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Arabidopsis_thaliana/Ensembl/TAIR10/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Arabidopsis_thaliana/Ensembl/TAIR10/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Arabidopsis_thaliana/Ensembl/TAIR10/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Arabidopsis_thaliana/Ensembl/TAIR10/Annotation/README.txt" - mito_name = "Mt" - } - 'EB2' { - fasta = "${params.igenomes_base}/Bacillus_subtilis_168/Ensembl/EB2/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Bacillus_subtilis_168/Ensembl/EB2/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Bacillus_subtilis_168/Ensembl/EB2/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Bacillus_subtilis_168/Ensembl/EB2/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Bacillus_subtilis_168/Ensembl/EB2/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Bacillus_subtilis_168/Ensembl/EB2/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Bacillus_subtilis_168/Ensembl/EB2/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Bacillus_subtilis_168/Ensembl/EB2/Annotation/README.txt" - } - 'UMD3.1' { - fasta = "${params.igenomes_base}/Bos_taurus/Ensembl/UMD3.1/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Bos_taurus/Ensembl/UMD3.1/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Bos_taurus/Ensembl/UMD3.1/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Bos_taurus/Ensembl/UMD3.1/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Bos_taurus/Ensembl/UMD3.1/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Bos_taurus/Ensembl/UMD3.1/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Bos_taurus/Ensembl/UMD3.1/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Bos_taurus/Ensembl/UMD3.1/Annotation/README.txt" - mito_name = "MT" - } - 'WBcel235' { - fasta = "${params.igenomes_base}/Caenorhabditis_elegans/Ensembl/WBcel235/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Caenorhabditis_elegans/Ensembl/WBcel235/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Caenorhabditis_elegans/Ensembl/WBcel235/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Caenorhabditis_elegans/Ensembl/WBcel235/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Caenorhabditis_elegans/Ensembl/WBcel235/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Caenorhabditis_elegans/Ensembl/WBcel235/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Caenorhabditis_elegans/Ensembl/WBcel235/Annotation/Genes/genes.bed" - mito_name = "MtDNA" - macs_gsize = "9e7" - } - 'CanFam3.1' { - fasta = "${params.igenomes_base}/Canis_familiaris/Ensembl/CanFam3.1/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Canis_familiaris/Ensembl/CanFam3.1/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Canis_familiaris/Ensembl/CanFam3.1/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Canis_familiaris/Ensembl/CanFam3.1/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Canis_familiaris/Ensembl/CanFam3.1/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Canis_familiaris/Ensembl/CanFam3.1/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Canis_familiaris/Ensembl/CanFam3.1/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Canis_familiaris/Ensembl/CanFam3.1/Annotation/README.txt" - mito_name = "MT" - } - 'GRCz10' { - fasta = "${params.igenomes_base}/Danio_rerio/Ensembl/GRCz10/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Danio_rerio/Ensembl/GRCz10/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Danio_rerio/Ensembl/GRCz10/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Danio_rerio/Ensembl/GRCz10/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Danio_rerio/Ensembl/GRCz10/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Danio_rerio/Ensembl/GRCz10/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Danio_rerio/Ensembl/GRCz10/Annotation/Genes/genes.bed" - mito_name = "MT" - } - 'BDGP6' { - fasta = "${params.igenomes_base}/Drosophila_melanogaster/Ensembl/BDGP6/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Drosophila_melanogaster/Ensembl/BDGP6/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Drosophila_melanogaster/Ensembl/BDGP6/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Drosophila_melanogaster/Ensembl/BDGP6/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Drosophila_melanogaster/Ensembl/BDGP6/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Drosophila_melanogaster/Ensembl/BDGP6/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Drosophila_melanogaster/Ensembl/BDGP6/Annotation/Genes/genes.bed" - mito_name = "M" - macs_gsize = "1.2e8" - } - 'EquCab2' { - fasta = "${params.igenomes_base}/Equus_caballus/Ensembl/EquCab2/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Equus_caballus/Ensembl/EquCab2/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Equus_caballus/Ensembl/EquCab2/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Equus_caballus/Ensembl/EquCab2/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Equus_caballus/Ensembl/EquCab2/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Equus_caballus/Ensembl/EquCab2/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Equus_caballus/Ensembl/EquCab2/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Equus_caballus/Ensembl/EquCab2/Annotation/README.txt" - mito_name = "MT" - } - 'EB1' { - fasta = "${params.igenomes_base}/Escherichia_coli_K_12_DH10B/Ensembl/EB1/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Escherichia_coli_K_12_DH10B/Ensembl/EB1/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Escherichia_coli_K_12_DH10B/Ensembl/EB1/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Escherichia_coli_K_12_DH10B/Ensembl/EB1/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Escherichia_coli_K_12_DH10B/Ensembl/EB1/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Escherichia_coli_K_12_DH10B/Ensembl/EB1/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Escherichia_coli_K_12_DH10B/Ensembl/EB1/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Escherichia_coli_K_12_DH10B/Ensembl/EB1/Annotation/README.txt" - } - 'Galgal4' { - fasta = "${params.igenomes_base}/Gallus_gallus/Ensembl/Galgal4/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Gallus_gallus/Ensembl/Galgal4/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Gallus_gallus/Ensembl/Galgal4/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Gallus_gallus/Ensembl/Galgal4/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Gallus_gallus/Ensembl/Galgal4/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Gallus_gallus/Ensembl/Galgal4/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Gallus_gallus/Ensembl/Galgal4/Annotation/Genes/genes.bed" - mito_name = "MT" - } - 'Gm01' { - fasta = "${params.igenomes_base}/Glycine_max/Ensembl/Gm01/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Glycine_max/Ensembl/Gm01/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Glycine_max/Ensembl/Gm01/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Glycine_max/Ensembl/Gm01/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Glycine_max/Ensembl/Gm01/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Glycine_max/Ensembl/Gm01/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Glycine_max/Ensembl/Gm01/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Glycine_max/Ensembl/Gm01/Annotation/README.txt" - } - 'Mmul_1' { - fasta = "${params.igenomes_base}/Macaca_mulatta/Ensembl/Mmul_1/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Macaca_mulatta/Ensembl/Mmul_1/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Macaca_mulatta/Ensembl/Mmul_1/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Macaca_mulatta/Ensembl/Mmul_1/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Macaca_mulatta/Ensembl/Mmul_1/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Macaca_mulatta/Ensembl/Mmul_1/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Macaca_mulatta/Ensembl/Mmul_1/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Macaca_mulatta/Ensembl/Mmul_1/Annotation/README.txt" - mito_name = "MT" - } - 'IRGSP-1.0' { - fasta = "${params.igenomes_base}/Oryza_sativa_japonica/Ensembl/IRGSP-1.0/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Oryza_sativa_japonica/Ensembl/IRGSP-1.0/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Oryza_sativa_japonica/Ensembl/IRGSP-1.0/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Oryza_sativa_japonica/Ensembl/IRGSP-1.0/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Oryza_sativa_japonica/Ensembl/IRGSP-1.0/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Oryza_sativa_japonica/Ensembl/IRGSP-1.0/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Oryza_sativa_japonica/Ensembl/IRGSP-1.0/Annotation/Genes/genes.bed" - mito_name = "Mt" - } - 'CHIMP2.1.4' { - fasta = "${params.igenomes_base}/Pan_troglodytes/Ensembl/CHIMP2.1.4/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Pan_troglodytes/Ensembl/CHIMP2.1.4/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Pan_troglodytes/Ensembl/CHIMP2.1.4/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Pan_troglodytes/Ensembl/CHIMP2.1.4/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Pan_troglodytes/Ensembl/CHIMP2.1.4/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Pan_troglodytes/Ensembl/CHIMP2.1.4/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Pan_troglodytes/Ensembl/CHIMP2.1.4/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Pan_troglodytes/Ensembl/CHIMP2.1.4/Annotation/README.txt" - mito_name = "MT" - } - 'Rnor_5.0' { - fasta = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_5.0/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_5.0/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_5.0/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_5.0/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_5.0/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_5.0/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_5.0/Annotation/Genes/genes.bed" - mito_name = "MT" - } - 'Rnor_6.0' { - fasta = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_6.0/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_6.0/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_6.0/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_6.0/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_6.0/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_6.0/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Rattus_norvegicus/Ensembl/Rnor_6.0/Annotation/Genes/genes.bed" - mito_name = "MT" - } - 'R64-1-1' { - fasta = "${params.igenomes_base}/Saccharomyces_cerevisiae/Ensembl/R64-1-1/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Saccharomyces_cerevisiae/Ensembl/R64-1-1/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Saccharomyces_cerevisiae/Ensembl/R64-1-1/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Saccharomyces_cerevisiae/Ensembl/R64-1-1/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Saccharomyces_cerevisiae/Ensembl/R64-1-1/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Saccharomyces_cerevisiae/Ensembl/R64-1-1/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Saccharomyces_cerevisiae/Ensembl/R64-1-1/Annotation/Genes/genes.bed" - mito_name = "MT" - macs_gsize = "1.2e7" - } - 'EF2' { - fasta = "${params.igenomes_base}/Schizosaccharomyces_pombe/Ensembl/EF2/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Schizosaccharomyces_pombe/Ensembl/EF2/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Schizosaccharomyces_pombe/Ensembl/EF2/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Schizosaccharomyces_pombe/Ensembl/EF2/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Schizosaccharomyces_pombe/Ensembl/EF2/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Schizosaccharomyces_pombe/Ensembl/EF2/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Schizosaccharomyces_pombe/Ensembl/EF2/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Schizosaccharomyces_pombe/Ensembl/EF2/Annotation/README.txt" - mito_name = "MT" - macs_gsize = "1.21e7" - } - 'Sbi1' { - fasta = "${params.igenomes_base}/Sorghum_bicolor/Ensembl/Sbi1/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Sorghum_bicolor/Ensembl/Sbi1/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Sorghum_bicolor/Ensembl/Sbi1/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Sorghum_bicolor/Ensembl/Sbi1/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Sorghum_bicolor/Ensembl/Sbi1/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Sorghum_bicolor/Ensembl/Sbi1/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Sorghum_bicolor/Ensembl/Sbi1/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Sorghum_bicolor/Ensembl/Sbi1/Annotation/README.txt" - } - 'Sscrofa10.2' { - fasta = "${params.igenomes_base}/Sus_scrofa/Ensembl/Sscrofa10.2/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Sus_scrofa/Ensembl/Sscrofa10.2/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Sus_scrofa/Ensembl/Sscrofa10.2/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Sus_scrofa/Ensembl/Sscrofa10.2/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Sus_scrofa/Ensembl/Sscrofa10.2/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Sus_scrofa/Ensembl/Sscrofa10.2/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Sus_scrofa/Ensembl/Sscrofa10.2/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Sus_scrofa/Ensembl/Sscrofa10.2/Annotation/README.txt" - mito_name = "MT" - } - 'AGPv3' { - fasta = "${params.igenomes_base}/Zea_mays/Ensembl/AGPv3/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Zea_mays/Ensembl/AGPv3/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Zea_mays/Ensembl/AGPv3/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Zea_mays/Ensembl/AGPv3/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Zea_mays/Ensembl/AGPv3/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Zea_mays/Ensembl/AGPv3/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Zea_mays/Ensembl/AGPv3/Annotation/Genes/genes.bed" - mito_name = "Mt" - } - 'hg38' { - fasta = "${params.igenomes_base}/Homo_sapiens/UCSC/hg38/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Homo_sapiens/UCSC/hg38/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Homo_sapiens/UCSC/hg38/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Homo_sapiens/UCSC/hg38/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Homo_sapiens/UCSC/hg38/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Homo_sapiens/UCSC/hg38/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Homo_sapiens/UCSC/hg38/Annotation/Genes/genes.bed" - mito_name = "chrM" - macs_gsize = "2.7e9" - blacklist = "${projectDir}/assets/blacklists/hg38-blacklist.bed" - } - 'hg19' { - fasta = "${params.igenomes_base}/Homo_sapiens/UCSC/hg19/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Homo_sapiens/UCSC/hg19/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Homo_sapiens/UCSC/hg19/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Homo_sapiens/UCSC/hg19/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Homo_sapiens/UCSC/hg19/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Homo_sapiens/UCSC/hg19/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Homo_sapiens/UCSC/hg19/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Homo_sapiens/UCSC/hg19/Annotation/README.txt" - mito_name = "chrM" - macs_gsize = "2.7e9" - blacklist = "${projectDir}/assets/blacklists/hg19-blacklist.bed" - } - 'mm10' { - fasta = "${params.igenomes_base}/Mus_musculus/UCSC/mm10/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Mus_musculus/UCSC/mm10/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Mus_musculus/UCSC/mm10/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Mus_musculus/UCSC/mm10/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Mus_musculus/UCSC/mm10/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Mus_musculus/UCSC/mm10/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Mus_musculus/UCSC/mm10/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Mus_musculus/UCSC/mm10/Annotation/README.txt" - mito_name = "chrM" - macs_gsize = "1.87e9" - blacklist = "${projectDir}/assets/blacklists/mm10-blacklist.bed" - } - 'bosTau8' { - fasta = "${params.igenomes_base}/Bos_taurus/UCSC/bosTau8/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Bos_taurus/UCSC/bosTau8/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Bos_taurus/UCSC/bosTau8/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Bos_taurus/UCSC/bosTau8/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Bos_taurus/UCSC/bosTau8/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Bos_taurus/UCSC/bosTau8/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Bos_taurus/UCSC/bosTau8/Annotation/Genes/genes.bed" - mito_name = "chrM" - } - 'ce10' { - fasta = "${params.igenomes_base}/Caenorhabditis_elegans/UCSC/ce10/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Caenorhabditis_elegans/UCSC/ce10/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Caenorhabditis_elegans/UCSC/ce10/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Caenorhabditis_elegans/UCSC/ce10/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Caenorhabditis_elegans/UCSC/ce10/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Caenorhabditis_elegans/UCSC/ce10/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Caenorhabditis_elegans/UCSC/ce10/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Caenorhabditis_elegans/UCSC/ce10/Annotation/README.txt" - mito_name = "chrM" - macs_gsize = "9e7" - } - 'canFam3' { - fasta = "${params.igenomes_base}/Canis_familiaris/UCSC/canFam3/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Canis_familiaris/UCSC/canFam3/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Canis_familiaris/UCSC/canFam3/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Canis_familiaris/UCSC/canFam3/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Canis_familiaris/UCSC/canFam3/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Canis_familiaris/UCSC/canFam3/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Canis_familiaris/UCSC/canFam3/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Canis_familiaris/UCSC/canFam3/Annotation/README.txt" - mito_name = "chrM" - } - 'danRer10' { - fasta = "${params.igenomes_base}/Danio_rerio/UCSC/danRer10/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Danio_rerio/UCSC/danRer10/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Danio_rerio/UCSC/danRer10/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Danio_rerio/UCSC/danRer10/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Danio_rerio/UCSC/danRer10/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Danio_rerio/UCSC/danRer10/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Danio_rerio/UCSC/danRer10/Annotation/Genes/genes.bed" - mito_name = "chrM" - macs_gsize = "1.37e9" - } - 'dm6' { - fasta = "${params.igenomes_base}/Drosophila_melanogaster/UCSC/dm6/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Drosophila_melanogaster/UCSC/dm6/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Drosophila_melanogaster/UCSC/dm6/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Drosophila_melanogaster/UCSC/dm6/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Drosophila_melanogaster/UCSC/dm6/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Drosophila_melanogaster/UCSC/dm6/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Drosophila_melanogaster/UCSC/dm6/Annotation/Genes/genes.bed" - mito_name = "chrM" - macs_gsize = "1.2e8" - } - 'equCab2' { - fasta = "${params.igenomes_base}/Equus_caballus/UCSC/equCab2/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Equus_caballus/UCSC/equCab2/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Equus_caballus/UCSC/equCab2/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Equus_caballus/UCSC/equCab2/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Equus_caballus/UCSC/equCab2/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Equus_caballus/UCSC/equCab2/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Equus_caballus/UCSC/equCab2/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Equus_caballus/UCSC/equCab2/Annotation/README.txt" - mito_name = "chrM" - } - 'galGal4' { - fasta = "${params.igenomes_base}/Gallus_gallus/UCSC/galGal4/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Gallus_gallus/UCSC/galGal4/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Gallus_gallus/UCSC/galGal4/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Gallus_gallus/UCSC/galGal4/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Gallus_gallus/UCSC/galGal4/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Gallus_gallus/UCSC/galGal4/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Gallus_gallus/UCSC/galGal4/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Gallus_gallus/UCSC/galGal4/Annotation/README.txt" - mito_name = "chrM" - } - 'panTro4' { - fasta = "${params.igenomes_base}/Pan_troglodytes/UCSC/panTro4/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Pan_troglodytes/UCSC/panTro4/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Pan_troglodytes/UCSC/panTro4/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Pan_troglodytes/UCSC/panTro4/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Pan_troglodytes/UCSC/panTro4/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Pan_troglodytes/UCSC/panTro4/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Pan_troglodytes/UCSC/panTro4/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Pan_troglodytes/UCSC/panTro4/Annotation/README.txt" - mito_name = "chrM" - } - 'rn6' { - fasta = "${params.igenomes_base}/Rattus_norvegicus/UCSC/rn6/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Rattus_norvegicus/UCSC/rn6/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Rattus_norvegicus/UCSC/rn6/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Rattus_norvegicus/UCSC/rn6/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Rattus_norvegicus/UCSC/rn6/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Rattus_norvegicus/UCSC/rn6/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Rattus_norvegicus/UCSC/rn6/Annotation/Genes/genes.bed" - mito_name = "chrM" - } - 'sacCer3' { - fasta = "${params.igenomes_base}/Saccharomyces_cerevisiae/UCSC/sacCer3/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Saccharomyces_cerevisiae/UCSC/sacCer3/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Saccharomyces_cerevisiae/UCSC/sacCer3/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Saccharomyces_cerevisiae/UCSC/sacCer3/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Saccharomyces_cerevisiae/UCSC/sacCer3/Sequence/BismarkIndex/" - readme = "${params.igenomes_base}/Saccharomyces_cerevisiae/UCSC/sacCer3/Annotation/README.txt" - mito_name = "chrM" - macs_gsize = "1.2e7" - } - 'susScr3' { - fasta = "${params.igenomes_base}/Sus_scrofa/UCSC/susScr3/Sequence/WholeGenomeFasta/genome.fa" - bwa = "${params.igenomes_base}/Sus_scrofa/UCSC/susScr3/Sequence/BWAIndex/version0.6.0/" - bowtie2 = "${params.igenomes_base}/Sus_scrofa/UCSC/susScr3/Sequence/Bowtie2Index/" - star = "${params.igenomes_base}/Sus_scrofa/UCSC/susScr3/Sequence/STARIndex/" - bismark = "${params.igenomes_base}/Sus_scrofa/UCSC/susScr3/Sequence/BismarkIndex/" - gtf = "${params.igenomes_base}/Sus_scrofa/UCSC/susScr3/Annotation/Genes/genes.gtf" - bed12 = "${params.igenomes_base}/Sus_scrofa/UCSC/susScr3/Annotation/Genes/genes.bed" - readme = "${params.igenomes_base}/Sus_scrofa/UCSC/susScr3/Annotation/README.txt" - mito_name = "chrM" - } - } -} diff --git a/conf/igenomes_ignored.config b/conf/igenomes_ignored.config deleted file mode 100644 index b4034d82..00000000 --- a/conf/igenomes_ignored.config +++ /dev/null @@ -1,9 +0,0 @@ -/* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Nextflow config file for iGenomes paths -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Empty genomes dictionary to use when igenomes is ignored. ----------------------------------------------------------------------------------------- -*/ - -params.genomes = [:] diff --git a/conf/modules.config b/conf/modules.config index d203d2b6..f0b0d55a 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -18,10 +18,6 @@ process { saveAs: { filename -> filename.equals('versions.yml') ? null : filename } ] - withName: FASTQC { - ext.args = '--quiet' - } - withName: 'MULTIQC' { ext.args = { params.multiqc_title ? "--title \"$params.multiqc_title\"" : '' } publishDir = [ diff --git a/conf/test.config b/conf/test.config index eeab819b..42de053a 100644 --- a/conf/test.config +++ b/conf/test.config @@ -25,6 +25,5 @@ params { // 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 + 'viralrecon/samplesheet/samplesheet_test_illumina_amplicon.csv' } diff --git a/conf/test_full.config b/conf/test_full.config index fa8010d7..d7bc0dad 100644 --- a/conf/test_full.config +++ b/conf/test_full.config @@ -19,6 +19,6 @@ params { // 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' - // Genome references - genome = 'R64-1-1' + // Fasta references + fasta = params.pipelines_testdata_base_path + 'viralrecon/genome/NC_045512.2/GCF_009858895.2_ASM985889v3_genomic.200409.fna.gz' } diff --git a/docs/output.md b/docs/output.md index bd33dd72..dee4caf2 100644 --- a/docs/output.md +++ b/docs/output.md @@ -12,23 +12,9 @@ The directories listed below will be created in the results directory after the The pipeline is built using [Nextflow](https://www.nextflow.io/) and processes data using the following steps: -- [FastQC](#fastqc) - Raw read QC - [MultiQC](#multiqc) - Aggregate report describing results and QC from the whole pipeline - [Pipeline information](#pipeline-information) - Report metrics generated during the workflow execution -### FastQC - -
-Output files - -- `fastqc/` - - `*_fastqc.html`: FastQC report containing quality metrics. - - `*_fastqc.zip`: Zip archive containing the FastQC report, tab-delimited data file and plot images. - -
- -[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
diff --git a/docs/usage.md b/docs/usage.md index 4b04220e..cc9d192e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -57,7 +57,7 @@ An [example samplesheet](../assets/samplesheet.csv) has been provided with the p The typical command for running the pipeline is as follows: ```bash -nextflow run nf-core/stableexpression --input ./samplesheet.csv --outdir ./results --genome GRCh37 -profile docker +nextflow run nf-core/stableexpression --input ./samplesheet.csv --outdir ./results -profile docker ``` This will launch the pipeline with the `docker` configuration profile. See below for more information about profiles. @@ -89,7 +89,6 @@ with: ```yaml title="params.yaml" input: './samplesheet.csv' outdir: './results/' -genome: 'GRCh37' <...> ``` diff --git a/main.nf b/main.nf index 71e9d065..e3c73087 100644 --- a/main.nf +++ b/main.nf @@ -18,19 +18,6 @@ include { STABLEEXPRESSION } from './workflows/stableexpression' include { PIPELINE_INITIALISATION } from './subworkflows/local/utils_nfcore_stableexpression_pipeline' include { PIPELINE_COMPLETION } from './subworkflows/local/utils_nfcore_stableexpression_pipeline' -include { getGenomeAttribute } from './subworkflows/local/utils_nfcore_stableexpression_pipeline' - -/* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - GENOME PARAMETER VALUES -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -*/ - -// 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') - /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ NAMED WORKFLOWS FOR PIPELINE diff --git a/modules.json b/modules.json index 844382b8..187b1ad8 100644 --- a/modules.json +++ b/modules.json @@ -5,11 +5,6 @@ "https://github.com/nf-core/modules.git": { "modules": { "nf-core": { - "fastqc": { - "branch": "master", - "git_sha": "08108058ea36a63f141c25c4e75f9f872a5b2296", - "installed_by": ["modules"] - }, "multiqc": { "branch": "master", "git_sha": "f0719ae309075ae4a291533883847c3f7c441dad", diff --git a/modules/nf-core/fastqc/environment.yml b/modules/nf-core/fastqc/environment.yml deleted file mode 100644 index 691d4c76..00000000 --- a/modules/nf-core/fastqc/environment.yml +++ /dev/null @@ -1,5 +0,0 @@ -channels: - - conda-forge - - bioconda -dependencies: - - bioconda::fastqc=0.12.1 diff --git a/modules/nf-core/fastqc/main.nf b/modules/nf-core/fastqc/main.nf deleted file mode 100644 index 033f4154..00000000 --- a/modules/nf-core/fastqc/main.nf +++ /dev/null @@ -1,64 +0,0 @@ -process FASTQC { - tag "${meta.id}" - label 'process_medium' - - 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' }" - - input: - tuple val(meta), path(reads) - - output: - tuple val(meta), path("*.html"), emit: html - tuple val(meta), path("*.zip") , emit: zip - 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}" - // 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 rename_to = old_new_pairs*.join(' ').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 = task.memory ? task.memory.toUnit('MB').toFloat() / task.cpus : null - // FastQC memory value allowed range (100 - 10000) - def fastqc_memory = memory_in_mb > 10000 ? 10000 : (memory_in_mb < 100 ? 100 : memory_in_mb) - - """ - 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 - """ - - stub: - def prefix = task.ext.prefix ?: "${meta.id}" - """ - 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 deleted file mode 100644 index 2b2e62b8..00000000 --- a/modules/nf-core/fastqc/meta.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: fastqc -description: Run FastQC on sequenced reads -keywords: - - quality control - - qc - - adapters - - fastq -tools: - - fastqc: - description: | - FastQC gives general quality metrics about your reads. - It provides information about the quality score distribution - across your reads, the per base sequence content (%A/C/G/T). - - You get information about adapter contamination and other - overrepresented sequences. - homepage: https://www.bioinformatics.babraham.ac.uk/projects/fastqc/ - documentation: https://www.bioinformatics.babraham.ac.uk/projects/fastqc/Help/ - licence: ["GPL-2.0-only"] - identifier: biotools:fastqc -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. -output: - - html: - - meta: - type: map - description: | - Groovy Map containing sample information - e.g. [ id:'test', single_end:false ] - - "*.html": - type: file - description: FastQC report - pattern: "*_{fastqc.html}" - - zip: - - meta: - type: map - description: | - Groovy Map containing sample information - e.g. [ id:'test', single_end:false ] - - "*.zip": - type: file - description: FastQC report archive - pattern: "*_{fastqc.zip}" - - versions: - - versions.yml: - type: file - description: File containing software versions - pattern: "versions.yml" -authors: - - "@drpatelh" - - "@grst" - - "@ewels" - - "@FelixKrueger" -maintainers: - - "@drpatelh" - - "@grst" - - "@ewels" - - "@FelixKrueger" diff --git a/modules/nf-core/fastqc/tests/main.nf.test b/modules/nf-core/fastqc/tests/main.nf.test deleted file mode 100644 index e9d79a07..00000000 --- a/modules/nf-core/fastqc/tests/main.nf.test +++ /dev/null @@ -1,309 +0,0 @@ -nextflow_process { - - name "Test Process FASTQC" - script "../main.nf" - process "FASTQC" - - tag "modules" - tag "modules_nfcore" - tag "fastqc" - - test("sarscov2 single-end [fastq]") { - - when { - process { - """ - input[0] = Channel.of([ - [ id: 'test', single_end:true ], - [ file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_1.fastq.gz', checkIfExists: true) ] - ]) - """ - } - } - - then { - assertAll ( - { assert process.success }, - // NOTE The report contains the date inside it, which means that the md5sum is stable per day, but not longer than that. So you can't md5sum it. - // looks like this:
Mon 2 Oct 2023
test.gz
- // https://github.com/nf-core/modules/pull/3903#issuecomment-1743620039 - { 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() } - ) - } - } - - test("sarscov2 paired-end [fastq]") { - - when { - process { - """ - input[0] = Channel.of([ - [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) ] - ]) - """ - } - } - - then { - assertAll ( - { assert process.success }, - { assert process.out.html[0][1][0] ==~ ".*/test_1_fastqc.html" }, - { assert process.out.html[0][1][1] ==~ ".*/test_2_fastqc.html" }, - { assert process.out.zip[0][1][0] ==~ ".*/test_1_fastqc.zip" }, - { 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() } - ) - } - } - - test("sarscov2 interleaved [fastq]") { - - when { - process { - """ - input[0] = Channel.of([ - [id: 'test', single_end: false], // meta map - file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_interleaved.fastq.gz', checkIfExists: true) - ]) - """ - } - } - - then { - assertAll ( - { assert process.success }, - { 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() } - ) - } - } - - test("sarscov2 paired-end [bam]") { - - 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 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() } - ) - } - } - - test("sarscov2 multiple [fastq]") { - - when { - process { - """ - input[0] = Channel.of([ - [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), - file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test2_1.fastq.gz', checkIfExists: true), - file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test2_2.fastq.gz', checkIfExists: true) ] - ]) - """ - } - } - - then { - assertAll ( - { assert process.success }, - { assert process.out.html[0][1][0] ==~ ".*/test_1_fastqc.html" }, - { assert process.out.html[0][1][1] ==~ ".*/test_2_fastqc.html" }, - { assert process.out.html[0][1][2] ==~ ".*/test_3_fastqc.html" }, - { assert process.out.html[0][1][3] ==~ ".*/test_4_fastqc.html" }, - { assert process.out.zip[0][1][0] ==~ ".*/test_1_fastqc.zip" }, - { assert process.out.zip[0][1][1] ==~ ".*/test_2_fastqc.zip" }, - { assert process.out.zip[0][1][2] ==~ ".*/test_3_fastqc.zip" }, - { assert process.out.zip[0][1][3] ==~ ".*/test_4_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 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() } - ) - } - } - - test("sarscov2 custom_prefix") { - - when { - process { - """ - input[0] = Channel.of([ - [ id:'mysample', single_end:true ], // meta map - file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_1.fastq.gz', checkIfExists: true) - ]) - """ - } - } - - then { - assertAll ( - { assert process.success }, - { 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() } - ) - } - } - - test("sarscov2 single-end [fastq] - stub") { - - options "-stub" - when { - process { - """ - input[0] = Channel.of([ - [ id: 'test', single_end:true ], - [ file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_1.fastq.gz', checkIfExists: true) ] - ]) - """ - } - } - - then { - assertAll ( - { assert process.success }, - { assert snapshot(process.out).match() } - ) - } - } - - test("sarscov2 paired-end [fastq] - 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/fastq/test_1.fastq.gz', checkIfExists: true), - file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_2.fastq.gz', checkIfExists: true) ] - ]) - """ - } - } - - then { - assertAll ( - { assert process.success }, - { assert snapshot(process.out).match() } - ) - } - } - - test("sarscov2 interleaved [fastq] - 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/fastq/test_interleaved.fastq.gz', checkIfExists: true) - ]) - """ - } - } - - then { - assertAll ( - { assert process.success }, - { assert snapshot(process.out).match() } - ) - } - } - - test("sarscov2 paired-end [bam] - 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).match() } - ) - } - } - - test("sarscov2 multiple [fastq] - 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/fastq/test_1.fastq.gz', checkIfExists: true), - file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_2.fastq.gz', checkIfExists: true), - file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test2_1.fastq.gz', checkIfExists: true), - file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test2_2.fastq.gz', checkIfExists: true) ] - ]) - """ - } - } - - then { - assertAll ( - { assert process.success }, - { assert snapshot(process.out).match() } - ) - } - } - - test("sarscov2 custom_prefix - stub") { - - options "-stub" - when { - process { - """ - input[0] = Channel.of([ - [ id:'mysample', single_end:true ], // meta map - file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_1.fastq.gz', checkIfExists: true) - ]) - """ - } - } - - then { - assertAll ( - { assert process.success }, - { assert snapshot(process.out).match() } - ) - } - } -} diff --git a/modules/nf-core/fastqc/tests/main.nf.test.snap b/modules/nf-core/fastqc/tests/main.nf.test.snap deleted file mode 100644 index d5db3092..00000000 --- a/modules/nf-core/fastqc/tests/main.nf.test.snap +++ /dev/null @@ -1,392 +0,0 @@ -{ - "sarscov2 custom_prefix": { - "content": [ - [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" - ] - ], - "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.3" - }, - "timestamp": "2024-07-22T11:02:16.374038" - }, - "sarscov2 single-end [fastq] - stub": { - "content": [ - { - "0": [ - [ - { - "id": "test", - "single_end": true - }, - "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ], - "1": [ - [ - { - "id": "test", - "single_end": true - }, - "test.zip:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ], - "2": [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" - ], - "html": [ - [ - { - "id": "test", - "single_end": true - }, - "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ], - "versions": [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" - ], - "zip": [ - [ - { - "id": "test", - "single_end": true - }, - "test.zip:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ] - } - ], - "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.3" - }, - "timestamp": "2024-07-22T11:02:24.993809" - }, - "sarscov2 custom_prefix - stub": { - "content": [ - { - "0": [ - [ - { - "id": "mysample", - "single_end": true - }, - "mysample.html:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ], - "1": [ - [ - { - "id": "mysample", - "single_end": true - }, - "mysample.zip:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ], - "2": [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" - ], - "html": [ - [ - { - "id": "mysample", - "single_end": true - }, - "mysample.html:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ], - "versions": [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" - ], - "zip": [ - [ - { - "id": "mysample", - "single_end": true - }, - "mysample.zip:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ] - } - ], - "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.3" - }, - "timestamp": "2024-07-22T11:03:10.93942" - }, - "sarscov2 interleaved [fastq]": { - "content": [ - [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" - ] - ], - "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.3" - }, - "timestamp": "2024-07-22T11:01:42.355718" - }, - "sarscov2 paired-end [bam]": { - "content": [ - [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" - ] - ], - "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.3" - }, - "timestamp": "2024-07-22T11:01:53.276274" - }, - "sarscov2 multiple [fastq]": { - "content": [ - [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" - ] - ], - "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.3" - }, - "timestamp": "2024-07-22T11:02:05.527626" - }, - "sarscov2 paired-end [fastq]": { - "content": [ - [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" - ] - ], - "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.3" - }, - "timestamp": "2024-07-22T11:01:31.188871" - }, - "sarscov2 paired-end [fastq] - stub": { - "content": [ - { - "0": [ - [ - { - "id": "test", - "single_end": false - }, - "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ], - "1": [ - [ - { - "id": "test", - "single_end": false - }, - "test.zip:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ], - "2": [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" - ], - "html": [ - [ - { - "id": "test", - "single_end": false - }, - "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ], - "versions": [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" - ], - "zip": [ - [ - { - "id": "test", - "single_end": false - }, - "test.zip:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ] - } - ], - "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.3" - }, - "timestamp": "2024-07-22T11:02:34.273566" - }, - "sarscov2 multiple [fastq] - stub": { - "content": [ - { - "0": [ - [ - { - "id": "test", - "single_end": false - }, - "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ], - "1": [ - [ - { - "id": "test", - "single_end": false - }, - "test.zip:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ], - "2": [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" - ], - "html": [ - [ - { - "id": "test", - "single_end": false - }, - "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ], - "versions": [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" - ], - "zip": [ - [ - { - "id": "test", - "single_end": false - }, - "test.zip:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ] - } - ], - "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.3" - }, - "timestamp": "2024-07-22T11:03:02.304411" - }, - "sarscov2 single-end [fastq]": { - "content": [ - [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" - ] - ], - "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.3" - }, - "timestamp": "2024-07-22T11:01:19.095607" - }, - "sarscov2 interleaved [fastq] - stub": { - "content": [ - { - "0": [ - [ - { - "id": "test", - "single_end": false - }, - "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ], - "1": [ - [ - { - "id": "test", - "single_end": false - }, - "test.zip:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ], - "2": [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" - ], - "html": [ - [ - { - "id": "test", - "single_end": false - }, - "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ], - "versions": [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" - ], - "zip": [ - [ - { - "id": "test", - "single_end": false - }, - "test.zip:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ] - } - ], - "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.3" - }, - "timestamp": "2024-07-22T11:02:44.640184" - }, - "sarscov2 paired-end [bam] - stub": { - "content": [ - { - "0": [ - [ - { - "id": "test", - "single_end": false - }, - "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ], - "1": [ - [ - { - "id": "test", - "single_end": false - }, - "test.zip:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ], - "2": [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" - ], - "html": [ - [ - { - "id": "test", - "single_end": false - }, - "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ], - "versions": [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" - ], - "zip": [ - [ - { - "id": "test", - "single_end": false - }, - "test.zip:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ] - } - ], - "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.3" - }, - "timestamp": "2024-07-22T11:02:53.550742" - } -} \ No newline at end of file diff --git a/nextflow.config b/nextflow.config index d301a1f8..f46d3581 100644 --- a/nextflow.config +++ b/nextflow.config @@ -13,11 +13,6 @@ params { // Input options input = null - // References - genome = null - igenomes_base = 's3://ngi-igenomes/igenomes/' - igenomes_ignore = false - // MultiQC options multiqc_config = null multiqc_title = null @@ -160,16 +155,25 @@ profiles { ] } } + 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 + +// 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/stableexpression 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/stableexpression 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/stableexpression.config" : "/dev/null" +// includeConfig params.custom_config_base && (!System.getenv('NXF_OFFLINE') || !params.custom_config_base.startsWith('http')) ? "${params.custom_config_base}/pipeline/stableexpression.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 @@ -180,8 +184,7 @@ podman.registry = 'quay.io' singularity.registry = 'quay.io' charliecloud.registry = 'quay.io' -// Load igenomes.config if required -includeConfig !params.igenomes_ignore ? 'conf/igenomes.config' : 'conf/igenomes_ignored.config' + // Export these variables to prevent local Python/R libraries from conflicting with those in the container // The JULIA depot path has been adjusted to a fixed path `/usr/local/share/julia` that needs to be used for packages in the container. @@ -241,7 +244,7 @@ manifest { homePage = 'https://github.com/nf-core/stableexpression' description = """This pipeline is dedicated to finding the most stable genes across count datasets""" mainScript = 'main.nf' - defaultBranch = 'master' + defaultBranch = 'main' nextflowVersion = '!>=24.04.2' version = '1.0dev' doi = '' @@ -249,7 +252,7 @@ manifest { // Nextflow plugins plugins { - id 'nf-schema@2.2.0' // Validation of pipeline parameters and creation of an input channel from a sample sheet + id 'nf-schema@2.3.0' // Validation of pipeline parameters and creation of an input channel from a sample sheet } validation { @@ -275,7 +278,7 @@ validation { https://doi.org/10.1038/s41587-020-0439-x * Software dependencies - https://github.com/nf-core/stableexpression/blob/master/CITATIONS.md + https://github.com/nf-core/stableexpression/blob/main/CITATIONS.md """ } summary { diff --git a/nextflow_schema.json b/nextflow_schema.json index 63e40ebf..ee89af0b 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/nf-core/stableexpression/master/nextflow_schema.json", + "$id": "https://raw.githubusercontent.com/nf-core/stableexpression/main/nextflow_schema.json", "title": "nf-core/stableexpression pipeline parameters", "description": "This pipeline is dedicated to finding the most stable genes across count datasets", "type": "object", @@ -43,45 +43,6 @@ } } }, - "reference_genome_options": { - "title": "Reference genome options", - "type": "object", - "fa_icon": "fas fa-dna", - "description": "Reference genome related files and options required for the workflow.", - "properties": { - "genome": { - "type": "string", - "description": "Name of iGenomes reference.", - "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." - }, - "fasta": { - "type": "string", - "format": "file-path", - "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.", - "fa_icon": "far fa-file-code" - }, - "igenomes_ignore": { - "type": "boolean", - "description": "Do not load the iGenomes reference config.", - "fa_icon": "fas fa-ban", - "hidden": true, - "help_text": "Do not load `igenomes.config` when running the pipeline. You may choose this option if you observe clashes between custom parameters and those supplied in `igenomes.config`." - }, - "igenomes_base": { - "type": "string", - "format": "directory-path", - "description": "The base path to the igenomes reference files", - "fa_icon": "fas fa-ban", - "hidden": true, - "default": "s3://ngi-igenomes/igenomes/" - } - } - }, "institutional_config_options": { "title": "Institutional config options", "type": "object", @@ -232,9 +193,6 @@ { "$ref": "#/$defs/input_output_options" }, - { - "$ref": "#/$defs/reference_genome_options" - }, { "$ref": "#/$defs/institutional_config_options" }, diff --git a/nf-test.config b/nf-test.config new file mode 100644 index 00000000..889df760 --- /dev/null +++ b/nf-test.config @@ -0,0 +1,24 @@ +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/**/*', 'subworkflows/nf-core/**/*' + + // 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 'nextflow.config', 'nf-test.config', 'conf/test.config', 'tests/nextflow.config', 'tests/.nftignore' + + // load the necessary plugins + plugins { + load "nft-utils@0.0.3" + } +} diff --git a/ro-crate-metadata.json b/ro-crate-metadata.json index 39afdcd8..4061aef6 100644 --- a/ro-crate-metadata.json +++ b/ro-crate-metadata.json @@ -22,8 +22,8 @@ "@id": "./", "@type": "Dataset", "creativeWorkStatus": "InProgress", - "datePublished": "2025-04-30T12:28:35+00:00", - "description": "

\n \n \n \"nf-core/stableexpression\"\n \n

\n\n[![GitHub Actions CI Status](https://github.com/nf-core/stableexpression/actions/workflows/ci.yml/badge.svg)](https://github.com/nf-core/stableexpression/actions/workflows/ci.yml)\n[![GitHub Actions Linting Status](https://github.com/nf-core/stableexpression/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/stableexpression/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/stableexpression/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/stableexpression)\n\n[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23stableexpression-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/stableexpression)[![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/stableexpression** 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/stableexpression \\\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/stableexpression/usage) and the [parameter documentation](https://nf-co.re/stableexpression/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/stableexpression/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/stableexpression/output).\n\n## Credits\n\nnf-core/stableexpression was originally written by Olivier Coen.\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 `#stableexpression` channel](https://nfcore.slack.com/channels/stableexpression) (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", + "datePublished": "2025-06-03T11:02:29+00:00", + "description": "

\n \n \n \"nf-core/stableexpression\"\n \n

\n\n[![GitHub Actions CI Status](https://github.com/nf-core/stableexpression/actions/workflows/ci.yml/badge.svg)](https://github.com/nf-core/stableexpression/actions/workflows/ci.yml)\n[![GitHub Actions Linting Status](https://github.com/nf-core/stableexpression/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/stableexpression/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/stableexpression/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%A524.04.2-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-3.3.1-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/3.3.1)\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/stableexpression)\n\n[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23stableexpression-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/stableexpression)[![Follow on Bluesky](https://img.shields.io/badge/bluesky-%40nf__core-1185fe?labelColor=000000&logo=bluesky)](https://bsky.app/profile/nf-co.re)[![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/stableexpression** is a bioinformatics pipeline that ...\n\n\n\n\n2. 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/stableexpression \\\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/stableexpression/usage) and the [parameter documentation](https://nf-co.re/stableexpression/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/stableexpression/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/stableexpression/output).\n\n## Credits\n\nnf-core/stableexpression was originally written by Olivier Coen.\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 `#stableexpression` channel](https://nfcore.slack.com/channels/stableexpression) (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", "hasPart": [ { "@id": "main.nf" @@ -99,7 +99,7 @@ }, "mentions": [ { - "@id": "#bd0c9113-aeeb-419b-8f3c-aacdc4b6b98e" + "@id": "#5c12b66f-a5bf-44fc-9e48-e3a228874d4a" } ], "name": "nf-core/stableexpression" @@ -128,7 +128,7 @@ } ], "dateCreated": "", - "dateModified": "2025-04-30T12:28:35Z", + "dateModified": "2025-06-03T11:02:29Z", "dct:conformsTo": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE/", "keywords": ["nf-core", "nextflow", "expression", "housekeeping-genes", "qpcr-analysis"], "license": ["MIT"], @@ -160,11 +160,11 @@ "version": "!>=24.04.2" }, { - "@id": "#bd0c9113-aeeb-419b-8f3c-aacdc4b6b98e", + "@id": "#5c12b66f-a5bf-44fc-9e48-e3a228874d4a", "@type": "TestSuite", "instance": [ { - "@id": "#2fd9e749-3841-494f-9e8e-4a65cd0d7f7c" + "@id": "#348ad8a0-760a-415b-a6e6-4ab3a65fd784" } ], "mainEntity": { @@ -173,10 +173,10 @@ "name": "Test suite for nf-core/stableexpression" }, { - "@id": "#2fd9e749-3841-494f-9e8e-4a65cd0d7f7c", + "@id": "#348ad8a0-760a-415b-a6e6-4ab3a65fd784", "@type": "TestInstance", "name": "GitHub Actions workflow for testing nf-core/stableexpression", - "resource": "repos/nf-core/stableexpression/actions/workflows/ci.yml", + "resource": "repos/nf-core/stableexpression/actions/workflows/nf-test.yml", "runsOn": { "@id": "https://w3id.org/ro/terms/test#GithubService" }, diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index 50b6f808..330a7ad3 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -63,11 +63,6 @@ workflow PIPELINE_INITIALISATION { nextflow_cli_args ) - // - // Custom validation for pipeline parameters - // - validateInputParameters() - // // Create channel from input file provided through params.input // @@ -150,12 +145,6 @@ workflow PIPELINE_COMPLETION { FUNCTIONS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -// -// Check and validate pipeline parameters -// -def validateInputParameters() { - genomeExistsError() -} // // Validate channels from input samplesheet @@ -172,31 +161,6 @@ def validateInputSamplesheet(input) { return [ metas[0], fastqs ] } // -// Get attribute from genome config file e.g. fasta -// -def getGenomeAttribute(attribute) { - if (params.genomes && params.genome && params.genomes.containsKey(params.genome)) { - if (params.genomes[ params.genome ].containsKey(attribute)) { - return params.genomes[ params.genome ][ attribute ] - } - } - return null -} - -// -// Exit pipeline if incorrect --genome key provided -// -def genomeExistsError() { - if (params.genomes && params.genome && !params.genomes.containsKey(params.genome)) { - def error_string = "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - " Genome '${params.genome}' not found in any config files provided to the pipeline.\n" + - " Currently, the available genome keys are:\n" + - " ${params.genomes.keySet().join(", ")}\n" + - "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" - error(error_string) - } -} -// // Generate methods description for MultiQC // def toolCitationText() { @@ -205,7 +169,6 @@ def toolCitationText() { // Uncomment function in methodsDescriptionText to render in MultiQC report def citation_text = [ "Tools used in the workflow included:", - "FastQC (Andrews 2010),", "MultiQC (Ewels et al. 2016)", "." ].join(' ').trim() @@ -218,7 +181,6 @@ def toolBibliographyText() { // 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 = [ - "
  • Andrews S, (2010) FastQC, URL: https://www.bioinformatics.babraham.ac.uk/projects/fastqc/).
  • ", "
  • Ewels, P., Magnusson, M., Lundin, S., & Käller, M. (2016). MultiQC: summarize analysis results for multiple tools and samples in a single report. Bioinformatics , 32(19), 3047–3048. doi: /10.1093/bioinformatics/btw354
  • " ].join(' ').trim() @@ -261,4 +223,3 @@ def methodsDescriptionText(mqc_methods_yaml) { return description_html.toString() } - diff --git a/tests/.nftignore b/tests/.nftignore new file mode 100644 index 00000000..dcdf312a --- /dev/null +++ b/tests/.nftignore @@ -0,0 +1,8 @@ +.DS_Store +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_plots/{svg,pdf,png}/*.{svg,pdf,png} +multiqc/multiqc_report.html +pipeline_info/*.{html,json,txt,yml} diff --git a/tests/default.nf.test b/tests/default.nf.test new file mode 100644 index 00000000..24113d3c --- /dev/null +++ b/tests/default.nf.test @@ -0,0 +1,35 @@ +nextflow_pipeline { + + name "Test pipeline" + script "../main.nf" + tag "pipeline" + + test("-profile test") { + + when { + params { + outdir = "$outputDir" + } + } + + then { + // stable_name: All files + folders in ${params.outdir}/ with a stable name + def stable_name = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}']) + // stable_path: All files in ${params.outdir}/ with stable content + def stable_path = getAllFilesFromDir(params.outdir, ignoreFile: 'tests/.nftignore') + assertAll( + { assert workflow.success}, + { assert snapshot( + // Number of successful tasks + workflow.trace.succeeded().size(), + // 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_stableexpression_software_mqc_versions.yml"), + // All stable path name, with a relative path + stable_name, + // All files with stable contents + stable_path + ).match() } + ) + } + } +} diff --git a/tests/nextflow.config b/tests/nextflow.config new file mode 100644 index 00000000..2079c3c0 --- /dev/null +++ b/tests/nextflow.config @@ -0,0 +1,12 @@ +/* +======================================================================================== + Nextflow config file for running nf-test tests +======================================================================================== +*/ + +// TODO nf-core: Specify any additional parameters here +// Or any resources requirements +params.modules_testdata_base_path = 'https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/' +params.pipelines_testdata_base_path = 'https://raw.githubusercontent.com/nf-core/test-datasets/refs/heads/stableexpression' + +aws.client.anonymous = true // fixes S3 access issues on self-hosted runners diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index aea36598..4cc7667c 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -3,7 +3,6 @@ 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' @@ -24,14 +23,6 @@ workflow STABLEEXPRESSION { 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 From 44c7b8f57bda5c79b1093cc116854382bc932335 Mon Sep 17 00:00:00 2001 From: nf-core-bot Date: Tue, 8 Jul 2025 11:39:18 +0000 Subject: [PATCH 003/258] Template update for nf-core/tools version 3.3.2 --- .github/actions/nf-test/action.yml | 4 - .github/workflows/linting.yml | 2 +- .github/workflows/linting_comment.yml | 2 +- .github/workflows/nf-test.yml | 45 +++---- .github/workflows/release-announcements.yml | 2 +- .nf-core.yml | 2 +- .pre-commit-config.yaml | 2 +- README.md | 6 +- assets/schema_input.json | 4 +- conf/base.config | 1 + modules.json | 2 +- modules/nf-core/multiqc/environment.yml | 4 +- modules/nf-core/multiqc/main.nf | 4 +- modules/nf-core/multiqc/meta.yml | 110 ++++++++++-------- .../nf-core/multiqc/tests/main.nf.test.snap | 18 +-- nextflow.config | 5 +- nf-test.config | 2 +- ro-crate-metadata.json | 16 +-- .../tests/nextflow.config | 2 +- tests/.nftignore | 1 + tests/nextflow.config | 6 +- 21 files changed, 128 insertions(+), 112 deletions(-) diff --git a/.github/actions/nf-test/action.yml b/.github/actions/nf-test/action.yml index 243e7823..bf44d961 100644 --- a/.github/actions/nf-test/action.yml +++ b/.github/actions/nf-test/action.yml @@ -54,13 +54,9 @@ runs: conda-solver: libmamba conda-remove-defaults: true - # TODO Skip failing conda tests and document their failures - # https://github.com/nf-core/modules/issues/7017 - name: Run nf-test shell: bash env: - NFT_DIFF: ${{ env.NFT_DIFF }} - NFT_DIFF_ARGS: ${{ env.NFT_DIFF_ARGS }} NFT_WORKDIR: ${{ env.NFT_WORKDIR }} run: | nf-test test \ diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index f2d7d1dd..8b0f88c3 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -13,7 +13,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - name: Set up Python 3.12 + - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.13" diff --git a/.github/workflows/linting_comment.yml b/.github/workflows/linting_comment.yml index 7e8050fb..d43797d9 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@4c1e823582f43b179e2cbb49c3eade4e41f992e2 # v10 + uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11 with: workflow: linting.yml workflow_conclusion: completed diff --git a/.github/workflows/nf-test.yml b/.github/workflows/nf-test.yml index f03aea0c..e7b58449 100644 --- a/.github/workflows/nf-test.yml +++ b/.github/workflows/nf-test.yml @@ -1,12 +1,5 @@ name: Run nf-test on: - push: - paths-ignore: - - "docs/**" - - "**/meta.yml" - - "**/*.md" - - "**/*.png" - - "**/*.svg" pull_request: paths-ignore: - "docs/**" @@ -35,7 +28,7 @@ jobs: nf-test-changes: name: nf-test-changes runs-on: # use self-hosted runners - - runs-on=$-nf-test-changes + - runs-on=${{ github.run_id }}-nf-test-changes - runner=4cpu-linux-x64 outputs: shard: ${{ steps.set-shards.outputs.shard }} @@ -69,7 +62,7 @@ jobs: needs: [nf-test-changes] if: ${{ needs.nf-test-changes.outputs.total_shards != '0' }} runs-on: # use self-hosted runners - - runs-on=$-nf-test + - runs-on=${{ github.run_id }}-nf-test - runner=4cpu-linux-x64 strategy: fail-fast: false @@ -85,7 +78,7 @@ jobs: - isMain: false profile: "singularity" NXF_VER: - - "24.04.2" + - "24.10.5" - "latest-everything" env: NXF_ANSI_LOG: false @@ -97,23 +90,39 @@ jobs: 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_DIFF: ${{ env.NFT_DIFF }} - NFT_DIFF_ARGS: ${{ env.NFT_DIFF_ARGS }} NFT_WORKDIR: ${{ env.NFT_WORKDIR }} 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=$-confirm-pass + - runs-on=${{ github.run_id }}-confirm-pass - runner=2cpu-linux-x64 steps: - - name: One or more tests failed + - name: One or more tests failed (excluding latest-everything) if: ${{ contains(needs.*.result, 'failure') }} run: exit 1 @@ -132,11 +141,3 @@ jobs: echo "DEBUG: toJSON(needs) = ${{ toJSON(needs) }}" echo "DEBUG: toJSON(needs.*.result) = ${{ toJSON(needs.*.result) }}" echo "::endgroup::" - - - name: Clean Workspace # Purge the workspace in case it's running on a self-hosted runner - if: always() - run: | - ls -la ./ - rm -rf ./* || true - rm -rf ./.??* || true - ls -la ./ diff --git a/.github/workflows/release-announcements.yml b/.github/workflows/release-announcements.yml index 4abaf484..0f732495 100644 --- a/.github/workflows/release-announcements.yml +++ b/.github/workflows/release-announcements.yml @@ -30,7 +30,7 @@ jobs: bsky-post: runs-on: ubuntu-latest steps: - - uses: zentered/bluesky-post-action@4aa83560bb3eac05dbad1e5f221ee339118abdd2 # v0.2.0 + - uses: zentered/bluesky-post-action@6461056ea355ea43b977e149f7bf76aaa572e5e8 # v0.3.0 with: post: | Pipeline release! ${{ github.repository }} v${{ github.event.release.tag_name }} - ${{ github.event.release.name }}! diff --git a/.nf-core.yml b/.nf-core.yml index 263a8a4e..8c90f6b3 100644 --- a/.nf-core.yml +++ b/.nf-core.yml @@ -10,7 +10,7 @@ lint: nextflow_config: - params.input schema_lint: false -nf_core_version: 3.3.1 +nf_core_version: 3.3.2 repository_type: pipeline template: author: Olivier Coen diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9d0b248d..bb41beec 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ repos: hooks: - id: prettier additional_dependencies: - - prettier@3.5.0 + - prettier@3.6.2 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: diff --git a/README.md b/README.md index 208e9b38..90f3a39f 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,12 @@ -[![GitHub Actions CI Status](https://github.com/nf-core/stableexpression/actions/workflows/ci.yml/badge.svg)](https://github.com/nf-core/stableexpression/actions/workflows/ci.yml) +[![GitHub Actions CI Status](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml/badge.svg)](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml) [![GitHub Actions Linting Status](https://github.com/nf-core/stableexpression/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/stableexpression/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/stableexpression/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/version-%E2%89%A524.04.2-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-3.3.1-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/3.3.1) +[![Nextflow](https://img.shields.io/badge/version-%E2%89%A524.10.5-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-3.3.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/3.3.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/) diff --git a/assets/schema_input.json b/assets/schema_input.json index 4e192cde..8618f24a 100644 --- a/assets/schema_input.json +++ b/assets/schema_input.json @@ -17,14 +17,14 @@ "type": "string", "format": "file-path", "exists": true, - "pattern": "^\\S+\\.f(ast)?q\\.gz$", + "pattern": "^([\\S\\s]*\\/)?[^\\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'" }, "fastq_2": { "type": "string", "format": "file-path", "exists": true, - "pattern": "^\\S+\\.f(ast)?q\\.gz$", + "pattern": "^([\\S\\s]*\\/)?[^\\s\\/]+\\.f(ast)?q\\.gz$", "errorMessage": "FastQ file for reads 2 cannot contain spaces and must have extension '.fq.gz' or '.fastq.gz'" } }, diff --git a/conf/base.config b/conf/base.config index f217ab8b..bfc5fe6d 100644 --- a/conf/base.config +++ b/conf/base.config @@ -61,5 +61,6 @@ process { } withLabel: process_gpu { ext.use_gpu = { workflow.profile.contains('gpu') } + accelerator = { workflow.profile.contains('gpu') ? 1 : null } } } diff --git a/modules.json b/modules.json index 187b1ad8..2cad52cd 100644 --- a/modules.json +++ b/modules.json @@ -7,7 +7,7 @@ "nf-core": { "multiqc": { "branch": "master", - "git_sha": "f0719ae309075ae4a291533883847c3f7c441dad", + "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", "installed_by": ["modules"] } } diff --git a/modules/nf-core/multiqc/environment.yml b/modules/nf-core/multiqc/environment.yml index a27122ce..812fc4c5 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.27 + - bioconda::multiqc=1.29 diff --git a/modules/nf-core/multiqc/main.nf b/modules/nf-core/multiqc/main.nf index 58d9313c..0ac3c369 100644 --- a/modules/nf-core/multiqc/main.nf +++ b/modules/nf-core/multiqc/main.nf @@ -3,8 +3,8 @@ process MULTIQC { conda "${moduleDir}/environment.yml" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/multiqc:1.27--pyhdfd78af_0' : - 'biocontainers/multiqc:1.27--pyhdfd78af_0' }" + 'https://depot.galaxyproject.org/singularity/multiqc:1.29--pyhdfd78af_0' : + 'biocontainers/multiqc:1.29--pyhdfd78af_0' }" input: path multiqc_files, stageAs: "?/*" diff --git a/modules/nf-core/multiqc/meta.yml b/modules/nf-core/multiqc/meta.yml index b16c1879..ce30eb73 100644 --- a/modules/nf-core/multiqc/meta.yml +++ b/modules/nf-core/multiqc/meta.yml @@ -15,57 +15,71 @@ tools: licence: ["GPL-3.0-or-later"] identifier: biotools:multiqc input: - - - multiqc_files: - type: file - description: | - List of reports / files recognised by MultiQC, for example the html and zip output of FastQC - - - 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: + - multiqc_files: + type: file + description: | + List of reports / files recognised by MultiQC, for example the html and zip output of FastQC + ontologies: [] + - multiqc_config: + type: file + description: Optional config yml for MultiQC + pattern: "*.{yml,yaml}" + ontologies: + - edam: http://edamontology.org/format_3750 # YAML + - extra_multiqc_config: + type: file + description: Second optional config yml for MultiQC. Will override common sections + in multiqc_config. + pattern: "*.{yml,yaml}" + ontologies: + - edam: http://edamontology.org/format_3750 # YAML + - multiqc_logo: + type: file + description: Optional logo file for MultiQC + pattern: "*.{png}" + 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}" + ontologies: + - edam: http://edamontology.org/format_3475 # TSV + - 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 # TSV +output: + report: + - "*multiqc_report.html": type: file - description: Optional logo file for MultiQC - pattern: "*.{png}" - - - replace_names: + description: MultiQC report file + pattern: "multiqc_report.html" + ontologies: [] + data: + - "*_data": + type: directory + description: MultiQC data dir + pattern: "multiqc_data" + plots: + - "*_plots": 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: + description: Plots created by MultiQC + pattern: "*_data" + ontologies: [] + versions: + - versions.yml: type: file - description: | - Optional TSV file with headers, passed to the MultiQC --sample_names - argument. - pattern: "*.{tsv}" -output: - - report: - - "*multiqc_report.html": - type: file - description: MultiQC report file - pattern: "multiqc_report.html" - - data: - - "*_data": - type: directory - description: MultiQC data dir - pattern: "multiqc_data" - - plots: - - "*_plots": - type: file - description: Plots created by MultiQC - pattern: "*_data" - - versions: - - versions.yml: - type: file - description: File containing software versions - pattern: "versions.yml" + description: File containing software versions + pattern: "versions.yml" + ontologies: + - edam: http://edamontology.org/format_3750 # YAML authors: - "@abhi18av" - "@bunop" diff --git a/modules/nf-core/multiqc/tests/main.nf.test.snap b/modules/nf-core/multiqc/tests/main.nf.test.snap index 7b7c1322..88e90571 100644 --- a/modules/nf-core/multiqc/tests/main.nf.test.snap +++ b/modules/nf-core/multiqc/tests/main.nf.test.snap @@ -2,14 +2,14 @@ "multiqc_versions_single": { "content": [ [ - "versions.yml:md5,8f3b8c1cec5388cf2708be948c9fa42f" + "versions.yml:md5,c1fe644a37468f6dae548d98bc72c2c1" ] ], "meta": { "nf-test": "0.9.2", - "nextflow": "24.10.4" + "nextflow": "25.04.2" }, - "timestamp": "2025-01-27T09:29:57.631982377" + "timestamp": "2025-05-22T11:50:41.182332996" }, "multiqc_stub": { "content": [ @@ -17,25 +17,25 @@ "multiqc_report.html", "multiqc_data", "multiqc_plots", - "versions.yml:md5,8f3b8c1cec5388cf2708be948c9fa42f" + "versions.yml:md5,c1fe644a37468f6dae548d98bc72c2c1" ] ], "meta": { "nf-test": "0.9.2", - "nextflow": "24.10.4" + "nextflow": "25.04.2" }, - "timestamp": "2025-01-27T09:30:34.743726958" + "timestamp": "2025-05-22T11:51:22.448739369" }, "multiqc_versions_config": { "content": [ [ - "versions.yml:md5,8f3b8c1cec5388cf2708be948c9fa42f" + "versions.yml:md5,c1fe644a37468f6dae548d98bc72c2c1" ] ], "meta": { "nf-test": "0.9.2", - "nextflow": "24.10.4" + "nextflow": "25.04.2" }, - "timestamp": "2025-01-27T09:30:21.44383553" + "timestamp": "2025-05-22T11:51:06.198928424" } } \ No newline at end of file diff --git a/nextflow.config b/nextflow.config index f46d3581..3a105e71 100644 --- a/nextflow.config +++ b/nextflow.config @@ -229,7 +229,6 @@ dag { manifest { name = 'nf-core/stableexpression' - author = """Olivier Coen""" // The author field is deprecated from Nextflow version 24.10.0, use contributors instead contributors = [ // TODO nf-core: Update the field with the details of the contributors to your pipeline. New with Nextflow version 24.10.0 [ @@ -245,14 +244,14 @@ manifest { description = """This pipeline is dedicated to finding the most stable genes across count datasets""" mainScript = 'main.nf' defaultBranch = 'main' - nextflowVersion = '!>=24.04.2' + nextflowVersion = '!>=24.10.5' version = '1.0dev' 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.4.2' // Validation of pipeline parameters and creation of an input channel from a sample sheet } validation { diff --git a/nf-test.config b/nf-test.config index 889df760..3a1fff59 100644 --- a/nf-test.config +++ b/nf-test.config @@ -9,7 +9,7 @@ config { configFile "tests/nextflow.config" // ignore tests coming from the nf-core/modules repo - ignore 'modules/nf-core/**/*', 'subworkflows/nf-core/**/*' + ignore 'modules/nf-core/**/tests/*', 'subworkflows/nf-core/**/tests/*' // run all test with defined profile(s) from the main nextflow.config profile "test" diff --git a/ro-crate-metadata.json b/ro-crate-metadata.json index 4061aef6..a9deb47e 100644 --- a/ro-crate-metadata.json +++ b/ro-crate-metadata.json @@ -22,8 +22,8 @@ "@id": "./", "@type": "Dataset", "creativeWorkStatus": "InProgress", - "datePublished": "2025-06-03T11:02:29+00:00", - "description": "

    \n \n \n \"nf-core/stableexpression\"\n \n

    \n\n[![GitHub Actions CI Status](https://github.com/nf-core/stableexpression/actions/workflows/ci.yml/badge.svg)](https://github.com/nf-core/stableexpression/actions/workflows/ci.yml)\n[![GitHub Actions Linting Status](https://github.com/nf-core/stableexpression/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/stableexpression/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/stableexpression/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%A524.04.2-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-3.3.1-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/3.3.1)\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/stableexpression)\n\n[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23stableexpression-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/stableexpression)[![Follow on Bluesky](https://img.shields.io/badge/bluesky-%40nf__core-1185fe?labelColor=000000&logo=bluesky)](https://bsky.app/profile/nf-co.re)[![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/stableexpression** is a bioinformatics pipeline that ...\n\n\n\n\n2. 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/stableexpression \\\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/stableexpression/usage) and the [parameter documentation](https://nf-co.re/stableexpression/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/stableexpression/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/stableexpression/output).\n\n## Credits\n\nnf-core/stableexpression was originally written by Olivier Coen.\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 `#stableexpression` channel](https://nfcore.slack.com/channels/stableexpression) (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", + "datePublished": "2025-07-08T11:39:13+00:00", + "description": "

    \n \n \n \"nf-core/stableexpression\"\n \n

    \n\n[![GitHub Actions CI Status](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml/badge.svg)](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml)\n[![GitHub Actions Linting Status](https://github.com/nf-core/stableexpression/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/stableexpression/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/stableexpression/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%A524.10.5-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-3.3.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/3.3.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/stableexpression)\n\n[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23stableexpression-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/stableexpression)[![Follow on Bluesky](https://img.shields.io/badge/bluesky-%40nf__core-1185fe?labelColor=000000&logo=bluesky)](https://bsky.app/profile/nf-co.re)[![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/stableexpression** is a bioinformatics pipeline that ...\n\n\n\n\n2. 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/stableexpression \\\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/stableexpression/usage) and the [parameter documentation](https://nf-co.re/stableexpression/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/stableexpression/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/stableexpression/output).\n\n## Credits\n\nnf-core/stableexpression was originally written by Olivier Coen.\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 `#stableexpression` channel](https://nfcore.slack.com/channels/stableexpression) (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", "hasPart": [ { "@id": "main.nf" @@ -99,7 +99,7 @@ }, "mentions": [ { - "@id": "#5c12b66f-a5bf-44fc-9e48-e3a228874d4a" + "@id": "#6a761be9-1032-4274-bb18-a7fd479235bb" } ], "name": "nf-core/stableexpression" @@ -128,7 +128,7 @@ } ], "dateCreated": "", - "dateModified": "2025-06-03T11:02:29Z", + "dateModified": "2025-07-08T11:39:13Z", "dct:conformsTo": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE/", "keywords": ["nf-core", "nextflow", "expression", "housekeeping-genes", "qpcr-analysis"], "license": ["MIT"], @@ -157,14 +157,14 @@ "url": { "@id": "https://www.nextflow.io/" }, - "version": "!>=24.04.2" + "version": "!>=24.10.5" }, { - "@id": "#5c12b66f-a5bf-44fc-9e48-e3a228874d4a", + "@id": "#6a761be9-1032-4274-bb18-a7fd479235bb", "@type": "TestSuite", "instance": [ { - "@id": "#348ad8a0-760a-415b-a6e6-4ab3a65fd784" + "@id": "#50726c2b-7779-46d7-9d16-a3709a6c7d67" } ], "mainEntity": { @@ -173,7 +173,7 @@ "name": "Test suite for nf-core/stableexpression" }, { - "@id": "#348ad8a0-760a-415b-a6e6-4ab3a65fd784", + "@id": "#50726c2b-7779-46d7-9d16-a3709a6c7d67", "@type": "TestInstance", "name": "GitHub Actions workflow for testing nf-core/stableexpression", "resource": "repos/nf-core/stableexpression/actions/workflows/nf-test.yml", diff --git a/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow.config b/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow.config index 0907ac58..09ef842a 100644 --- a/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow.config +++ b/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow.config @@ -1,5 +1,5 @@ plugins { - id "nf-schema@2.1.0" + id "nf-schema@2.4.2" } validation { diff --git a/tests/.nftignore b/tests/.nftignore index dcdf312a..16409f40 100644 --- a/tests/.nftignore +++ b/tests/.nftignore @@ -1,4 +1,5 @@ .DS_Store +multiqc/multiqc_data/BETA-multiqc.parquet multiqc/multiqc_data/multiqc.log multiqc/multiqc_data/multiqc_data.json multiqc/multiqc_data/multiqc_sources.txt diff --git a/tests/nextflow.config b/tests/nextflow.config index 2079c3c0..dd96159d 100644 --- a/tests/nextflow.config +++ b/tests/nextflow.config @@ -6,7 +6,9 @@ // TODO nf-core: Specify any additional parameters here // Or any resources requirements -params.modules_testdata_base_path = 'https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/' -params.pipelines_testdata_base_path = 'https://raw.githubusercontent.com/nf-core/test-datasets/refs/heads/stableexpression' +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/stableexpression' +} aws.client.anonymous = true // fixes S3 access issues on self-hosted runners From aa7f87b8ee33781deb9d77a91486f138e8be2f8f Mon Sep 17 00:00:00 2001 From: nf-core-bot Date: Thu, 16 Oct 2025 13:39:12 +0000 Subject: [PATCH 004/258] Template update for nf-core/tools version 3.4.1 --- .devcontainer/devcontainer.json | 28 ++++----- .devcontainer/setup.sh | 13 ++++ .github/actions/nf-test/action.yml | 6 +- .github/workflows/awsfulltest.yml | 12 ++-- .github/workflows/awstest.yml | 12 ++-- .github/workflows/clean-up.yml | 2 +- .github/workflows/download_pipeline.yml | 6 +- .github/workflows/fix_linting.yml | 16 ++--- .github/workflows/linting.yml | 14 ++--- .github/workflows/linting_comment.yml | 2 +- .github/workflows/nf-test.yml | 9 +-- .github/workflows/release-announcements.yml | 7 +++ .../workflows/template-version-comment.yml | 2 +- .gitpod.yml | 10 --- .nf-core.yml | 2 +- .pre-commit-config.yaml | 2 +- .prettierignore | 1 + README.md | 5 +- docs/usage.md | 2 +- main.nf | 5 +- modules.json | 8 +-- modules/nf-core/multiqc/environment.yml | 2 +- modules/nf-core/multiqc/main.nf | 4 +- .../nf-core/multiqc/tests/main.nf.test.snap | 18 +++--- modules/nf-core/multiqc/tests/tags.yml | 2 - nextflow.config | 63 ++++++------------- nextflow_schema.json | 12 ++++ ro-crate-metadata.json | 16 ++--- .../main.nf | 31 ++++++++- .../utils_nextflow_pipeline/tests/tags.yml | 2 - .../utils_nfcore_pipeline/tests/tags.yml | 2 - .../nf-core/utils_nfschema_plugin/main.nf | 40 ++++++++++-- .../utils_nfschema_plugin/tests/main.nf.test | 56 +++++++++++++++++ .../tests/nextflow.config | 4 +- tests/.nftignore | 3 +- tests/default.nf.test | 2 - 36 files changed, 265 insertions(+), 156 deletions(-) create mode 100755 .devcontainer/setup.sh delete mode 100644 .gitpod.yml delete mode 100644 modules/nf-core/multiqc/tests/tags.yml delete mode 100644 subworkflows/nf-core/utils_nextflow_pipeline/tests/tags.yml delete mode 100644 subworkflows/nf-core/utils_nfcore_pipeline/tests/tags.yml diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b290e090..97c8c97f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,20 +1,20 @@ { "name": "nfcore", - "image": "nfcore/gitpod:latest", - "remoteUser": "gitpod", - "runArgs": ["--privileged"], + "image": "nfcore/devcontainer:latest", - // Configure tool-specific properties. - "customizations": { - // Configure properties specific to VS Code. - "vscode": { - // Set *default* container specific settings.json values on container create. - "settings": { - "python.defaultInterpreterPath": "/opt/conda/bin/python" - }, + "remoteUser": "root", + "privileged": true, - // Add the IDs of extensions you want installed when the container is created. - "extensions": ["ms-python.python", "ms-python.vscode-pylance", "nf-core.nf-core-extensionpack"] - } + "remoteEnv": { + // Workspace path on the host for mounting with docker-outside-of-docker + "LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}" + }, + + "onCreateCommand": "./.devcontainer/setup.sh", + + "hostRequirements": { + "cpus": 4, + "memory": "16gb", + "storage": "32gb" } } diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh new file mode 100755 index 00000000..f9b8e3f2 --- /dev/null +++ b/.devcontainer/setup.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +# Customise the terminal command prompt +echo "export PROMPT_DIRTRIM=2" >> $HOME/.bashrc +echo "export PS1='\[\e[3;36m\]\w ->\[\e[0m\\] '" >> $HOME/.bashrc +export PROMPT_DIRTRIM=2 +export PS1='\[\e[3;36m\]\w ->\[\e[0m\\] ' + +# Update Nextflow +nextflow self-update + +# Update welcome message +echo "Welcome to the nf-core/stableexpression devcontainer!" > /usr/local/etc/vscode-dev-containers/first-run-notice.txt diff --git a/.github/actions/nf-test/action.yml b/.github/actions/nf-test/action.yml index bf44d961..3b9724c7 100644 --- a/.github/actions/nf-test/action.yml +++ b/.github/actions/nf-test/action.yml @@ -25,9 +25,9 @@ runs: version: "${{ env.NXF_VERSION }}" - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6 with: - python-version: "3.13" + python-version: "3.14" - name: Install nf-test uses: nf-core/setup-nf-test@v1 @@ -52,6 +52,8 @@ runs: with: auto-update-conda: true conda-solver: libmamba + channels: conda-forge + channel-priority: strict conda-remove-defaults: true - name: Run nf-test diff --git a/.github/workflows/awsfulltest.yml b/.github/workflows/awsfulltest.yml index a73aa7a8..59b87e9f 100644 --- a/.github/workflows/awsfulltest.yml +++ b/.github/workflows/awsfulltest.yml @@ -28,15 +28,15 @@ jobs: # 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 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: ${{ steps.revision.outputs.revision }} - workdir: s3://${{ secrets.AWS_S3_BUCKET }}/work/stableexpression/work-${{ steps.revision.outputs.revision }} + workdir: s3://${{ vars.AWS_S3_BUCKET }}/work/stableexpression/work-${{ steps.revision.outputs.revision }} parameters: | { "hook_url": "${{ secrets.MEGATESTS_ALERTS_SLACK_HOOK_URL }}", - "outdir": "s3://${{ secrets.AWS_S3_BUCKET }}/stableexpression/results-${{ steps.revision.outputs.revision }}" + "outdir": "s3://${{ vars.AWS_S3_BUCKET }}/stableexpression/results-${{ steps.revision.outputs.revision }}" } profiles: test_full @@ -44,5 +44,5 @@ jobs: 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 174c3ef3..6700b805 100644 --- a/.github/workflows/awstest.yml +++ b/.github/workflows/awstest.yml @@ -14,14 +14,14 @@ jobs: - name: Launch workflow via Seqera Platform uses: seqeralabs/action-tower-launch@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/stableexpression/work-${{ github.sha }} + workdir: s3://${{ vars.AWS_S3_BUCKET }}/work/stableexpression/work-${{ github.sha }} parameters: | { - "outdir": "s3://${{ secrets.AWS_S3_BUCKET }}/stableexpression/results-test-${{ github.sha }}" + "outdir": "s3://${{ vars.AWS_S3_BUCKET }}/stableexpression/results-test-${{ github.sha }}" } profiles: test @@ -29,5 +29,5 @@ jobs: 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/clean-up.yml b/.github/workflows/clean-up.yml index ac030fd5..6adb0fff 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@5bef64f19d7facfb25b37b414482c7164d639639 # v9 + - uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # 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 999bcc38..6d94bcbf 100644 --- a/.github/workflows/download_pipeline.yml +++ b/.github/workflows/download_pipeline.yml @@ -44,9 +44,9 @@ jobs: - name: Disk space cleanup uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6 with: - python-version: "3.13" + python-version: "3.14" architecture: "x64" - name: Setup Apptainer @@ -57,7 +57,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install git+https://github.com/nf-core/tools.git@dev + pip install git+https://github.com/nf-core/tools.git - name: Make a cache directory for the container images run: | diff --git a/.github/workflows/fix_linting.yml b/.github/workflows/fix_linting.yml index 133936ad..cf35e844 100644 --- a/.github/workflows/fix_linting.yml +++ b/.github/workflows/fix_linting.yml @@ -13,13 +13,13 @@ jobs: runs-on: ubuntu-latest steps: # Use the @nf-core-bot token to check out so we can push later - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 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@71345be0265236311c031f5c7866368bd1eff043 # v4 + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5 with: comment-id: ${{ github.event.comment.id }} reactions: eyes @@ -32,9 +32,9 @@ jobs: GITHUB_TOKEN: ${{ secrets.nf_core_bot_auth_token }} # Install and run pre-commit - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6 with: - python-version: "3.13" + python-version: "3.14" - name: Install pre-commit run: pip install pre-commit @@ -47,7 +47,7 @@ jobs: # indication that the linting has finished - name: react if linting finished succesfully if: steps.pre-commit.outcome == 'success' - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4 + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5 with: comment-id: ${{ github.event.comment.id }} reactions: "+1" @@ -67,21 +67,21 @@ jobs: - 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@71345be0265236311c031f5c7866368bd1eff043 # v4 + 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@71345be0265236311c031f5c7866368bd1eff043 # v4 + 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@71345be0265236311c031f5c7866368bd1eff043 # v4 + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5 with: issue-number: ${{ github.event.issue.number }} body: | diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 8b0f88c3..30e66026 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -11,12 +11,12 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 - - name: Set up Python 3.13 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + - name: Set up Python 3.14 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6 with: - python-version: "3.13" + python-version: "3.14" - name: Install pre-commit run: pip install pre-commit @@ -28,14 +28,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out pipeline code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 - name: Install Nextflow uses: nf-core/setup-nextflow@v2 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6 with: - python-version: "3.13" + python-version: "3.14" architecture: "x64" - name: read .nf-core.yml diff --git a/.github/workflows/linting_comment.yml b/.github/workflows/linting_comment.yml index d43797d9..e6e9bc26 100644 --- a/.github/workflows/linting_comment.yml +++ b/.github/workflows/linting_comment.yml @@ -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@52423e01640425a022ef5fd42c6fb5f633a02728 # v2 + uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2 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 index e7b58449..e20bf6d0 100644 --- a/.github/workflows/nf-test.yml +++ b/.github/workflows/nf-test.yml @@ -18,7 +18,7 @@ concurrency: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NFT_VER: "0.9.2" + NFT_VER: "0.9.3" NFT_WORKDIR: "~" NXF_ANSI_LOG: false NXF_SINGULARITY_CACHEDIR: ${{ github.workspace }}/.singularity @@ -40,7 +40,7 @@ jobs: rm -rf ./* || true rm -rf ./.??* || true ls -la ./ - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: fetch-depth: 0 @@ -78,14 +78,14 @@ jobs: - isMain: false profile: "singularity" NXF_VER: - - "24.10.5" + - "25.04.0" - "latest-everything" env: NXF_ANSI_LOG: false TOTAL_SHARDS: ${{ needs.nf-test-changes.outputs.total_shards }} steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: fetch-depth: 0 @@ -95,6 +95,7 @@ jobs: 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 }} diff --git a/.github/workflows/release-announcements.yml b/.github/workflows/release-announcements.yml index 0f732495..e64cebd6 100644 --- a/.github/workflows/release-announcements.yml +++ b/.github/workflows/release-announcements.yml @@ -14,6 +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 + - name: get description + id: get_topics + 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@master with: access-token: ${{ secrets.MASTODON_ACCESS_TOKEN }} @@ -23,6 +28,8 @@ jobs: message: | Pipeline release! ${{ github.repository }} v${{ github.event.release.tag_name }} - ${{ github.event.release.name }}! + ${{ steps.get_topics.outputs.description }} + Please see the changelog: ${{ github.event.release.html_url }} ${{ steps.get_topics.outputs.topics }} #nfcore #openscience #nextflow #bioinformatics diff --git a/.github/workflows/template-version-comment.yml b/.github/workflows/template-version-comment.yml index beb5c77f..c5988af9 100644 --- a/.github/workflows/template-version-comment.yml +++ b/.github/workflows/template-version-comment.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out pipeline code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: ref: ${{ github.event.pull_request.head.sha }} diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index 83599f63..00000000 --- a/.gitpod.yml +++ /dev/null @@ -1,10 +0,0 @@ -image: nfcore/gitpod:latest -tasks: - - name: Update Nextflow and setup pre-commit - command: | - pre-commit install --install-hooks - nextflow self-update - -vscode: - extensions: - - nf-core.nf-core-extensionpack # https://github.com/nf-core/vscode-extensionpack diff --git a/.nf-core.yml b/.nf-core.yml index 8c90f6b3..328ce751 100644 --- a/.nf-core.yml +++ b/.nf-core.yml @@ -10,7 +10,7 @@ lint: nextflow_config: - params.input schema_lint: false -nf_core_version: 3.3.2 +nf_core_version: 3.4.1 repository_type: pipeline template: author: Olivier Coen diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bb41beec..d06777a8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ repos: additional_dependencies: - prettier@3.6.2 - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: trailing-whitespace args: [--markdown-linebreak-ext=md] diff --git a/.prettierignore b/.prettierignore index edd29f01..2255e3e3 100644 --- a/.prettierignore +++ b/.prettierignore @@ -10,4 +10,5 @@ testing/ testing* *.pyc bin/ +.nf-test/ ro-crate-metadata.json diff --git a/README.md b/README.md index 90f3a39f..ce6f85ca 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,13 @@ +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new/nf-core/stableexpression) [![GitHub Actions CI Status](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml/badge.svg)](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml) [![GitHub Actions Linting Status](https://github.com/nf-core/stableexpression/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/stableexpression/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/stableexpression/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/version-%E2%89%A524.10.5-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-3.3.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/3.3.2) +[![Nextflow](https://img.shields.io/badge/version-%E2%89%A525.04.0-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-3.4.1-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/3.4.1) [![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/) diff --git a/docs/usage.md b/docs/usage.md index cc9d192e..6ee23ca6 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -148,7 +148,7 @@ If `-profile` is not specified, the pipeline will run locally and expect all sof - `shifter` - A generic configuration profile to be used with [Shifter](https://nersc.gitlab.io/development/shifter/how-to-use/) - `charliecloud` - - A generic configuration profile to be used with [Charliecloud](https://hpc.github.io/charliecloud/) + - A generic configuration profile to be used with [Charliecloud](https://charliecloud.io/) - `apptainer` - A generic configuration profile to be used with [Apptainer](https://apptainer.org/) - `wave` diff --git a/main.nf b/main.nf index e3c73087..40987e52 100644 --- a/main.nf +++ b/main.nf @@ -61,7 +61,10 @@ workflow { params.monochrome_logs, args, params.outdir, - params.input + params.input, + params.help, + params.help_full, + params.show_hidden ) // diff --git a/modules.json b/modules.json index 2cad52cd..c1c2b2e8 100644 --- a/modules.json +++ b/modules.json @@ -7,7 +7,7 @@ "nf-core": { "multiqc": { "branch": "master", - "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", + "git_sha": "e10b76ca0c66213581bec2833e30d31f239dec0b", "installed_by": ["modules"] } } @@ -16,17 +16,17 @@ "nf-core": { "utils_nextflow_pipeline": { "branch": "master", - "git_sha": "c2b22d85f30a706a3073387f30380704fcae013b", + "git_sha": "05954dab2ff481bcb999f24455da29a5828af08d", "installed_by": ["subworkflows"] }, "utils_nfcore_pipeline": { "branch": "master", - "git_sha": "51ae5406a030d4da1e49e4dab49756844fdd6c7a", + "git_sha": "05954dab2ff481bcb999f24455da29a5828af08d", "installed_by": ["subworkflows"] }, "utils_nfschema_plugin": { "branch": "master", - "git_sha": "2fd2cd6d0e7b273747f32e465fdc6bcc3ae0814e", + "git_sha": "4b406a74dc0449c0401ed87d5bfff4252fd277fd", "installed_by": ["subworkflows"] } } diff --git a/modules/nf-core/multiqc/environment.yml b/modules/nf-core/multiqc/environment.yml index 812fc4c5..dd513cbd 100644 --- a/modules/nf-core/multiqc/environment.yml +++ b/modules/nf-core/multiqc/environment.yml @@ -4,4 +4,4 @@ channels: - conda-forge - bioconda dependencies: - - bioconda::multiqc=1.29 + - bioconda::multiqc=1.31 diff --git a/modules/nf-core/multiqc/main.nf b/modules/nf-core/multiqc/main.nf index 0ac3c369..5288f5cc 100644 --- a/modules/nf-core/multiqc/main.nf +++ b/modules/nf-core/multiqc/main.nf @@ -3,8 +3,8 @@ process MULTIQC { conda "${moduleDir}/environment.yml" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/multiqc:1.29--pyhdfd78af_0' : - 'biocontainers/multiqc:1.29--pyhdfd78af_0' }" + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/ef/eff0eafe78d5f3b65a6639265a16b89fdca88d06d18894f90fcdb50142004329/data' : + 'community.wave.seqera.io/library/multiqc:1.31--1efbafd542a23882' }" input: path multiqc_files, stageAs: "?/*" diff --git a/modules/nf-core/multiqc/tests/main.nf.test.snap b/modules/nf-core/multiqc/tests/main.nf.test.snap index 88e90571..17881d15 100644 --- a/modules/nf-core/multiqc/tests/main.nf.test.snap +++ b/modules/nf-core/multiqc/tests/main.nf.test.snap @@ -2,14 +2,14 @@ "multiqc_versions_single": { "content": [ [ - "versions.yml:md5,c1fe644a37468f6dae548d98bc72c2c1" + "versions.yml:md5,8968b114a3e20756d8af2b80713bcc4f" ] ], "meta": { "nf-test": "0.9.2", - "nextflow": "25.04.2" + "nextflow": "25.04.6" }, - "timestamp": "2025-05-22T11:50:41.182332996" + "timestamp": "2025-09-08T20:57:36.139055243" }, "multiqc_stub": { "content": [ @@ -17,25 +17,25 @@ "multiqc_report.html", "multiqc_data", "multiqc_plots", - "versions.yml:md5,c1fe644a37468f6dae548d98bc72c2c1" + "versions.yml:md5,8968b114a3e20756d8af2b80713bcc4f" ] ], "meta": { "nf-test": "0.9.2", - "nextflow": "25.04.2" + "nextflow": "25.04.6" }, - "timestamp": "2025-05-22T11:51:22.448739369" + "timestamp": "2025-09-08T20:59:15.142230631" }, "multiqc_versions_config": { "content": [ [ - "versions.yml:md5,c1fe644a37468f6dae548d98bc72c2c1" + "versions.yml:md5,8968b114a3e20756d8af2b80713bcc4f" ] ], "meta": { "nf-test": "0.9.2", - "nextflow": "25.04.2" + "nextflow": "25.04.6" }, - "timestamp": "2025-05-22T11:51:06.198928424" + "timestamp": "2025-09-08T20:58:29.629087066" } } \ No newline at end of file diff --git a/modules/nf-core/multiqc/tests/tags.yml b/modules/nf-core/multiqc/tests/tags.yml deleted file mode 100644 index bea6c0d3..00000000 --- a/modules/nf-core/multiqc/tests/tags.yml +++ /dev/null @@ -1,2 +0,0 @@ -multiqc: - - modules/nf-core/multiqc/** diff --git a/nextflow.config b/nextflow.config index 3a105e71..e4e72bae 100644 --- a/nextflow.config +++ b/nextflow.config @@ -27,13 +27,15 @@ params { email_on_fail = null plaintext_email = false monochrome_logs = false - hook_url = null + hook_url = System.getenv('HOOK_URL') 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 @@ -86,7 +88,18 @@ profiles { apptainer.enabled = false docker.runOptions = '-u $(id -u):$(id -g)' } - arm { + arm64 { + process.arch = 'arm64' + // TODO https://github.com/nf-core/modules/issues/6694 + // 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 { @@ -143,18 +156,6 @@ 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' @@ -163,7 +164,6 @@ profiles { test { includeConfig 'conf/test.config' } test_full { includeConfig 'conf/test_full.config' } } - // Load nf-core custom profiles from different institutions // 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. @@ -244,46 +244,19 @@ manifest { description = """This pipeline is dedicated to finding the most stable genes across count datasets""" mainScript = 'main.nf' defaultBranch = 'main' - nextflowVersion = '!>=24.10.5' + nextflowVersion = '!>=25.04.0' version = '1.0dev' doi = '' } // Nextflow plugins plugins { - id 'nf-schema@2.4.2' // 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/stableexpression -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/stableexpression ${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/stableexpression/blob/main/CITATIONS.md -""" - } - summary { - beforeText = validation.help.beforeText - afterText = validation.help.afterText - } } // Load modules.config for DSL2 module specific options diff --git a/nextflow_schema.json b/nextflow_schema.json index ee89af0b..bc12467a 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -185,6 +185,18 @@ "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": { + "type": ["boolean", "string"], + "description": "Display the help message." + }, + "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/ro-crate-metadata.json b/ro-crate-metadata.json index a9deb47e..a7ebeb6a 100644 --- a/ro-crate-metadata.json +++ b/ro-crate-metadata.json @@ -22,8 +22,8 @@ "@id": "./", "@type": "Dataset", "creativeWorkStatus": "InProgress", - "datePublished": "2025-07-08T11:39:13+00:00", - "description": "

    \n \n \n \"nf-core/stableexpression\"\n \n

    \n\n[![GitHub Actions CI Status](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml/badge.svg)](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml)\n[![GitHub Actions Linting Status](https://github.com/nf-core/stableexpression/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/stableexpression/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/stableexpression/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%A524.10.5-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-3.3.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/3.3.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/stableexpression)\n\n[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23stableexpression-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/stableexpression)[![Follow on Bluesky](https://img.shields.io/badge/bluesky-%40nf__core-1185fe?labelColor=000000&logo=bluesky)](https://bsky.app/profile/nf-co.re)[![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/stableexpression** is a bioinformatics pipeline that ...\n\n\n\n\n2. 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/stableexpression \\\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/stableexpression/usage) and the [parameter documentation](https://nf-co.re/stableexpression/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/stableexpression/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/stableexpression/output).\n\n## Credits\n\nnf-core/stableexpression was originally written by Olivier Coen.\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 `#stableexpression` channel](https://nfcore.slack.com/channels/stableexpression) (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", + "datePublished": "2025-10-16T13:39:07+00:00", + "description": "

    \n \n \n \"nf-core/stableexpression\"\n \n

    \n\n[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new/nf-core/stableexpression)\n[![GitHub Actions CI Status](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml/badge.svg)](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml)\n[![GitHub Actions Linting Status](https://github.com/nf-core/stableexpression/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/stableexpression/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/stableexpression/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.04.0-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-3.4.1-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/3.4.1)\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/stableexpression)\n\n[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23stableexpression-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/stableexpression)[![Follow on Bluesky](https://img.shields.io/badge/bluesky-%40nf__core-1185fe?labelColor=000000&logo=bluesky)](https://bsky.app/profile/nf-co.re)[![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/stableexpression** is a bioinformatics pipeline that ...\n\n\n\n\n2. 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/stableexpression \\\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/stableexpression/usage) and the [parameter documentation](https://nf-co.re/stableexpression/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/stableexpression/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/stableexpression/output).\n\n## Credits\n\nnf-core/stableexpression was originally written by Olivier Coen.\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 `#stableexpression` channel](https://nfcore.slack.com/channels/stableexpression) (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", "hasPart": [ { "@id": "main.nf" @@ -99,7 +99,7 @@ }, "mentions": [ { - "@id": "#6a761be9-1032-4274-bb18-a7fd479235bb" + "@id": "#ee49bf85-7254-477b-a76d-88f5b94409cf" } ], "name": "nf-core/stableexpression" @@ -128,7 +128,7 @@ } ], "dateCreated": "", - "dateModified": "2025-07-08T11:39:13Z", + "dateModified": "2025-10-16T13:39:07Z", "dct:conformsTo": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE/", "keywords": ["nf-core", "nextflow", "expression", "housekeeping-genes", "qpcr-analysis"], "license": ["MIT"], @@ -157,14 +157,14 @@ "url": { "@id": "https://www.nextflow.io/" }, - "version": "!>=24.10.5" + "version": "!>=25.04.0" }, { - "@id": "#6a761be9-1032-4274-bb18-a7fd479235bb", + "@id": "#ee49bf85-7254-477b-a76d-88f5b94409cf", "@type": "TestSuite", "instance": [ { - "@id": "#50726c2b-7779-46d7-9d16-a3709a6c7d67" + "@id": "#60e3a4be-d59e-46f5-88ef-2dd82b8e7558" } ], "mainEntity": { @@ -173,7 +173,7 @@ "name": "Test suite for nf-core/stableexpression" }, { - "@id": "#50726c2b-7779-46d7-9d16-a3709a6c7d67", + "@id": "#60e3a4be-d59e-46f5-88ef-2dd82b8e7558", "@type": "TestInstance", "name": "GitHub Actions workflow for testing nf-core/stableexpression", "resource": "repos/nf-core/stableexpression/actions/workflows/nf-test.yml", diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index 330a7ad3..4d7c04e2 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -11,6 +11,7 @@ include { UTILS_NFSCHEMA_PLUGIN } from '../../nf-core/utils_nfschema_plugin' include { paramsSummaryMap } from 'plugin/nf-schema' include { samplesheetToList } from 'plugin/nf-schema' +include { paramsHelp } 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' @@ -32,6 +33,9 @@ workflow PIPELINE_INITIALISATION { nextflow_cli_args // array: List of positional nextflow CLI args outdir // string: The output directory where the results will be saved input // string: Path to input samplesheet + help // boolean: Display help message and exit + help_full // boolean: Show the full help message + show_hidden // boolean: Show hidden parameters in the help message main: @@ -50,10 +54,35 @@ workflow PIPELINE_INITIALISATION { // // Validate parameters and generate parameter summary to stdout // + 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/stableexpression ${workflow.manifest.version}\033[0m +-\033[2m----------------------------------------------------\033[0m- +""" + after_text = """${workflow.manifest.doi ? "\n* The pipeline\n" : ""}${workflow.manifest.doi.tokenize(",").collect { " https://doi.org/${it.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/stableexpression/blob/main/CITATIONS.md +""" + command = "nextflow run ${workflow.manifest.name} -profile --input samplesheet.csv --outdir " + UTILS_NFSCHEMA_PLUGIN ( workflow, validate_params, - null + null, + help, + help_full, + show_hidden, + before_text, + after_text, + command ) // 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 f8476112..00000000 --- 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/tests/tags.yml b/subworkflows/nf-core/utils_nfcore_pipeline/tests/tags.yml deleted file mode 100644 index ac8523c9..00000000 --- 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 4994303e..ee4738c8 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,29 +16,56 @@ 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 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 << [parametersSchema: parameters_schema] + } + log.info paramsHelp( + help_options, + params.help instanceof String ? params.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 << [parametersSchema: 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 << [parametersSchema: parameters_schema] } + validateParameters(validateOptions) } emit: 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 8fb30164..c977917a 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,12 @@ 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] = "" """ } } @@ -51,6 +57,12 @@ 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] = "" """ } } @@ -77,6 +89,12 @@ 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] = "" """ } } @@ -103,6 +121,12 @@ 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] = "" """ } } @@ -114,4 +138,36 @@ 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" + """ + } + } + + 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 09ef842a..8d8c7371 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.4.2" + id "nf-schema@2.5.1" } 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 index 16409f40..83f7a0a5 100644 --- a/tests/.nftignore +++ b/tests/.nftignore @@ -1,9 +1,10 @@ .DS_Store -multiqc/multiqc_data/BETA-multiqc.parquet +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 pipeline_info/*.{html,json,txt,yml} diff --git a/tests/default.nf.test b/tests/default.nf.test index 24113d3c..43916ae9 100644 --- a/tests/default.nf.test +++ b/tests/default.nf.test @@ -20,8 +20,6 @@ nextflow_pipeline { assertAll( { assert workflow.success}, { assert snapshot( - // Number of successful tasks - workflow.trace.succeeded().size(), // 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_stableexpression_software_mqc_versions.yml"), // All stable path name, with a relative path From 45a38052a1e067c562b59db285e32f98c62dda61 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sat, 17 May 2025 08:20:16 +0200 Subject: [PATCH 005/258] renamed processes and publish dirs --- ...q2_normalise.R => normalise_with_deseq2.R} | 0 ...ger_normalise.R => normalise_with_edger.R} | 0 conf/modules.config | 50 +++++++- modules/local/dataset_statistics/main.nf | 2 - modules/local/gene_statistics/main.nf | 4 +- .../gprofiler}/environment.yml | 2 +- .../idmapping => idmapping/gprofiler}/main.nf | 4 +- modules/local/merge_data/main.nf | 2 - .../deseq2}/environment.yml | 2 +- .../deseq2}/main.nf | 6 +- .../edger}/environment.yml | 2 +- .../normalise => normalisation/edger}/main.nf | 6 +- modules/local/quantile_normalisation/main.nf | 4 +- .../local/expression_normalisation/main.nf | 18 +-- .../expressionatlas/getdata/main.nf.test.snap | 113 ------------------ .../gprofiler}/main.nf.test | 6 +- .../gprofiler}/main.nf.test.snap | 62 +++++----- .../deseq2}/main.nf.test | 18 +-- .../deseq2}/main.nf.test.snap | 0 .../edger}/main.nf.test | 18 +-- .../edger}/main.nf.test.snap | 0 .../local/quantile_normalisation/main.nf.test | 6 +- .../quantile_normalisation/main.nf.test.snap | 14 +-- .../base/counts.csv | 0 .../base/design.csv | 0 .../many_zeros/counts.csv | 0 .../many_zeros/design.csv | 0 .../one_group/counts.csv | 0 .../one_group/design.csv | 0 .../count.raw.cpm.csv | 0 workflows/stableexpression.nf | 10 +- 31 files changed, 131 insertions(+), 218 deletions(-) rename bin/{deseq2_normalise.R => normalise_with_deseq2.R} (100%) rename bin/{edger_normalise.R => normalise_with_edger.R} (100%) rename modules/local/{gprofiler/idmapping => idmapping/gprofiler}/environment.yml (84%) rename modules/local/{gprofiler/idmapping => idmapping/gprofiler}/main.nf (97%) rename modules/local/{deseq2/normalise => normalisation/deseq2}/environment.yml (85%) rename modules/local/{deseq2/normalise => normalisation/deseq2}/main.nf (88%) rename modules/local/{edger/normalise => normalisation/edger}/environment.yml (86%) rename modules/local/{edger/normalise => normalisation/edger}/main.nf (89%) delete mode 100644 tests/modules/local/expressionatlas/getdata/main.nf.test.snap rename tests/modules/local/{gprofiler/idmapping => idmapping/gprofiler}/main.nf.test (97%) rename tests/modules/local/{gprofiler/idmapping => idmapping/gprofiler}/main.nf.test.snap (82%) rename tests/modules/local/{deseq2/normalise => normalisation/deseq2}/main.nf.test (76%) rename tests/modules/local/{deseq2/normalise => normalisation/deseq2}/main.nf.test.snap (100%) rename tests/modules/local/{edger/normalise => normalisation/edger}/main.nf.test (76%) rename tests/modules/local/{edger/normalise => normalisation/edger}/main.nf.test.snap (100%) rename tests/test_data/{normalise => normalisation}/base/counts.csv (100%) rename tests/test_data/{normalise => normalisation}/base/design.csv (100%) rename tests/test_data/{normalise => normalisation}/many_zeros/counts.csv (100%) rename tests/test_data/{normalise => normalisation}/many_zeros/design.csv (100%) rename tests/test_data/{normalise => normalisation}/one_group/counts.csv (100%) rename tests/test_data/{normalise => normalisation}/one_group/design.csv (100%) rename tests/test_data/{quantile_normalise => quantile_normalisation}/count.raw.cpm.csv (100%) diff --git a/bin/deseq2_normalise.R b/bin/normalise_with_deseq2.R similarity index 100% rename from bin/deseq2_normalise.R rename to bin/normalise_with_deseq2.R diff --git a/bin/edger_normalise.R b/bin/normalise_with_edger.R similarity index 100% rename from bin/edger_normalise.R rename to bin/normalise_with_edger.R diff --git a/conf/modules.config b/conf/modules.config index f0b0d55a..58429f87 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -13,17 +13,57 @@ process { publishDir = [ - path: { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" }, - mode: params.publish_dir_mode, - saveAs: { filename -> filename.equals('versions.yml') ? null : filename } + path: { "${params.outdir}/${task.process.tokenize(':')[-1].toLowerCase()}" }, + mode: params.publish_dir_mode ] + withName: FLYE { + ext.args = { + [ + meta.genome_size ? "--genome-size ${meta.genome_size}" : '', + params.flye_args + ].join(" ").trim() + } + publishDir = [ + path: { "${params.outdir}/${meta.id}/assembly/flye/" }, + mode: params.publish_dir_mode, + saveAs: { filename -> filename.equals('versions.yml') ? null : filename } + ] + } + + withName: 'NORMALISATION_DESEQ2' { + publishDir = [ + path: { "${params.outdir}/${meta.dataset}/normalised/deseq2/" }, + mode: params.publish_dir_mode + ] + } + + withName: 'NORMALISATION_EDGER' { + publishDir = [ + path: { "${params.outdir}/${meta.dataset}/normalised/edger/" }, + mode: params.publish_dir_mode + ] + } + + withName: 'QUANTILE_NORMALISATION' { + publishDir = [ + path: { "${params.outdir}/${meta.dataset}/quantile_normalised/" }, + mode: params.publish_dir_mode + ] + } + + withName: 'MERGE_DATA' { + publishDir = [ + path: { "${params.outdir}/merged_datasets/" }, + mode: params.publish_dir_mode + ] + } + withName: 'MULTIQC' { ext.args = { params.multiqc_title ? "--title \"$params.multiqc_title\"" : '' } publishDir = [ path: { "${params.outdir}/multiqc" }, - mode: params.publish_dir_mode, - saveAs: { filename -> filename.equals('versions.yml') ? null : filename } + mode: params.publish_dir_mode ] } diff --git a/modules/local/dataset_statistics/main.nf b/modules/local/dataset_statistics/main.nf index 4595a4d8..fad91d5a 100644 --- a/modules/local/dataset_statistics/main.nf +++ b/modules/local/dataset_statistics/main.nf @@ -2,8 +2,6 @@ process DATASET_STATISTICS { label 'process_low' - publishDir "${params.outdir}/dataset_statistics" - tag "${meta.dataset}" conda "${moduleDir}/environment.yml" diff --git a/modules/local/gene_statistics/main.nf b/modules/local/gene_statistics/main.nf index 3e1262b6..1953c855 100644 --- a/modules/local/gene_statistics/main.nf +++ b/modules/local/gene_statistics/main.nf @@ -1,5 +1,5 @@ process GENE_STATISTICS { - debug true + label 'process_low' errorStrategy = { @@ -12,8 +12,6 @@ process GENE_STATISTICS { } } - publishDir "${params.outdir}/gene_statistics" - conda "${moduleDir}/environment.yml" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': diff --git a/modules/local/gprofiler/idmapping/environment.yml b/modules/local/idmapping/gprofiler/environment.yml similarity index 84% rename from modules/local/gprofiler/idmapping/environment.yml rename to modules/local/idmapping/gprofiler/environment.yml index 8329e64b..aa931b5e 100644 --- a/modules/local/gprofiler/idmapping/environment.yml +++ b/modules/local/idmapping/gprofiler/environment.yml @@ -1,4 +1,4 @@ -name: gprofiler_idmapping +name: idmapping_gprofiler channels: - conda-forge dependencies: diff --git a/modules/local/gprofiler/idmapping/main.nf b/modules/local/idmapping/gprofiler/main.nf similarity index 97% rename from modules/local/gprofiler/idmapping/main.nf rename to modules/local/idmapping/gprofiler/main.nf index f859bb68..d010c426 100644 --- a/modules/local/gprofiler/idmapping/main.nf +++ b/modules/local/idmapping/gprofiler/main.nf @@ -1,9 +1,7 @@ -process GPROFILER_IDMAPPING { +process IDMAPPING_GPROFILER { label 'process_low' - publishDir "${params.outdir}/idmapping" - tag "${meta.dataset}" // limiting to 8 threads at a time to avoid 429 errors with the G Profiler API server diff --git a/modules/local/merge_data/main.nf b/modules/local/merge_data/main.nf index 6507e5c2..db9f01f0 100644 --- a/modules/local/merge_data/main.nf +++ b/modules/local/merge_data/main.nf @@ -2,8 +2,6 @@ process MERGE_DATA { label 'process_low' - publishDir "${params.outdir}/merged_data" - conda "${moduleDir}/environment.yml" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': diff --git a/modules/local/deseq2/normalise/environment.yml b/modules/local/normalisation/deseq2/environment.yml similarity index 85% rename from modules/local/deseq2/normalise/environment.yml rename to modules/local/normalisation/deseq2/environment.yml index 2ddc8904..512715e1 100644 --- a/modules/local/deseq2/normalise/environment.yml +++ b/modules/local/normalisation/deseq2/environment.yml @@ -1,4 +1,4 @@ -name: deseq_normalise +name: normalisation_deseq2 channels: - conda-forge - bioconda diff --git a/modules/local/deseq2/normalise/main.nf b/modules/local/normalisation/deseq2/main.nf similarity index 88% rename from modules/local/deseq2/normalise/main.nf rename to modules/local/normalisation/deseq2/main.nf index f8e9921e..71a7786c 100644 --- a/modules/local/deseq2/normalise/main.nf +++ b/modules/local/normalisation/deseq2/main.nf @@ -1,9 +1,7 @@ -process DESEQ2_NORMALISE { +process NORMALISATION_DESEQ2 { label 'process_low' - publishDir "${params.outdir}/normalisation/deseq2" - tag "${meta.dataset}" // ignoring cases when the count dataframe gets empty after filtering (the script throws a 100 in this case) @@ -27,7 +25,7 @@ process DESEQ2_NORMALISE { script: def design_file = meta.design """ - deseq2_normalise.R --counts "$count_file" --design "$design_file" + normalise_with_deseq2.R --counts "$count_file" --design "$design_file" """ diff --git a/modules/local/edger/normalise/environment.yml b/modules/local/normalisation/edger/environment.yml similarity index 86% rename from modules/local/edger/normalise/environment.yml rename to modules/local/normalisation/edger/environment.yml index d460b7cd..cd4944ab 100644 --- a/modules/local/edger/normalise/environment.yml +++ b/modules/local/normalisation/edger/environment.yml @@ -1,4 +1,4 @@ -name: edger_normalise +name: normalisation_edger channels: - conda-forge - bioconda diff --git a/modules/local/edger/normalise/main.nf b/modules/local/normalisation/edger/main.nf similarity index 89% rename from modules/local/edger/normalise/main.nf rename to modules/local/normalisation/edger/main.nf index d25347ab..68092bc2 100644 --- a/modules/local/edger/normalise/main.nf +++ b/modules/local/normalisation/edger/main.nf @@ -1,9 +1,7 @@ -process EDGER_NORMALISE { +process NORMALISATION_EDGER { label 'process_low' - publishDir "${params.outdir}/normalisation/edger" - tag "${meta.dataset}" // ignoring cases when the count dataframe gets empty after filtering (the script throws a 100 in this case) @@ -27,7 +25,7 @@ process EDGER_NORMALISE { script: def design_file = meta.design """ - edger_normalise.R --counts "$count_file" --design "$design_file" + normalise_with_edger.R --counts "$count_file" --design "$design_file" """ } diff --git a/modules/local/quantile_normalisation/main.nf b/modules/local/quantile_normalisation/main.nf index b2891d38..59b91ca0 100644 --- a/modules/local/quantile_normalisation/main.nf +++ b/modules/local/quantile_normalisation/main.nf @@ -1,9 +1,7 @@ -process QUANTILE_NORMALISE { +process QUANTILE_NORMALISATION { label 'process_low' - publishDir "${params.outdir}/quantile_normalisation" - tag "${meta.dataset}" conda "${moduleDir}/environment.yml" diff --git a/subworkflows/local/expression_normalisation/main.nf b/subworkflows/local/expression_normalisation/main.nf index 34249204..b5e4bfe9 100644 --- a/subworkflows/local/expression_normalisation/main.nf +++ b/subworkflows/local/expression_normalisation/main.nf @@ -8,9 +8,9 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -include { DESEQ2_NORMALISE } from '../../../modules/local/deseq2/normalise/main' -include { EDGER_NORMALISE } from '../../../modules/local/edger/normalise/main' -include { QUANTILE_NORMALISE } from '../../../modules/local/quantile_normalisation/main' +include { NORMALISATION_DESEQ2 } from '../../../modules/local/normalisation/deseq2/main' +include { NORMALISATION_EDGER } from '../../../modules/local/normalisation/edger/main' +include { QUANTILE_NORMALISATION } from '../../../modules/local/quantile_normalisation/main' include { DATASET_STATISTICS } from '../../../modules/local/dataset_statistics/main' /* @@ -42,12 +42,12 @@ workflow EXPRESSION_NORMALISATION { ch_raw_rnaseq_datasets = ch_datasets.raw.filter { meta, file -> meta.platform == 'rnaseq' } if ( normalisation_method == 'deseq2' ) { - DESEQ2_NORMALISE( ch_raw_rnaseq_datasets ) - ch_raw_rnaseq_datasets_normalised = DESEQ2_NORMALISE.out.cpm + NORMALISATION_DESEQ2( ch_raw_rnaseq_datasets ) + ch_raw_rnaseq_datasets_normalised = NORMALISATION_DESEQ2.out.cpm } else { // 'edger' - EDGER_NORMALISE( ch_raw_rnaseq_datasets ) - ch_raw_rnaseq_datasets_normalised = EDGER_NORMALISE.out.cpm + NORMALISATION_EDGER( ch_raw_rnaseq_datasets ) + ch_raw_rnaseq_datasets_normalised = NORMALISATION_EDGER.out.cpm } // @@ -55,8 +55,8 @@ workflow EXPRESSION_NORMALISATION { // // putting all normalised count datasets together and performing quantile normalisation - ch_datasets.normalised.concat( ch_raw_rnaseq_datasets_normalised ) | QUANTILE_NORMALISE - ch_quantile_normalised_datasets = QUANTILE_NORMALISE.out.counts + ch_datasets.normalised.concat( ch_raw_rnaseq_datasets_normalised ) | QUANTILE_NORMALISATION + ch_quantile_normalised_datasets = QUANTILE_NORMALISATION.out.counts // // MODULE: Dataset statistics diff --git a/tests/modules/local/expressionatlas/getdata/main.nf.test.snap b/tests/modules/local/expressionatlas/getdata/main.nf.test.snap deleted file mode 100644 index b3a5f521..00000000 --- a/tests/modules/local/expressionatlas/getdata/main.nf.test.snap +++ /dev/null @@ -1,113 +0,0 @@ -{ - "Transcriptome Analysis of the potato (rnaseq)": { - "content": [ - { - "0": [ - "E_MTAB_552_rnaseq.design.csv:md5,b81490696f638e90c1cf14236bb0c08c" - ], - "1": [ - "E_MTAB_552_rnaseq.rnaseq.raw.counts.csv:md5,830f50b60b17b62f9ca2f6a163a2879f" - ], - "2": [ - [ - "EXPRESSIONATLAS_GETDATA", - "R", - "4.3.3 (2024-02-29)" - ] - ], - "3": [ - [ - "EXPRESSIONATLAS_GETDATA", - "ExpressionAtlas", - "1.30.0" - ] - ], - "counts": [ - "E_MTAB_552_rnaseq.rnaseq.raw.counts.csv:md5,830f50b60b17b62f9ca2f6a163a2879f" - ], - "design": [ - "E_MTAB_552_rnaseq.design.csv:md5,b81490696f638e90c1cf14236bb0c08c" - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.0" - }, - "timestamp": "2025-05-12T12:17:16.305872196" - }, - "Arabidopsis Geo dataset": { - "content": [ - { - "0": [ - "E_GEOD_62537_A_AFFY_2.design.csv:md5,7d7dd72be4f5b326dd25a36db01ebf88" - ], - "1": [ - "E_GEOD_62537_A_AFFY_2.microarray.normalised.counts.csv:md5,673c55171d0ccfc1d036bf43c49ae320" - ], - "2": [ - [ - "EXPRESSIONATLAS_GETDATA", - "R", - "4.3.3 (2024-02-29)" - ] - ], - "3": [ - [ - "EXPRESSIONATLAS_GETDATA", - "ExpressionAtlas", - "1.30.0" - ] - ], - "counts": [ - "E_GEOD_62537_A_AFFY_2.microarray.normalised.counts.csv:md5,673c55171d0ccfc1d036bf43c49ae320" - ], - "design": [ - "E_GEOD_62537_A_AFFY_2.design.csv:md5,7d7dd72be4f5b326dd25a36db01ebf88" - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.0" - }, - "timestamp": "2025-05-12T12:17:49.460792842" - }, - "Transcription profiling by array of Arabidopsis mutant for fis2 (microarray)": { - "content": [ - { - "0": [ - "E_TABM_1007_A_AFFY_2.design.csv:md5,120f63cae2193b97d7451483bdbbaab1" - ], - "1": [ - "E_TABM_1007_A_AFFY_2.microarray.normalised.counts.csv:md5,a3afe33d7eaed3339da9109bf25bb3ed" - ], - "2": [ - [ - "EXPRESSIONATLAS_GETDATA", - "R", - "4.3.3 (2024-02-29)" - ] - ], - "3": [ - [ - "EXPRESSIONATLAS_GETDATA", - "ExpressionAtlas", - "1.30.0" - ] - ], - "counts": [ - "E_TABM_1007_A_AFFY_2.microarray.normalised.counts.csv:md5,a3afe33d7eaed3339da9109bf25bb3ed" - ], - "design": [ - "E_TABM_1007_A_AFFY_2.design.csv:md5,120f63cae2193b97d7451483bdbbaab1" - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.0" - }, - "timestamp": "2025-05-12T12:17:33.926612754" - } -} \ No newline at end of file diff --git a/tests/modules/local/gprofiler/idmapping/main.nf.test b/tests/modules/local/idmapping/gprofiler/main.nf.test similarity index 97% rename from tests/modules/local/gprofiler/idmapping/main.nf.test rename to tests/modules/local/idmapping/gprofiler/main.nf.test index a19f98a6..10b9cdc4 100644 --- a/tests/modules/local/gprofiler/idmapping/main.nf.test +++ b/tests/modules/local/idmapping/gprofiler/main.nf.test @@ -1,8 +1,8 @@ nextflow_process { - name "Test Process GPROFILER_IDMAPPING" - script "modules/local/gprofiler/idmapping/main.nf" - process "GPROFILER_IDMAPPING" + name "Test Process IDMAPPING_GPROFILER" + script "modules/local/idmapping/gprofiler/main.nf" + process "IDMAPPING_GPROFILER" tag "idmapping" tag "module" diff --git a/tests/modules/local/gprofiler/idmapping/main.nf.test.snap b/tests/modules/local/idmapping/gprofiler/main.nf.test.snap similarity index 82% rename from tests/modules/local/gprofiler/idmapping/main.nf.test.snap rename to tests/modules/local/idmapping/gprofiler/main.nf.test.snap index 649c7294..78eb984e 100644 --- a/tests/modules/local/gprofiler/idmapping/main.nf.test.snap +++ b/tests/modules/local/idmapping/gprofiler/main.nf.test.snap @@ -5,45 +5,45 @@ "0": [ [ [ - + ], "counts.ensembl_ids.renamed.csv:md5,b2f45fd17fcb2688ea08b94527f70c1f" ] ], "1": [ - + ], "2": [ "counts.ensembl_ids.mapping.csv:md5,6ff8d8f71b9df7a1b08ff0bfda8da755" ], "3": [ [ - "GPROFILER_IDMAPPING", + "IDMAPPING_GPROFILER", "python", "3.12.8" ] ], "4": [ [ - "GPROFILER_IDMAPPING", + "IDMAPPING_GPROFILER", "pandas", "2.2.3" ] ], "5": [ [ - "GPROFILER_IDMAPPING", + "IDMAPPING_GPROFILER", "requests", "2.32.3" ] ], "metadata": [ - + ], "renamed": [ [ [ - + ], "counts.ensembl_ids.renamed.csv:md5,b2f45fd17fcb2688ea08b94527f70c1f" ] @@ -52,9 +52,9 @@ ], "meta": { "nf-test": "0.9.2", - "nextflow": "24.10.3" + "nextflow": "25.04.0" }, - "timestamp": "2025-02-05T12:00:14.408028923" + "timestamp": "2025-05-14T11:36:14.264437894" }, "Map Ensembl IDs to themselves": { "content": [ @@ -62,7 +62,7 @@ "0": [ [ [ - + ], "counts.ensembl_ids.renamed.csv:md5,ef96059e3283d4305b2c004d649ae648" ] @@ -75,21 +75,21 @@ ], "3": [ [ - "GPROFILER_IDMAPPING", + "IDMAPPING_GPROFILER", "python", "3.12.8" ] ], "4": [ [ - "GPROFILER_IDMAPPING", + "IDMAPPING_GPROFILER", "pandas", "2.2.3" ] ], "5": [ [ - "GPROFILER_IDMAPPING", + "IDMAPPING_GPROFILER", "requests", "2.32.3" ] @@ -100,7 +100,7 @@ "renamed": [ [ [ - + ], "counts.ensembl_ids.renamed.csv:md5,ef96059e3283d4305b2c004d649ae648" ] @@ -109,9 +109,9 @@ ], "meta": { "nf-test": "0.9.2", - "nextflow": "24.10.3" + "nextflow": "25.04.0" }, - "timestamp": "2025-02-05T11:59:32.564006233" + "timestamp": "2025-05-14T11:35:32.690454163" }, "Map Uniprot IDs": { "content": [ @@ -119,7 +119,7 @@ "0": [ [ [ - + ], "counts.uniprot_ids.renamed.csv:md5,a93d7145f8b35f6074cdf21c7c97de7c" ] @@ -132,21 +132,21 @@ ], "3": [ [ - "GPROFILER_IDMAPPING", + "IDMAPPING_GPROFILER", "python", "3.12.8" ] ], "4": [ [ - "GPROFILER_IDMAPPING", + "IDMAPPING_GPROFILER", "pandas", "2.2.3" ] ], "5": [ [ - "GPROFILER_IDMAPPING", + "IDMAPPING_GPROFILER", "requests", "2.32.3" ] @@ -157,7 +157,7 @@ "renamed": [ [ [ - + ], "counts.uniprot_ids.renamed.csv:md5,a93d7145f8b35f6074cdf21c7c97de7c" ] @@ -166,9 +166,9 @@ ], "meta": { "nf-test": "0.9.2", - "nextflow": "24.10.3" + "nextflow": "25.04.0" }, - "timestamp": "2025-02-05T11:59:50.805226174" + "timestamp": "2025-05-14T11:35:50.772485621" }, "Map NCBI IDs": { "content": [ @@ -176,7 +176,7 @@ "0": [ [ [ - + ], "counts.ncbi_ids.renamed.csv:md5,a93d7145f8b35f6074cdf21c7c97de7c" ] @@ -189,21 +189,21 @@ ], "3": [ [ - "GPROFILER_IDMAPPING", + "IDMAPPING_GPROFILER", "python", "3.12.8" ] ], "4": [ [ - "GPROFILER_IDMAPPING", + "IDMAPPING_GPROFILER", "pandas", "2.2.3" ] ], "5": [ [ - "GPROFILER_IDMAPPING", + "IDMAPPING_GPROFILER", "requests", "2.32.3" ] @@ -214,7 +214,7 @@ "renamed": [ [ [ - + ], "counts.ncbi_ids.renamed.csv:md5,a93d7145f8b35f6074cdf21c7c97de7c" ] @@ -223,8 +223,8 @@ ], "meta": { "nf-test": "0.9.2", - "nextflow": "24.10.3" + "nextflow": "25.04.0" }, - "timestamp": "2025-02-05T11:59:41.467446589" + "timestamp": "2025-05-14T11:35:41.055648628" } -} \ No newline at end of file +} diff --git a/tests/modules/local/deseq2/normalise/main.nf.test b/tests/modules/local/normalisation/deseq2/main.nf.test similarity index 76% rename from tests/modules/local/deseq2/normalise/main.nf.test rename to tests/modules/local/normalisation/deseq2/main.nf.test index 76418c22..f03421bb 100644 --- a/tests/modules/local/deseq2/normalise/main.nf.test +++ b/tests/modules/local/normalisation/deseq2/main.nf.test @@ -1,8 +1,8 @@ nextflow_process { - name "Test Process DESEQ2_NORMALISE" - script "modules/local/deseq2/normalise/main.nf" - process "DESEQ2_NORMALISE" + name "Test Process NORMALISATION_DESEQ2" + script "modules/local/normalisation/deseq2/main.nf" + process "NORMALISATION_DESEQ2" tag "deseq2" tag "normalise" tag "module" @@ -13,8 +13,8 @@ nextflow_process { process { """ - meta = [accession: "accession", design: file('$projectDir/tests/test_data/normalise/base/design.csv')] - input[0] = [meta, file('$projectDir/tests/test_data/normalise/base/counts.csv')] + meta = [accession: "accession", design: file('$projectDir/tests/test_data/normalisation/base/design.csv')] + input[0] = [meta, file('$projectDir/tests/test_data/normalisation/base/counts.csv')] """ } } @@ -36,8 +36,8 @@ nextflow_process { process { """ - meta = [accession: "accession", design: file('$projectDir/tests/test_data/normalise/many_zeros/design.csv')] - input[0] = [meta, file('$projectDir/tests/test_data/normalise/many_zeros/counts.csv')] + meta = [accession: "accession", design: file('$projectDir/tests/test_data/normalisation/many_zeros/design.csv')] + input[0] = [meta, file('$projectDir/tests/test_data/normalisation/many_zeros/counts.csv')] """ } } @@ -57,8 +57,8 @@ nextflow_process { process { """ - meta = [accession: "accession", design: file('$projectDir/tests/test_data/normalise/one_group/design.csv')] - input[0] = [meta, file('$projectDir/tests/test_data/normalise/one_group/counts.csv')] + meta = [accession: "accession", design: file('$projectDir/tests/test_data/normalisation/one_group/design.csv')] + input[0] = [meta, file('$projectDir/tests/test_data/normalisation/one_group/counts.csv')] """ } } diff --git a/tests/modules/local/deseq2/normalise/main.nf.test.snap b/tests/modules/local/normalisation/deseq2/main.nf.test.snap similarity index 100% rename from tests/modules/local/deseq2/normalise/main.nf.test.snap rename to tests/modules/local/normalisation/deseq2/main.nf.test.snap diff --git a/tests/modules/local/edger/normalise/main.nf.test b/tests/modules/local/normalisation/edger/main.nf.test similarity index 76% rename from tests/modules/local/edger/normalise/main.nf.test rename to tests/modules/local/normalisation/edger/main.nf.test index 31dc23c7..6df60a75 100644 --- a/tests/modules/local/edger/normalise/main.nf.test +++ b/tests/modules/local/normalisation/edger/main.nf.test @@ -1,8 +1,8 @@ nextflow_process { - name "Test Process EDGER_NORMALISE" - script "modules/local/edger/normalise/main.nf" - process "EDGER_NORMALISE" + name "Test Process NORMALISATION_EDGER" + script "modules/local/normalisation/edger/main.nf" + process "NORMALISATION_EDGER" tag "edger" tag "normalisation" tag "module" @@ -13,8 +13,8 @@ nextflow_process { process { """ - meta = [accession: "accession", design: file('$projectDir/tests/test_data/normalise/base/design.csv')] - input[0] = [meta, file('$projectDir/tests/test_data/normalise/base/counts.csv')] + meta = [accession: "accession", design: file('$projectDir/tests/test_data/normalisation/base/design.csv')] + input[0] = [meta, file('$projectDir/tests/test_data/normalisation/base/counts.csv')] """ } } @@ -36,8 +36,8 @@ nextflow_process { process { """ - meta = [accession: "accession", design: file('$projectDir/tests/test_data/normalise/many_zeros/design.csv')] - input[0] = [meta, file('$projectDir/tests/test_data/normalise/many_zeros/counts.csv')] + meta = [accession: "accession", design: file('$projectDir/tests/test_data/normalisation/many_zeros/design.csv')] + input[0] = [meta, file('$projectDir/tests/test_data/normalisation/many_zeros/counts.csv')] """ } } @@ -57,8 +57,8 @@ nextflow_process { process { """ - meta = [accession: "accession", design: file('$projectDir/tests/test_data/normalise/one_group/design.csv')] - input[0] = [meta, file('$projectDir/tests/test_data/normalise/one_group/counts.csv')] + meta = [accession: "accession", design: file('$projectDir/tests/test_data/normalisation/one_group/design.csv')] + input[0] = [meta, file('$projectDir/tests/test_data/normalisation/one_group/counts.csv')] """ } } diff --git a/tests/modules/local/edger/normalise/main.nf.test.snap b/tests/modules/local/normalisation/edger/main.nf.test.snap similarity index 100% rename from tests/modules/local/edger/normalise/main.nf.test.snap rename to tests/modules/local/normalisation/edger/main.nf.test.snap diff --git a/tests/modules/local/quantile_normalisation/main.nf.test b/tests/modules/local/quantile_normalisation/main.nf.test index bba28046..ee9423ce 100644 --- a/tests/modules/local/quantile_normalisation/main.nf.test +++ b/tests/modules/local/quantile_normalisation/main.nf.test @@ -1,8 +1,8 @@ nextflow_process { - name "Test Process QUANTILE_NORMALISE" + name "Test Process QUANTILE_NORMALISATION" script "modules/local/quantile_normalisation/main.nf" - process "QUANTILE_NORMALISE" + process "QUANTILE_NORMALISATION" tag "quant_norm" tag "module" @@ -12,7 +12,7 @@ nextflow_process { process { """ meta = [dataset: 'test'] - count_file = file( '$projectDir/tests/test_data/quantile_normalise/count.raw.cpm.csv', checkIfExists: true) + count_file = file( '$projectDir/tests/test_data/quantile_normalisation/count.raw.cpm.csv', checkIfExists: true) input[0] = [meta, count_file] """ } diff --git a/tests/modules/local/quantile_normalisation/main.nf.test.snap b/tests/modules/local/quantile_normalisation/main.nf.test.snap index 5fff9838..8a4e0826 100644 --- a/tests/modules/local/quantile_normalisation/main.nf.test.snap +++ b/tests/modules/local/quantile_normalisation/main.nf.test.snap @@ -12,28 +12,28 @@ ], "1": [ [ - "QUANTILE_NORMALISE", + "QUANTILE_NORMALISATION", "python", "3.12.8" ] ], "2": [ [ - "QUANTILE_NORMALISE", + "QUANTILE_NORMALISATION", "pandas", "2.2.3" ] ], "3": [ [ - "QUANTILE_NORMALISE", + "QUANTILE_NORMALISATION", "scikit-learn", "1.6.1" ] ], "4": [ [ - "QUANTILE_NORMALISE", + "QUANTILE_NORMALISATION", "pyarrow", "19.0.0" ] @@ -50,8 +50,8 @@ ], "meta": { "nf-test": "0.9.2", - "nextflow": "24.10.6" + "nextflow": "25.04.0" }, - "timestamp": "2025-05-08T14:38:27.184977502" + "timestamp": "2025-05-14T12:01:16.0219458" } -} \ No newline at end of file +} diff --git a/tests/test_data/normalise/base/counts.csv b/tests/test_data/normalisation/base/counts.csv similarity index 100% rename from tests/test_data/normalise/base/counts.csv rename to tests/test_data/normalisation/base/counts.csv diff --git a/tests/test_data/normalise/base/design.csv b/tests/test_data/normalisation/base/design.csv similarity index 100% rename from tests/test_data/normalise/base/design.csv rename to tests/test_data/normalisation/base/design.csv diff --git a/tests/test_data/normalise/many_zeros/counts.csv b/tests/test_data/normalisation/many_zeros/counts.csv similarity index 100% rename from tests/test_data/normalise/many_zeros/counts.csv rename to tests/test_data/normalisation/many_zeros/counts.csv diff --git a/tests/test_data/normalise/many_zeros/design.csv b/tests/test_data/normalisation/many_zeros/design.csv similarity index 100% rename from tests/test_data/normalise/many_zeros/design.csv rename to tests/test_data/normalisation/many_zeros/design.csv diff --git a/tests/test_data/normalise/one_group/counts.csv b/tests/test_data/normalisation/one_group/counts.csv similarity index 100% rename from tests/test_data/normalise/one_group/counts.csv rename to tests/test_data/normalisation/one_group/counts.csv diff --git a/tests/test_data/normalise/one_group/design.csv b/tests/test_data/normalisation/one_group/design.csv similarity index 100% rename from tests/test_data/normalise/one_group/design.csv rename to tests/test_data/normalisation/one_group/design.csv diff --git a/tests/test_data/quantile_normalise/count.raw.cpm.csv b/tests/test_data/quantile_normalisation/count.raw.cpm.csv similarity index 100% rename from tests/test_data/quantile_normalise/count.raw.cpm.csv rename to tests/test_data/quantile_normalisation/count.raw.cpm.csv diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 9e2162c7..68370310 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -7,7 +7,7 @@ include { EXPRESSIONATLAS_FETCHDATA } from '../subworkflows/local/expressionatlas_fetchdata/main' include { EXPRESSION_NORMALISATION } from '../subworkflows/local/expression_normalisation/main.nf' -include { GPROFILER_IDMAPPING } from '../modules/local/gprofiler/idmapping/main' +include { IDMAPPING_GPROFILER } from '../modules/local/idmapping/gprofiler/main' include { MERGE_DATA } from '../modules/local/merge_data/main' include { GENE_STATISTICS } from '../modules/local/gene_statistics/main' include { MULTIQC } from '../modules/nf-core/multiqc/main' @@ -68,15 +68,15 @@ workflow STABLEEXPRESSION { } else { // tries to map gene IDs to Ensembl IDs whenever possible - GPROFILER_IDMAPPING( + IDMAPPING_GPROFILER( ch_datasets.combine( ch_species ), params.gene_id_mapping ? Channel.fromPath( params.gene_id_mapping, checkIfExists: true ) : 'none' ) - ch_datasets = GPROFILER_IDMAPPING.out.renamed - ch_gene_metadata = ch_gene_metadata.mix( GPROFILER_IDMAPPING.out.metadata ) + ch_datasets = IDMAPPING_GPROFILER.out.renamed + ch_gene_metadata = ch_gene_metadata.mix( IDMAPPING_GPROFILER.out.metadata ) // the gene id mappings are the sum // of those provided by the user and those fetched from g:Profiler - ch_gene_id_mapping = GPROFILER_IDMAPPING.out.mapping + ch_gene_id_mapping = IDMAPPING_GPROFILER.out.mapping } // From 7459f2a31d7a112fb413c943a6149de5468728d4 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sat, 17 May 2025 09:13:29 +0200 Subject: [PATCH 006/258] add prefix to dataset_statistics --- bin/get_dataset_statistics.py | 12 +++++++----- modules/local/dataset_statistics/main.nf | 3 ++- modules/local/expressionatlas/getaccessions/main.nf | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/bin/get_dataset_statistics.py b/bin/get_dataset_statistics.py index 9c772442..907b0c26 100755 --- a/bin/get_dataset_statistics.py +++ b/bin/get_dataset_statistics.py @@ -33,6 +33,9 @@ def parse_args(): parser.add_argument( "--counts", type=Path, dest="count_file", required=True, help="Count file" ) + parser.add_argument( + "--output", type=str, dest="outfile_name", required=True, help="Output file" + ) return parser.parse_args() @@ -54,12 +57,11 @@ def compute_dataset_statistics(count_df: pd.DataFrame): return dataset_stats_df.T -def export_count_data(dataset_stats_df: pd.DataFrame, count_file: Path): +def export_count_data(dataset_stats_df: pd.DataFrame, outfile_name: str): """Export dataset statistics to CSV files.""" - outfilename = count_file.name.replace(QUANT_NORM_SUFFIX, DATASET_STATISTICS_SUFFIX) - logger.info(f"Exporting dataset statistics counts to: {outfilename}") + logger.info(f"Exporting dataset statistics counts to: {outfile_name}") dataset_stats_df.index.name = SAMPLE_COLNAME - dataset_stats_df.to_csv(outfilename, index=True, header=True) + dataset_stats_df.to_csv(outfile_name, index=True, header=True) ##################################################### @@ -79,7 +81,7 @@ def main(): dataset_stats_df = compute_dataset_statistics(count_df) - export_count_data(dataset_stats_df, count_file) + export_count_data(dataset_stats_df, args.outfile_name) if __name__ == "__main__": diff --git a/modules/local/dataset_statistics/main.nf b/modules/local/dataset_statistics/main.nf index fad91d5a..77fbccc9 100644 --- a/modules/local/dataset_statistics/main.nf +++ b/modules/local/dataset_statistics/main.nf @@ -21,8 +21,9 @@ process DATASET_STATISTICS { script: + def prefix = task.ext.prefix ?: "${meta.dataset}" """ - get_dataset_statistics.py --counts $count_file + get_dataset_statistics.py --counts $count_file --output ${prefix}.dataset_stats.csv """ diff --git a/modules/local/expressionatlas/getaccessions/main.nf b/modules/local/expressionatlas/getaccessions/main.nf index 100602fa..8c5dcf88 100644 --- a/modules/local/expressionatlas/getaccessions/main.nf +++ b/modules/local/expressionatlas/getaccessions/main.nf @@ -35,7 +35,7 @@ process EXPRESSIONATLAS_GETACCESSIONS { stub: """ - touch accessions.csv + touch accessions.txt """ } From 3201ab313c1870fbdc29f90b4ed16cb94c898273 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sat, 17 May 2025 09:13:44 +0200 Subject: [PATCH 007/258] rename custom_datasets folder --- conf/test_dataset.config | 4 ++-- conf/test_dataset_custom_mapping.config | 8 ++++---- conf/test_local_and_downloaded.config | 4 ++-- conf/test_one_accession.config | 2 +- conf/test_one_accession_low_gene_count.config | 2 +- .../main.nf | 1 + .../expression_normalisation/main.nf.test | 20 +++++++++---------- .../main.nf.test.snap | 10 ++++++---- tests/test_data/custom_datasets/input.csv | 3 --- tests/test_data/input_datasets/input.csv | 3 +++ .../microarray.normalised.csv | 0 .../microarray.normalised.design.csv | 0 .../rnaseq.raw.csv | 0 .../rnaseq.raw.design.csv | 0 14 files changed, 30 insertions(+), 27 deletions(-) delete mode 100644 tests/test_data/custom_datasets/input.csv create mode 100644 tests/test_data/input_datasets/input.csv rename tests/test_data/{custom_datasets => input_datasets}/microarray.normalised.csv (100%) rename tests/test_data/{custom_datasets => input_datasets}/microarray.normalised.design.csv (100%) rename tests/test_data/{custom_datasets => input_datasets}/rnaseq.raw.csv (100%) rename tests/test_data/{custom_datasets => input_datasets}/rnaseq.raw.design.csv (100%) diff --git a/conf/test_dataset.config b/conf/test_dataset.config index 9049c848..5a338c0b 100644 --- a/conf/test_dataset.config +++ b/conf/test_dataset.config @@ -6,7 +6,7 @@ It tests the different ways to use the pipeline, with small data Use as follows: - nextflow run nf-core/stableexpression -profile test, --outdir + nextflow run nf-core/stableexpression -profile test_dataset, --outdir ---------------------------------------------------------------------------------------- */ @@ -17,6 +17,6 @@ params { // Input data species = 'solanum tuberosum' - datasets = "tests/test_data/custom_datasets/input.csv" + datasets = "tests/test_data/input_datasets/input.csv" outdir = "results" } diff --git a/conf/test_dataset_custom_mapping.config b/conf/test_dataset_custom_mapping.config index 52994331..3ec42a50 100644 --- a/conf/test_dataset_custom_mapping.config +++ b/conf/test_dataset_custom_mapping.config @@ -6,7 +6,7 @@ It tests the different ways to use the pipeline, with small data Use as follows: - nextflow run nf-core/stableexpression -profile test, --outdir + nextflow run nf-core/stableexpression -profile test_dataset_custom_mapping, --outdir ---------------------------------------------------------------------------------------- */ @@ -17,9 +17,9 @@ params { // Input data species = 'solanum tuberosum' - datasets = "tests/test_data/custom_datasets/input.csv" + datasets = "tests/test_data/input_datasets/input.csv" skip_gprofiler = true - gene_id_mapping = "tests/test_data/custom_datasets/mapping.csv" - gene_metadata = "tests/test_data/custom_datasets/metadata.csv" + gene_id_mapping = "tests/test_data/input_datasets/mapping.csv" + gene_metadata = "tests/test_data/input_datasets/metadata.csv" outdir = "results" } diff --git a/conf/test_local_and_downloaded.config b/conf/test_local_and_downloaded.config index ea6f22af..7e37b6fc 100644 --- a/conf/test_local_and_downloaded.config +++ b/conf/test_local_and_downloaded.config @@ -6,7 +6,7 @@ It tests the different ways to use the pipeline, with small data Use as follows: - nextflow run nf-core/stableexpression -profile test, --outdir + nextflow run nf-core/stableexpression -profile test_local_and_downloaded, --outdir ---------------------------------------------------------------------------------------- */ @@ -27,6 +27,6 @@ params { species = 'solanum tuberosum' eatlas_keywords = "potato,stress" eatlas_accessions = "E-MTAB-552" - datasets = "tests/test_data/custom_datasets/input.csv" + datasets = "tests/test_data/input_datasets/input.csv" outdir = "results" } diff --git a/conf/test_one_accession.config b/conf/test_one_accession.config index b54b01b8..ba2977e3 100644 --- a/conf/test_one_accession.config +++ b/conf/test_one_accession.config @@ -6,7 +6,7 @@ It tests the different ways to use the pipeline, with small data Use as follows: - nextflow run nf-core/stableexpression -profile test, --outdir + nextflow run nf-core/stableexpression -profile test_one_accession, --outdir ---------------------------------------------------------------------------------------- */ diff --git a/conf/test_one_accession_low_gene_count.config b/conf/test_one_accession_low_gene_count.config index 03bd4250..eaf773b6 100644 --- a/conf/test_one_accession_low_gene_count.config +++ b/conf/test_one_accession_low_gene_count.config @@ -6,7 +6,7 @@ It tests the different ways to use the pipeline, with small data Use as follows: - nextflow run nf-core/stableexpression -profile test, --outdir + nextflow run nf-core/stableexpression -profile test_one_accession_low_gene_count, --outdir ---------------------------------------------------------------------------------------- */ diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index 9e726b40..c581d36d 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -12,6 +12,7 @@ import org.yaml.snakeyaml.Yaml 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' diff --git a/tests/subworkflows/local/expression_normalisation/main.nf.test b/tests/subworkflows/local/expression_normalisation/main.nf.test index d5407534..9a08c61f 100644 --- a/tests/subworkflows/local/expression_normalisation/main.nf.test +++ b/tests/subworkflows/local/expression_normalisation/main.nf.test @@ -11,10 +11,10 @@ nextflow_workflow { when { workflow { """ - rnaseq_raw_file = file( '$projectDir/tests/test_data/custom_datasets/rnaseq.raw.csv', checkIfExists: true ) - rnaseq_raw_design_file = file( '$projectDir/tests/test_data/custom_datasets/rnaseq.raw.design.csv', checkIfExists: true ) - microarray_normalised_file = file( '$projectDir/tests/test_data/custom_datasets/microarray.normalised.csv', checkIfExists: true ) - microarray_normalised_design_file = file( '$projectDir/tests/test_data/custom_datasets/microarray.normalised.design.csv', checkIfExists: true ) + rnaseq_raw_file = file( '$projectDir/tests/test_data/input_datasets/rnaseq.raw.csv', checkIfExists: true ) + rnaseq_raw_design_file = file( '$projectDir/tests/test_data/input_datasets/rnaseq.raw.design.csv', checkIfExists: true ) + microarray_normalised_file = file( '$projectDir/tests/test_data/input_datasets/microarray.normalised.csv', checkIfExists: true ) + microarray_normalised_design_file = file( '$projectDir/tests/test_data/input_datasets/microarray.normalised.design.csv', checkIfExists: true ) ch_datasets = Channel.of( [ [normalised: false, design: rnaseq_raw_design_file, dataset: "rnaseq_raw", platform: "rnaseq"], rnaseq_raw_file], [ [normalised: true, design: microarray_normalised_design_file, dataset: "microarray_normalised", platform: "microarray"], microarray_normalised_file ] @@ -40,10 +40,10 @@ nextflow_workflow { when { workflow { """ - rnaseq_raw_file = file( '$projectDir/tests/test_data/custom_datasets/rnaseq.raw.csv', checkIfExists: true ) - rnaseq_raw_design_file = file( '$projectDir/tests/test_data/custom_datasets/rnaseq.raw.design.csv', checkIfExists: true ) - microarray_normalised_file = file( '$projectDir/tests/test_data/custom_datasets/microarray.normalised.csv', checkIfExists: true ) - microarray_normalised_design_file = file( '$projectDir/tests/test_data/custom_datasets/microarray.normalised.design.csv', checkIfExists: true ) + rnaseq_raw_file = file( '$projectDir/tests/test_data/input_datasets/rnaseq.raw.csv', checkIfExists: true ) + rnaseq_raw_design_file = file( '$projectDir/tests/test_data/input_datasets/rnaseq.raw.design.csv', checkIfExists: true ) + microarray_normalised_file = file( '$projectDir/tests/test_data/input_datasets/microarray.normalised.csv', checkIfExists: true ) + microarray_normalised_design_file = file( '$projectDir/tests/test_data/input_datasets/microarray.normalised.design.csv', checkIfExists: true ) ch_datasets = Channel.of( [ [normalised: false, design: rnaseq_raw_design_file, dataset: "rnaseq_raw", platform: "rnaseq"], rnaseq_raw_file], [ [normalised: true, design: microarray_normalised_design_file, dataset: "microarray_normalised", platform: "microarray"], microarray_normalised_file ] @@ -69,8 +69,8 @@ nextflow_workflow { when { workflow { """ - microarray_normalised_file = file( '$projectDir/tests/test_data/custom_datasets/microarray.normalised.csv', checkIfExists: true ) - microarray_normalised_design_file = file( '$projectDir/tests/test_data/custom_datasets/microarray.normalised.design.csv', checkIfExists: true ) + microarray_normalised_file = file( '$projectDir/tests/test_data/input_datasets/microarray.normalised.csv', checkIfExists: true ) + microarray_normalised_design_file = file( '$projectDir/tests/test_data/input_datasets/microarray.normalised.design.csv', checkIfExists: true ) ch_datasets = Channel.of( [ [normalised: true, design: microarray_normalised_design_file, dataset: "microarray_normalised", platform: "microarray"], microarray_normalised_file ] ) diff --git a/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test.snap b/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test.snap index e43779d1..e4d34695 100644 --- a/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test.snap +++ b/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test.snap @@ -13,7 +13,8 @@ [ { "dataset": "E_MTAB_4251_rnaseq", - "design": "E_MTAB_4251_rnaseq.design.csv:md5,4f3ef7b76ca6ed1ec3157295bc4a8d84" + "design": "E_MTAB_4251_rnaseq.design.csv:md5,4f3ef7b76ca6ed1ec3157295bc4a8d84", + "normalised": false }, "E_MTAB_4251_rnaseq.rnaseq.raw.counts.csv:md5,5cf27be0e00b93d5d431754ba8058687" ], @@ -57,7 +58,8 @@ [ { "dataset": "E_MTAB_4251_rnaseq", - "design": "E_MTAB_4251_rnaseq.design.csv:md5,4f3ef7b76ca6ed1ec3157295bc4a8d84" + "design": "E_MTAB_4251_rnaseq.design.csv:md5,4f3ef7b76ca6ed1ec3157295bc4a8d84", + "normalised": false }, "E_MTAB_4251_rnaseq.rnaseq.raw.counts.csv:md5,5cf27be0e00b93d5d431754ba8058687" ], @@ -96,6 +98,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.0" }, - "timestamp": "2025-05-12T16:09:37.515591183" + "timestamp": "2025-05-14T15:02:23.965187481" } -} \ No newline at end of file +} diff --git a/tests/test_data/custom_datasets/input.csv b/tests/test_data/custom_datasets/input.csv deleted file mode 100644 index 697e034b..00000000 --- a/tests/test_data/custom_datasets/input.csv +++ /dev/null @@ -1,3 +0,0 @@ -counts,design,platform,normalised -tests/test_data/custom_datasets/microarray.normalised.csv,tests/test_data/custom_datasets/microarray.normalised.design.csv,microarray,true -tests/test_data/custom_datasets/rnaseq.raw.csv,tests/test_data/custom_datasets/rnaseq.raw.design.csv,rnaseq,false diff --git a/tests/test_data/input_datasets/input.csv b/tests/test_data/input_datasets/input.csv new file mode 100644 index 00000000..6ea4aa16 --- /dev/null +++ b/tests/test_data/input_datasets/input.csv @@ -0,0 +1,3 @@ +counts,design,platform,normalised +tests/test_data/input_datasets/microarray.normalised.csv,tests/test_data/input_datasets/microarray.normalised.design.csv,microarray,true +tests/test_data/input_datasets/rnaseq.raw.csv,tests/test_data/input_datasets/rnaseq.raw.design.csv,rnaseq,false diff --git a/tests/test_data/custom_datasets/microarray.normalised.csv b/tests/test_data/input_datasets/microarray.normalised.csv similarity index 100% rename from tests/test_data/custom_datasets/microarray.normalised.csv rename to tests/test_data/input_datasets/microarray.normalised.csv diff --git a/tests/test_data/custom_datasets/microarray.normalised.design.csv b/tests/test_data/input_datasets/microarray.normalised.design.csv similarity index 100% rename from tests/test_data/custom_datasets/microarray.normalised.design.csv rename to tests/test_data/input_datasets/microarray.normalised.design.csv diff --git a/tests/test_data/custom_datasets/rnaseq.raw.csv b/tests/test_data/input_datasets/rnaseq.raw.csv similarity index 100% rename from tests/test_data/custom_datasets/rnaseq.raw.csv rename to tests/test_data/input_datasets/rnaseq.raw.csv diff --git a/tests/test_data/custom_datasets/rnaseq.raw.design.csv b/tests/test_data/input_datasets/rnaseq.raw.design.csv similarity index 100% rename from tests/test_data/custom_datasets/rnaseq.raw.design.csv rename to tests/test_data/input_datasets/rnaseq.raw.design.csv From 6ddf979efb2dd024e34e46e2dcf22c0e82475e6e Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sun, 18 May 2025 06:16:40 +0200 Subject: [PATCH 008/258] refactor main workflow --- workflows/stableexpression.nf | 45 +++++++++++++++-------------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 68370310..66832e39 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -47,7 +47,9 @@ workflow STABLEEXPRESSION { ) // putting all datasets together (local datasets + Expression Atlas datasets) - ch_datasets = ch_input_datasets.concat( EXPRESSIONATLAS_FETCHDATA.out.downloaded_datasets ) + ch_input_datasets + .concat( EXPRESSIONATLAS_FETCHDATA.out.downloaded_datasets ) + .set { ch_datasets } // // MODULE: ID Mapping @@ -72,11 +74,16 @@ workflow STABLEEXPRESSION { ch_datasets.combine( ch_species ), params.gene_id_mapping ? Channel.fromPath( params.gene_id_mapping, checkIfExists: true ) : 'none' ) - ch_datasets = IDMAPPING_GPROFILER.out.renamed - ch_gene_metadata = ch_gene_metadata.mix( IDMAPPING_GPROFILER.out.metadata ) + + IDMAPPING_GPROFILER.out.renamed.set { ch_datasets } + + ch_gene_metadata + .mix( IDMAPPING_GPROFILER.out.metadata ) + .set { ch_gene_metadata } + // the gene id mappings are the sum // of those provided by the user and those fetched from g:Profiler - ch_gene_id_mapping = IDMAPPING_GPROFILER.out.mapping + IDMAPPING_GPROFILER.out.mapping.set { ch_gene_id_mapping } } // @@ -87,37 +94,23 @@ workflow STABLEEXPRESSION { ch_datasets, params.normalisation_method ) - ch_normalised_counts = EXPRESSION_NORMALISATION.out.normalised_counts - ch_dataset_statistics = EXPRESSION_NORMALISATION.out.dataset_statistics + + EXPRESSION_NORMALISATION.out.normalised_counts.set { ch_normalised_counts } + EXPRESSION_NORMALISATION.out.dataset_statistics.set { ch_dataset_statistics } // // MODULE: Merge count files and design files and filter out zero counts // - ch_normalised_counts - .map { meta, file -> [file] } - .collect() - .set { ch_count_files } - - ch_normalised_counts - .map { meta, file -> [meta.design] } - .collect() - .set { ch_design_files } - - ch_dataset_statistics - .map { meta, file -> [file] } - .collect() - .set { ch_dataset_stat_files } - MERGE_DATA( - ch_count_files, - ch_design_files, - ch_dataset_stat_files, + ch_normalised_counts.map { meta, file -> [file] }.collect(), + ch_normalised_counts.map { meta, file -> [meta.design] }.collect(), + ch_dataset_statistics.map { meta, file -> [file] }.collect(), params.nb_top_gene_candidates ) - ch_candidate_gene_counts = MERGE_DATA.out.candidate_gene_counts - ch_ks_stats = MERGE_DATA.out.ks_test_statistics + MERGE_DATA.out.candidate_gene_counts.set { ch_candidate_gene_counts } + MERGE_DATA.out.ks_test_statistics.set { ch_ks_stats } // // MODULE: Gene statistics From 6d08ff18872b9a249d5260aaea0085f88cf4ca95 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sun, 18 May 2025 07:22:10 +0200 Subject: [PATCH 009/258] refactor expression atlas get accessions --- bin/get_eatlas_accessions.py | 6 +-- .../getaccessions/environment.yml | 1 + .../expressionatlas/getaccessions/main.nf | 16 ++++--- .../getaccessions/main.nf.test.snap | 42 +++++++++++++------ 4 files changed, 44 insertions(+), 21 deletions(-) diff --git a/bin/get_eatlas_accessions.py b/bin/get_eatlas_accessions.py index 06347785..b2ac2a95 100755 --- a/bin/get_eatlas_accessions.py +++ b/bin/get_eatlas_accessions.py @@ -11,7 +11,7 @@ wait_exponential, before_sleep_log, ) -import json +import yaml from functools import partial from multiprocessing import Pool import nltk @@ -23,7 +23,7 @@ ALL_EXP_URL = "https://www.ebi.ac.uk/gxa/json/experiments/" ACCESSION_OUTFILE_NAME = "accessions.txt" -FILTERED_EXPERIMENTS_OUTFILE_NAME = "filtered_experiments.json" +FILTERED_EXPERIMENTS_OUTFILE_NAME = "filtered_experiments.yaml" ################################################################## ################################################################## @@ -437,7 +437,7 @@ def main(): logger.info(f"Writing filtered experiments to {FILTERED_EXPERIMENTS_OUTFILE_NAME}") with open(FILTERED_EXPERIMENTS_OUTFILE_NAME, "w") as fout: - json.dump(results, fout) + yaml.dump(results, fout) if __name__ == "__main__": diff --git a/modules/local/expressionatlas/getaccessions/environment.yml b/modules/local/expressionatlas/getaccessions/environment.yml index a6e12662..7a9ed092 100644 --- a/modules/local/expressionatlas/getaccessions/environment.yml +++ b/modules/local/expressionatlas/getaccessions/environment.yml @@ -7,3 +7,4 @@ dependencies: - conda-forge::nltk==3.9.1 - conda-forge::tenacity==9.0.0 - conda-forge::pandas==2.2.3 + - conda-forge::pyyaml==6.0.2 diff --git a/modules/local/expressionatlas/getaccessions/main.nf b/modules/local/expressionatlas/getaccessions/main.nf index 8c5dcf88..48b44fcb 100644 --- a/modules/local/expressionatlas/getaccessions/main.nf +++ b/modules/local/expressionatlas/getaccessions/main.nf @@ -4,15 +4,16 @@ process EXPRESSIONATLAS_GETACCESSIONS { conda "${moduleDir}/environment.yml" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/e4/e459ae44332297f0429e7dd501bc3a6f9b5504b13e2db0002a5d3021cc9ac443/data': - 'community.wave.seqera.io/library/nltk_pandas_python_requests_tenacity:a29bfda256e4f39f' }" + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/5e/5e3d9b407277b8bb8f8850eba40724b1cae9bd6e11ae0019011af82e6ac17cd4/data': + 'community.wave.seqera.io/library/nltk_pandas_python_pyyaml_pruned:2218f9c10723fbf3' }" input: val species val keywords output: - path 'accessions.txt', emit: txt + path "accessions.txt", emit: txt + path "filtered_experiments.yaml", emit: filtered_experiments tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('requests'), eval('python3 -c "import requests; print(requests.__version__)"'), topic: versions tuple val("${task.process}"), val('nltk'), eval('python3 -c "import nltk; print(nltk.__version__)"'), topic: versions @@ -24,18 +25,21 @@ process EXPRESSIONATLAS_GETACCESSIONS { // the folder where nltk will download data needs to be writable (necessary for singularity) if (keywords_string == "") { """ - NLTK_DATA=$PWD get_eatlas_accessions.py --species $species + NLTK_DATA=$PWD get_eatlas_accessions.py \ + --species $species \ """ } else { """ - NLTK_DATA=$PWD get_eatlas_accessions.py --species $species --keywords $keywords_string + NLTK_DATA=$PWD get_eatlas_accessions.py \ + --species $species \ + --keywords $keywords_string """ } stub: """ - touch accessions.txt + touch accessions.txt filtered_experiments.yaml """ } diff --git a/tests/modules/local/expressionatlas/getaccessions/main.nf.test.snap b/tests/modules/local/expressionatlas/getaccessions/main.nf.test.snap index 1bcaf1d0..92fbf4cd 100644 --- a/tests/modules/local/expressionatlas/getaccessions/main.nf.test.snap +++ b/tests/modules/local/expressionatlas/getaccessions/main.nf.test.snap @@ -6,26 +6,32 @@ "accessions.txt:md5,dad63ac7a1715277fa44567bc40b5872" ], "1": [ + "filtered_experiments.yaml:md5,2848656a9bdeeecb510eac8965a5d0e3" + ], + "2": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "python", "3.12.8" ] ], - "2": [ + "3": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "requests", "2.32.3" ] ], - "3": [ + "4": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "nltk", "3.9.1" ] ], + "filtered_experiments": [ + "filtered_experiments.yaml:md5,2848656a9bdeeecb510eac8965a5d0e3" + ], "txt": [ "accessions.txt:md5,dad63ac7a1715277fa44567bc40b5872" ] @@ -33,9 +39,9 @@ ], "meta": { "nf-test": "0.9.2", - "nextflow": "24.10.3" + "nextflow": "25.04.2" }, - "timestamp": "2025-01-01T14:25:08.425684541" + "timestamp": "2025-05-18T07:05:49.797203628" }, "Solanum tuberosum one keyword": { "content": [ @@ -44,26 +50,32 @@ "accessions.txt:md5,dad63ac7a1715277fa44567bc40b5872" ], "1": [ + "filtered_experiments.yaml:md5,7a89b3c95e77725ca2e85eaea29041c0" + ], + "2": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "python", "3.12.8" ] ], - "2": [ + "3": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "requests", "2.32.3" ] ], - "3": [ + "4": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "nltk", "3.9.1" ] ], + "filtered_experiments": [ + "filtered_experiments.yaml:md5,7a89b3c95e77725ca2e85eaea29041c0" + ], "txt": [ "accessions.txt:md5,dad63ac7a1715277fa44567bc40b5872" ] @@ -71,9 +83,9 @@ ], "meta": { "nf-test": "0.9.2", - "nextflow": "24.10.3" + "nextflow": "25.04.2" }, - "timestamp": "2025-01-01T14:23:43.573176854" + "timestamp": "2025-05-18T07:05:09.434053957" }, "Solanum tuberosum two keywords": { "content": [ @@ -82,26 +94,32 @@ "accessions.txt:md5,dad63ac7a1715277fa44567bc40b5872" ], "1": [ + "filtered_experiments.yaml:md5,78d8835569150e5051bf4d730fc47f79" + ], + "2": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "python", "3.12.8" ] ], - "2": [ + "3": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "requests", "2.32.3" ] ], - "3": [ + "4": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "nltk", "3.9.1" ] ], + "filtered_experiments": [ + "filtered_experiments.yaml:md5,78d8835569150e5051bf4d730fc47f79" + ], "txt": [ "accessions.txt:md5,dad63ac7a1715277fa44567bc40b5872" ] @@ -109,8 +127,8 @@ ], "meta": { "nf-test": "0.9.2", - "nextflow": "24.10.3" + "nextflow": "25.04.2" }, - "timestamp": "2025-01-01T14:24:29.42124266" + "timestamp": "2025-05-18T07:05:31.438421087" } } \ No newline at end of file From 5e0a687dbe5e785b60be5369eb4856c05122cd4e Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Tue, 17 Jun 2025 22:38:35 +0200 Subject: [PATCH 010/258] change param for fetching eatlas data --- nextflow.config | 6 +++--- nextflow_schema.json | 6 +++--- subworkflows/local/expressionatlas_fetchdata/main.nf | 4 ++-- .../utils_nfcore_stableexpression_pipeline/main.nf | 10 ---------- .../local/expressionatlas_fetchdata/main.nf.test | 2 +- workflows/stableexpression.nf | 2 +- 6 files changed, 10 insertions(+), 20 deletions(-) diff --git a/nextflow.config b/nextflow.config index 79aeee03..7bcca910 100644 --- a/nextflow.config +++ b/nextflow.config @@ -31,9 +31,9 @@ params { eatlas_accessions = "" // Expression atlas - fetch_eatlas_accessions = false - eatlas_keywords = "" - eatlas_accessions = "" + skip_fetch_eatlas_accessions = false + eatlas_keywords = "" + eatlas_accessions = "" // Boilerplate options outdir = null diff --git a/nextflow_schema.json b/nextflow_schema.json index edf79775..93ac4caa 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -56,11 +56,11 @@ "fa_icon": "fas fa-book-atlas", "description": "Options for fetching datasets from Expression Atlas.", "properties": { - "fetch_eatlas_accessions": { + "skip_fetch_eatlas_accessions": { "type": "boolean", "fa_icon": "fas fa-cloud-arrow-down", - "description": "Automatically etches Expression Atlas accessions for this species. and downloads the corresponding count datasets and experimental designs.", - "help_text": "If no Expression Atlas keywords are provided (with `--eatlas_keywords`) and if you want to get all Expression Atlas accessions for this species, provide this parameter." + "description": "Skip fetching Expression Atlas accessions.", + "help_text": "Expression Atlas accessions are automatically fetched by default. You can skip this step by setting this parameter." }, "eatlas_keywords": { "type": "string", diff --git a/subworkflows/local/expressionatlas_fetchdata/main.nf b/subworkflows/local/expressionatlas_fetchdata/main.nf index 439ebd86..1fab05f0 100644 --- a/subworkflows/local/expressionatlas_fetchdata/main.nf +++ b/subworkflows/local/expressionatlas_fetchdata/main.nf @@ -23,7 +23,7 @@ workflow EXPRESSIONATLAS_FETCHDATA { ch_species eatlas_accessions eatlas_keywords - fetch_eatlas_accessions + skip_fetch_eatlas_accessions main: @@ -31,7 +31,7 @@ workflow EXPRESSIONATLAS_FETCHDATA { ch_accessions = Channel.fromList( eatlas_accessions.tokenize(',') ) // fetching Expression Atlas accessions if applicable - if ( fetch_eatlas_accessions || eatlas_keywords ) { + if ( !skip_fetch_eatlas_accessions || eatlas_keywords ) { // // MODULE: Expression Atlas - Get accessions diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index c581d36d..1e66b144 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -147,16 +147,6 @@ def validateInputParameters(params) { error('You must provide a species name') } - // checking that the user has provided at least one dataset and / or expression atlas arguments - if ( - !params.datasets - && !params.eatlas_accessions - && !params.fetch_eatlas_accessions - && !params.eatlas_keywords - ) { - error('You must provide at least either --datasets or --fetch_eatlas_accessions or --eatlas_accessions or --eatlas_keywords') - } - // if expression atlas accessions are provided, checking that they are well formated if ( params.eatlas_accessions ) { for ( accession in params.eatlas_accessions.tokenize(',') ) { diff --git a/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test b/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test index 421785b0..d88ddc26 100644 --- a/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test +++ b/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test @@ -14,7 +14,7 @@ nextflow_workflow { species = 'solanum tuberosum' eatlas_accessions = "E-MTAB-552,E-GEOD-61690 ,E-PROT-138" eatlas_keywords = "potato,stress" - fetch_eatlas_accessions = false // no impact since we define keywords + skip_fetch_eatlas_accessions = false input[0] = Channel.value( species.split(' ').join('_') ) input[1] = eatlas_accessions diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 66832e39..5383c4ff 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -43,7 +43,7 @@ workflow STABLEEXPRESSION { ch_species, params.eatlas_accessions, params.eatlas_keywords, - params.fetch_eatlas_accessions + params.skip_fetch_eatlas_accessions ) // putting all datasets together (local datasets + Expression Atlas datasets) From 8e93a552c02a7a3fb07515218719657ffb65e6c5 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Tue, 17 Jun 2025 22:56:18 +0200 Subject: [PATCH 011/258] add parameter to fetch eatlas accessions only --- nextflow.config | 1 + nextflow_schema.json | 6 ++++ .../local/expressionatlas_fetchdata/main.nf | 36 +++++++++---------- .../expressionatlas_fetchdata/main.nf.test | 31 +++++++++++++++- workflows/stableexpression.nf | 8 ++--- 5 files changed, 57 insertions(+), 25 deletions(-) diff --git a/nextflow.config b/nextflow.config index 7bcca910..af72cdc3 100644 --- a/nextflow.config +++ b/nextflow.config @@ -34,6 +34,7 @@ params { skip_fetch_eatlas_accessions = false eatlas_keywords = "" eatlas_accessions = "" + accessions_only = false // Boilerplate options outdir = null diff --git a/nextflow_schema.json b/nextflow_schema.json index 93ac4caa..8aa058e8 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -75,6 +75,12 @@ "description": "Provide directly Expression Atlas accession(s) (separated by commas) that you want to download.", "fa_icon": "fas fa-id-card", "help_text": "Example: `--eatlas_accessions 'E-MTAB-552,E-GEOD-61690'`" + }, + "accessions_only": { + "type": "boolean", + "description": "Only get accessions from Expression Atlas and exit.", + "fa_icon": "fas fa-id-card", + "help_text": "Use this option if you want to only get Expression Atlas accessions and skip the rest of the pipeline." } } }, diff --git a/subworkflows/local/expressionatlas_fetchdata/main.nf b/subworkflows/local/expressionatlas_fetchdata/main.nf index 1fab05f0..a2bb49f7 100644 --- a/subworkflows/local/expressionatlas_fetchdata/main.nf +++ b/subworkflows/local/expressionatlas_fetchdata/main.nf @@ -21,40 +21,40 @@ workflow EXPRESSIONATLAS_FETCHDATA { take: ch_species - eatlas_accessions - eatlas_keywords - skip_fetch_eatlas_accessions main: - ch_accessions = Channel.fromList( eatlas_accessions.tokenize(',') ) + ch_accessions = Channel.fromList( params.eatlas_accessions.tokenize(',') ) + ch_eatlas_datasets = Channel.empty() // fetching Expression Atlas accessions if applicable - if ( !skip_fetch_eatlas_accessions || eatlas_keywords ) { + if ( !params.skip_fetch_eatlas_accessions || params.eatlas_keywords ) { - // - // MODULE: Expression Atlas - Get accessions - // - ch_eatlas_keywords = Channel.value( eatlas_keywords ) + ch_eatlas_keywords = Channel.value( ) // getting Expression Atlas accessions given a species name and keywords // keywords can be an empty string - EXPRESSIONATLAS_GETACCESSIONS( ch_species, ch_eatlas_keywords ) + EXPRESSIONATLAS_GETACCESSIONS( + ch_species, + params.eatlas_keywords + ) // appending to accessions provided by the user // ensures that no accessions is present twice (provided by the user and fetched from E. Atlas) // removing E-PROT- accessions - ch_accessions = ch_accessions - .concat( EXPRESSIONATLAS_GETACCESSIONS.out.txt.splitText() ) - .unique() - .map { it -> it.trim() } - .filter { it.startsWith('E-') && !it.startsWith('E-PROT-') } + ch_accessions + .concat( EXPRESSIONATLAS_GETACCESSIONS.out.txt.splitText() ) + .unique() + .map { it -> it.trim() } + .filter { it.startsWith('E-') && !it.startsWith('E-PROT-') } + .set ( ch_accessions ) } - // - // MODULE: Expression Atlas - Get data - // + if ( params.accessions_only ) { + log.info "Exporting Expression Atlas accessions and exiting." + System.exit(0) + } // Downloading Expression Atlas data for each accession in ch_accessions EXPRESSIONATLAS_GETDATA( ch_accessions ) diff --git a/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test b/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test index d88ddc26..bcc3786f 100644 --- a/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test +++ b/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test @@ -6,7 +6,7 @@ nextflow_workflow { tag "expressionatlas_fetchdata" tag "subworkflow" - test("Should run without failures") { + test("Download Expression Atlas datasets") { when { workflow { @@ -15,6 +15,35 @@ nextflow_workflow { eatlas_accessions = "E-MTAB-552,E-GEOD-61690 ,E-PROT-138" eatlas_keywords = "potato,stress" skip_fetch_eatlas_accessions = false + accessions_only = false + + input[0] = Channel.value( species.split(' ').join('_') ) + input[1] = eatlas_accessions + input[2] = eatlas_keywords + input[3] = fetch_eatlas_accessions + """ + } + } + + then { + assertAll( + { assert workflow.success }, + { assert snapshot(workflow.out).match() } + ) + } + + } + + test("Download Expression Atlas accessions only") { + + when { + workflow { + """ + species = 'solanum tuberosum' + eatlas_accessions = "E-MTAB-552,E-GEOD-61690 ,E-PROT-138" + eatlas_keywords = "potato,stress" + skip_fetch_eatlas_accessions = false + accessions_only = true input[0] = Channel.value( species.split(' ').join('_') ) input[1] = eatlas_accessions diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 5383c4ff..6d312cd5 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -33,18 +33,14 @@ workflow STABLEEXPRESSION { main: ch_multiqc_files = Channel.empty() + ch_species = Channel.value( params.species.split(' ').join('_') ) // // SUBWORKFLOW: fetching Expression Atlas datasets if needed // - EXPRESSIONATLAS_FETCHDATA( - ch_species, - params.eatlas_accessions, - params.eatlas_keywords, - params.skip_fetch_eatlas_accessions - ) + EXPRESSIONATLAS_FETCHDATA( ch_species ) // putting all datasets together (local datasets + Expression Atlas datasets) ch_input_datasets From 4170ed959c864d1e2a0d3ce3458ac7764bd5c9be Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Tue, 17 Jun 2025 23:09:21 +0200 Subject: [PATCH 012/258] put idmapping in separate subworkflow --- modules/local/idmapping/gprofiler/main.nf | 3 +- nextflow.config | 2 +- nextflow_schema.json | 2 +- .../local/expression_normalisation/main.nf | 10 --- .../local/expressionatlas_fetchdata/main.nf | 16 +---- subworkflows/local/idmapping/main.nf | 48 +++++++++++++ workflows/stableexpression.nf | 72 ++++++------------- 7 files changed, 77 insertions(+), 76 deletions(-) create mode 100644 subworkflows/local/idmapping/main.nf diff --git a/modules/local/idmapping/gprofiler/main.nf b/modules/local/idmapping/gprofiler/main.nf index d010c426..dfcca8e6 100644 --- a/modules/local/idmapping/gprofiler/main.nf +++ b/modules/local/idmapping/gprofiler/main.nf @@ -31,7 +31,8 @@ process IDMAPPING_GPROFILER { 'community.wave.seqera.io/library/pandas_python_requests:8c6da05a2935a952' }" input: - tuple val(meta), path(count_file), val(species) + tuple val(meta), path(count_file) + val species val gene_id_mapping_file output: diff --git a/nextflow.config b/nextflow.config index af72cdc3..5b39e90b 100644 --- a/nextflow.config +++ b/nextflow.config @@ -22,7 +22,7 @@ params { // ID mapping gene_metadata = null - gene_id_mapping = null + gene_id_mapping_file = null skip_gprofiler = false // Expression atlas diff --git a/nextflow_schema.json b/nextflow_schema.json index 8aa058e8..384a77e8 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -96,7 +96,7 @@ "fa_icon": "fas fa-ban", "help": "If you don't want to map gene IDs with g:Profiler, you can skip this step by providing `--skip_gprofiler`. It can be in particular useful if the g:Profiler is down and if you already have a custom mapping file." }, - "gene_id_mapping": { + "gene_id_mapping_file": { "type": "string", "format": "file-path", "exists": true, diff --git a/subworkflows/local/expression_normalisation/main.nf b/subworkflows/local/expression_normalisation/main.nf index b5e4bfe9..92c7435c 100644 --- a/subworkflows/local/expression_normalisation/main.nf +++ b/subworkflows/local/expression_normalisation/main.nf @@ -1,13 +1,3 @@ -// -// Subworkflow with functionality specific to the nf-core/stableexpression pipeline -// - -/* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - IMPORT MODULES / SUBWORKFLOWS / FUNCTIONS -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -*/ - include { NORMALISATION_DESEQ2 } from '../../../modules/local/normalisation/deseq2/main' include { NORMALISATION_EDGER } from '../../../modules/local/normalisation/edger/main' include { QUANTILE_NORMALISATION } from '../../../modules/local/quantile_normalisation/main' diff --git a/subworkflows/local/expressionatlas_fetchdata/main.nf b/subworkflows/local/expressionatlas_fetchdata/main.nf index a2bb49f7..14d4a292 100644 --- a/subworkflows/local/expressionatlas_fetchdata/main.nf +++ b/subworkflows/local/expressionatlas_fetchdata/main.nf @@ -1,13 +1,3 @@ -// -// Subworkflow with functionality specific to the nf-core/stableexpression pipeline -// - -/* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - IMPORT MODULES / SUBWORKFLOWS / FUNCTIONS -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -*/ - include { EXPRESSIONATLAS_GETACCESSIONS } from '../../../modules/local/expressionatlas/getaccessions/main' include { EXPRESSIONATLAS_GETDATA } from '../../../modules/local/expressionatlas/getdata/main' @@ -20,7 +10,7 @@ include { EXPRESSIONATLAS_GETDATA } from '../../../modules/local/ workflow EXPRESSIONATLAS_FETCHDATA { take: - ch_species + species main: @@ -31,12 +21,10 @@ workflow EXPRESSIONATLAS_FETCHDATA { // fetching Expression Atlas accessions if applicable if ( !params.skip_fetch_eatlas_accessions || params.eatlas_keywords ) { - ch_eatlas_keywords = Channel.value( ) - // getting Expression Atlas accessions given a species name and keywords // keywords can be an empty string EXPRESSIONATLAS_GETACCESSIONS( - ch_species, + Channel.value( species ), params.eatlas_keywords ) diff --git a/subworkflows/local/idmapping/main.nf b/subworkflows/local/idmapping/main.nf new file mode 100644 index 00000000..251bdea1 --- /dev/null +++ b/subworkflows/local/idmapping/main.nf @@ -0,0 +1,48 @@ +include { IDMAPPING_GPROFILER } from '../../../modules/local/idmapping/gprofiler/main' + +/* +======================================================================================== + SUBWORKFLOW TO DOWNLOAD EXPRESSIONATLAS ACCESSIONS AND DATASETS +======================================================================================== +*/ + +workflow IDMAPPING { + + take: + ch_datasets + species + + main: + + def ch_gene_metadata = params.gene_metadata ? Channel.fromPath( params.gene_metadata, checkIfExists: true ) : Channel.empty() + + if ( params.skip_gprofiler ) { + + def ch_gene_id_mapping = params.gene_id_mapping_file ? Channel.fromPath( params.gene_id_mapping, checkIfExists: true ) : Channel.empty() + + } else { + + // tries to map gene IDs to Ensembl IDs whenever possible + IDMAPPING_GPROFILER( + ch_datasets, + species, + params.gene_id_mapping_file ? Channel.fromPath( params.gene_id_mapping_file, checkIfExists: true ) : 'none' + ) + + IDMAPPING_GPROFILER.out.renamed.set { ch_datasets } + + ch_gene_metadata + .mix( IDMAPPING_GPROFILER.out.metadata ) + .set { ch_gene_metadata } + + // the gene id mappings are the sum + // of those provided by the user and those fetched from g:Profiler + IDMAPPING_GPROFILER.out.mapping.set { ch_gene_id_mapping } + + } + + emit: + datasets = ch_datasets + gene_metadata = ch_gene_metadata + gene_id_mapping = ch_gene_id_mapping +} diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 6d312cd5..e77a850a 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -5,9 +5,10 @@ */ include { EXPRESSIONATLAS_FETCHDATA } from '../subworkflows/local/expressionatlas_fetchdata/main' +include { IDMAPPING } from '../subworkflows/local/idmapping/main.nf' include { EXPRESSION_NORMALISATION } from '../subworkflows/local/expression_normalisation/main.nf' -include { IDMAPPING_GPROFILER } from '../modules/local/idmapping/gprofiler/main' + include { MERGE_DATA } from '../modules/local/merge_data/main' include { GENE_STATISTICS } from '../modules/local/gene_statistics/main' include { MULTIQC } from '../modules/nf-core/multiqc/main' @@ -32,71 +33,43 @@ workflow STABLEEXPRESSION { main: + ch_multiqc_files = Channel.empty() - ch_species = Channel.value( params.species.split(' ').join('_') ) + species = params.species.split(' ').join('_') - // - // SUBWORKFLOW: fetching Expression Atlas datasets if needed - // + // ----------------------------------------------------------------- + // FETCH EXPRESSION ATLAS DATASETS IF NEEDED + // ----------------------------------------------------------------- - EXPRESSIONATLAS_FETCHDATA( ch_species ) + EXPRESSIONATLAS_FETCHDATA( species ) // putting all datasets together (local datasets + Expression Atlas datasets) ch_input_datasets .concat( EXPRESSIONATLAS_FETCHDATA.out.downloaded_datasets ) .set { ch_datasets } - // - // MODULE: ID Mapping - // - - ch_gene_metadata = Channel.empty() - if ( params.gene_metadata ) { - ch_gene_metadata = Channel.fromPath( params.gene_metadata, checkIfExists: true ) - } - - if ( params.skip_gprofiler ) { - - ch_gene_id_mapping = Channel.empty() - if ( params.gene_id_mapping ) { - // the gene id mappings will only be those provided by the user - ch_gene_id_mapping = Channel.fromPath( params.gene_id_mapping, checkIfExists: true ) - } - - } else { - // tries to map gene IDs to Ensembl IDs whenever possible - IDMAPPING_GPROFILER( - ch_datasets.combine( ch_species ), - params.gene_id_mapping ? Channel.fromPath( params.gene_id_mapping, checkIfExists: true ) : 'none' - ) - - IDMAPPING_GPROFILER.out.renamed.set { ch_datasets } - - ch_gene_metadata - .mix( IDMAPPING_GPROFILER.out.metadata ) - .set { ch_gene_metadata } + // ----------------------------------------------------------------- + // IDMAPPING + // ----------------------------------------------------------------- - // the gene id mappings are the sum - // of those provided by the user and those fetched from g:Profiler - IDMAPPING_GPROFILER.out.mapping.set { ch_gene_id_mapping } - } + IDMAPPING ( ch_datasets, species ) - // - // SURBWORKFLOW: normalisation of raw count datasets (including RNA-seq datasets) - // + // ----------------------------------------------------------------- + // NORMALISATION OF RAW COUNT DATASETS (INCLUDING RNA-SEQ DATASETS) + // ----------------------------------------------------------------- EXPRESSION_NORMALISATION( - ch_datasets, + IDMAPPING.out.datasets, params.normalisation_method ) EXPRESSION_NORMALISATION.out.normalised_counts.set { ch_normalised_counts } EXPRESSION_NORMALISATION.out.dataset_statistics.set { ch_dataset_statistics } - // + // ----------------------------------------------------------------- // MODULE: Merge count files and design files and filter out zero counts - // + // ----------------------------------------------------------------- MERGE_DATA( ch_normalised_counts.map { meta, file -> [file] }.collect(), @@ -108,13 +81,14 @@ workflow STABLEEXPRESSION { MERGE_DATA.out.candidate_gene_counts.set { ch_candidate_gene_counts } MERGE_DATA.out.ks_test_statistics.set { ch_ks_stats } - // + // ----------------------------------------------------------------- // MODULE: Gene statistics - // + // ----------------------------------------------------------------- + GENE_STATISTICS( MERGE_DATA.out.all_counts, - ch_gene_metadata.collect(), - ch_gene_id_mapping.collect(), + IDMAPPING.out.gene_metadata.collect(), + IDMAPPING.out.gene_id_mapping.collect(), params.nb_top_gene_candidates, ch_ks_stats, params.ks_pvalue_threshold From b823bedd7fe56b89b5c59b99447a96ef230a5745 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Tue, 17 Jun 2025 23:39:51 +0200 Subject: [PATCH 013/258] put multiqc in separate subworkflow --- assets/multiqc_config.yml | 2 +- .../local/expression_normalisation/main.nf | 8 +-- .../local/expressionatlas_fetchdata/main.nf | 6 +- subworkflows/local/idmapping/main.nf | 2 +- subworkflows/local/multiqc/main.nf | 63 +++++++++++++++++ workflows/stableexpression.nf | 68 +++---------------- 6 files changed, 81 insertions(+), 68 deletions(-) create mode 100644 subworkflows/local/multiqc/main.nf diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index 5c965ffa..90cea002 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -237,7 +237,7 @@ sp: fn: "*top_stable_genes_summary.csv" max_filesize: 5000000 # 5MB expression_distributions_top_stable_genes: - fn: "*top_stable_genes_transposed_counts.csv" + fn: "*top_stable_genes_transposed_counts*.csv" max_filesize: 50000000 # 50MB gene_statistics: fn: "*stats_all_genes.csv" diff --git a/subworkflows/local/expression_normalisation/main.nf b/subworkflows/local/expression_normalisation/main.nf index 92c7435c..8960bd94 100644 --- a/subworkflows/local/expression_normalisation/main.nf +++ b/subworkflows/local/expression_normalisation/main.nf @@ -1,7 +1,7 @@ -include { NORMALISATION_DESEQ2 } from '../../../modules/local/normalisation/deseq2/main' -include { NORMALISATION_EDGER } from '../../../modules/local/normalisation/edger/main' -include { QUANTILE_NORMALISATION } from '../../../modules/local/quantile_normalisation/main' -include { DATASET_STATISTICS } from '../../../modules/local/dataset_statistics/main' +include { NORMALISATION_DESEQ2 } from '../../../modules/local/normalisation/deseq2' +include { NORMALISATION_EDGER } from '../../../modules/local/normalisation/edger' +include { QUANTILE_NORMALISATION } from '../../../modules/local/quantile_normalisation' +include { DATASET_STATISTICS } from '../../../modules/local/dataset_statistics' /* ======================================================================================== diff --git a/subworkflows/local/expressionatlas_fetchdata/main.nf b/subworkflows/local/expressionatlas_fetchdata/main.nf index 14d4a292..77922612 100644 --- a/subworkflows/local/expressionatlas_fetchdata/main.nf +++ b/subworkflows/local/expressionatlas_fetchdata/main.nf @@ -1,5 +1,5 @@ -include { EXPRESSIONATLAS_GETACCESSIONS } from '../../../modules/local/expressionatlas/getaccessions/main' -include { EXPRESSIONATLAS_GETDATA } from '../../../modules/local/expressionatlas/getdata/main' +include { EXPRESSIONATLAS_GETACCESSIONS } from '../../../modules/local/expressionatlas/getaccessions' +include { EXPRESSIONATLAS_GETDATA } from '../../../modules/local/expressionatlas/getdata' /* ======================================================================================== @@ -32,7 +32,7 @@ workflow EXPRESSIONATLAS_FETCHDATA { // ensures that no accessions is present twice (provided by the user and fetched from E. Atlas) // removing E-PROT- accessions ch_accessions - .concat( EXPRESSIONATLAS_GETACCESSIONS.out.txt.splitText() ) + .concat( EXPRESSIONATLAS_GETACCESSIONS.out.txt.splitText(expression_normalisation) ) .unique() .map { it -> it.trim() } .filter { it.startsWith('E-') && !it.startsWith('E-PROT-') } diff --git a/subworkflows/local/idmapping/main.nf b/subworkflows/local/idmapping/main.nf index 251bdea1..06f072ee 100644 --- a/subworkflows/local/idmapping/main.nf +++ b/subworkflows/local/idmapping/main.nf @@ -1,4 +1,4 @@ -include { IDMAPPING_GPROFILER } from '../../../modules/local/idmapping/gprofiler/main' +include { IDMAPPING_GPROFILER } from '../../../modules/local/idmapping/gprofiler' /* ======================================================================================== diff --git a/subworkflows/local/multiqc/main.nf b/subworkflows/local/multiqc/main.nf new file mode 100644 index 00000000..45b3fd0e --- /dev/null +++ b/subworkflows/local/multiqc/main.nf @@ -0,0 +1,63 @@ +include { MULTIQC } from '../../../modules/nf-core/multiqc' + +/* +======================================================================================== + SUBWORKFLOW TO DOWNLOAD EXPRESSIONATLAS ACCESSIONS AND DATASETS +======================================================================================== +*/ + +workflow MULTIQC_WORKFLOW { + + take: + ch_multiqc_files + + main: + + // + // Collate and save software versions + // + + ch_collated_versions = customSoftwareVersionsToYAML( Channel.topic('versions') ) + .collectFile( + storeDir: "${params.outdir}/pipeline_info", + name: 'nf_core_' + 'stableexpression_software_' + 'mqc_' + 'versions.yml', + sort: true, + newLine: true + ) + + summary_params = paramsSummaryMap( workflow, parameters_schema: "nextflow_schema.json") + ch_workflow_summary = Channel.value(paramsSummaryMultiqc(summary_params)) + + 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) + + Channel.value( methodsDescriptionText( ch_multiqc_custom_methods_description ) ) + .collectFile( + name: 'methods_description_mqc.yaml', + sort: true + ) + .set { ch_methods_description_file } + + ch_multiqc_files + .mix( ch_workflow_summary.collectFile(name: 'workflow_summary_mqc.yaml') ) + .mix( ch_collated_versions ) + .mix( ch_methods_description_file ) + .set { ch_multiqc_files } + + 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() + + MULTIQC ( + ch_multiqc_files.collect(), + ch_multiqc_config.toList(), + ch_multiqc_custom_config.toList(), + ch_multiqc_logo.toList(), + [], + [] + ) + + emit: + report = MULTIQC.out.report +} diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index e77a850a..d5fb301a 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -4,14 +4,15 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -include { EXPRESSIONATLAS_FETCHDATA } from '../subworkflows/local/expressionatlas_fetchdata/main' -include { IDMAPPING } from '../subworkflows/local/idmapping/main.nf' -include { EXPRESSION_NORMALISATION } from '../subworkflows/local/expression_normalisation/main.nf' +include { EXPRESSIONATLAS_FETCHDATA } from '../subworkflows/local/expressionatlas_fetchdata' +include { IDMAPPING } from '../subworkflows/local/idmapping' +include { EXPRESSION_NORMALISATION } from '../subworkflows/local/expression_normalisation' +include { MULTIQC_WORKFLOW } from '../subworkflows/local/multiqc' -include { MERGE_DATA } from '../modules/local/merge_data/main' -include { GENE_STATISTICS } from '../modules/local/gene_statistics/main' -include { MULTIQC } from '../modules/nf-core/multiqc/main' +include { MERGE_DATA } from '../modules/local/merge_data' +include { GENE_STATISTICS } from '../modules/local/gene_statistics' + include { parseInputDatasets } from '../subworkflows/local/utils_nfcore_stableexpression_pipeline' include { customSoftwareVersionsToYAML } from '../subworkflows/local/utils_nfcore_stableexpression_pipeline' @@ -103,62 +104,11 @@ workflow STABLEEXPRESSION { .mix( ch_ks_stats.collect() ) .mix ( MERGE_DATA.out.distribution_correlations.collect() ) + MULTIQC_WORKFLOW( ch_multiqc_files ) - // - // Collate and save software versions - // TODO: use the nf-core functions when they are adapted to channel topics - // - - ch_collated_versions = customSoftwareVersionsToYAML( Channel.topic('versions') ) - .collectFile( - storeDir: "${params.outdir}/pipeline_info", - name: 'nf_core_' + 'stableexpression_software_' + 'mqc_' + 'versions.yml', - sort: true, - newLine: true - ) - - // - // 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() + multiqc_report = MULTIQC_WORKFLOW.out.report.toList() } From 03bdbf74b4c3e9a96f1d7af760b1cd24d7acb81c Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 18 Jun 2025 07:19:37 +0200 Subject: [PATCH 014/258] fix behaviour with accessions only param --- nextflow.config | 5 - .../local/expressionatlas_fetchdata/main.nf | 36 ++--- subworkflows/local/idmapping/main.nf | 13 +- subworkflows/local/multiqc/main.nf | 5 + workflows/stableexpression.nf | 125 +++++++++--------- 5 files changed, 91 insertions(+), 93 deletions(-) diff --git a/nextflow.config b/nextflow.config index 5b39e90b..67f97379 100644 --- a/nextflow.config +++ b/nextflow.config @@ -25,11 +25,6 @@ params { gene_id_mapping_file = null skip_gprofiler = false - // Expression atlas - fetch_eatlas_accessions = false - eatlas_keywords = "" - eatlas_accessions = "" - // Expression atlas skip_fetch_eatlas_accessions = false eatlas_keywords = "" diff --git a/subworkflows/local/expressionatlas_fetchdata/main.nf b/subworkflows/local/expressionatlas_fetchdata/main.nf index 77922612..3c0b405f 100644 --- a/subworkflows/local/expressionatlas_fetchdata/main.nf +++ b/subworkflows/local/expressionatlas_fetchdata/main.nf @@ -10,21 +10,22 @@ include { EXPRESSIONATLAS_GETDATA } from '../../../modules/local/ workflow EXPRESSIONATLAS_FETCHDATA { take: - species + ch_species main: - ch_accessions = Channel.fromList( params.eatlas_accessions.tokenize(',') ) ch_eatlas_datasets = Channel.empty() + ch_accessions = Channel.fromList( params.eatlas_accessions.tokenize(',') ) + // fetching Expression Atlas accessions if applicable if ( !params.skip_fetch_eatlas_accessions || params.eatlas_keywords ) { // getting Expression Atlas accessions given a species name and keywords // keywords can be an empty string EXPRESSIONATLAS_GETACCESSIONS( - Channel.value( species ), + ch_species, params.eatlas_keywords ) @@ -32,30 +33,29 @@ workflow EXPRESSIONATLAS_FETCHDATA { // ensures that no accessions is present twice (provided by the user and fetched from E. Atlas) // removing E-PROT- accessions ch_accessions - .concat( EXPRESSIONATLAS_GETACCESSIONS.out.txt.splitText(expression_normalisation) ) + .concat( EXPRESSIONATLAS_GETACCESSIONS.out.txt.splitText() ) .unique() .map { it -> it.trim() } .filter { it.startsWith('E-') && !it.startsWith('E-PROT-') } - .set ( ch_accessions ) + .set { ch_accessions } } - if ( params.accessions_only ) { - log.info "Exporting Expression Atlas accessions and exiting." - System.exit(0) - } + if ( !params.accessions_only ) { + + // Downloading Expression Atlas data for each accession in ch_accessions + EXPRESSIONATLAS_GETDATA( ch_accessions ) - // Downloading Expression Atlas data for each accession in ch_accessions - EXPRESSIONATLAS_GETDATA( ch_accessions ) + // adding dataset id (accession + data_type) in the file meta + ch_etlas_design = addDatasetIdToMetadata( EXPRESSIONATLAS_GETDATA.out.design.flatten() ) + ch_eatlas_counts = addDatasetIdToMetadata( EXPRESSIONATLAS_GETDATA.out.counts.flatten() ) - // adding dataset id (accession + data_type) in the file meta - ch_etlas_design = addDatasetIdToMetadata( EXPRESSIONATLAS_GETDATA.out.design.flatten() ) - ch_eatlas_counts = addDatasetIdToMetadata( EXPRESSIONATLAS_GETDATA.out.counts.flatten() ) + // adding design files to the meta of their respective count files + ch_eatlas_datasets = groupFilesByDatasetId( ch_etlas_design, ch_eatlas_counts ) - // adding design files to the meta of their respective count files - ch_eatlas_datasets = groupFilesByDatasetId( ch_etlas_design, ch_eatlas_counts ) + // adding normalisation state in the meta + augmentToMetadata( ch_eatlas_datasets ) - // adding normalisation state in the meta - augmentToMetadata( ch_eatlas_datasets ) + } emit: downloaded_datasets = ch_eatlas_datasets diff --git a/subworkflows/local/idmapping/main.nf b/subworkflows/local/idmapping/main.nf index 06f072ee..a7f1520e 100644 --- a/subworkflows/local/idmapping/main.nf +++ b/subworkflows/local/idmapping/main.nf @@ -10,22 +10,19 @@ workflow IDMAPPING { take: ch_datasets - species + ch_species main: - def ch_gene_metadata = params.gene_metadata ? Channel.fromPath( params.gene_metadata, checkIfExists: true ) : Channel.empty() + ch_gene_metadata = params.gene_metadata ? Channel.fromPath( params.gene_metadata, checkIfExists: true ) : Channel.empty() + ch_gene_id_mapping = params.gene_id_mapping_file ? Channel.fromPath( params.gene_id_mapping, checkIfExists: true ) : Channel.empty() - if ( params.skip_gprofiler ) { - - def ch_gene_id_mapping = params.gene_id_mapping_file ? Channel.fromPath( params.gene_id_mapping, checkIfExists: true ) : Channel.empty() - - } else { + if ( !params.skip_gprofiler ) { // tries to map gene IDs to Ensembl IDs whenever possible IDMAPPING_GPROFILER( ch_datasets, - species, + ch_species, params.gene_id_mapping_file ? Channel.fromPath( params.gene_id_mapping_file, checkIfExists: true ) : 'none' ) diff --git a/subworkflows/local/multiqc/main.nf b/subworkflows/local/multiqc/main.nf index 45b3fd0e..7a5d0b9e 100644 --- a/subworkflows/local/multiqc/main.nf +++ b/subworkflows/local/multiqc/main.nf @@ -1,5 +1,10 @@ include { MULTIQC } from '../../../modules/nf-core/multiqc' +include { customSoftwareVersionsToYAML } from '../utils_nfcore_stableexpression_pipeline' +include { methodsDescriptionText } from '../utils_nfcore_stableexpression_pipeline' +include { paramsSummaryMultiqc } from '../../nf-core/utils_nfcore_pipeline' +include { paramsSummaryMap } from 'plugin/nf-schema' + /* ======================================================================================== SUBWORKFLOW TO DOWNLOAD EXPRESSIONATLAS ACCESSIONS AND DATASETS diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index d5fb301a..177e9632 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -9,18 +9,9 @@ include { IDMAPPING } from '../subworkflows/local/i include { EXPRESSION_NORMALISATION } from '../subworkflows/local/expression_normalisation' include { MULTIQC_WORKFLOW } from '../subworkflows/local/multiqc' - include { MERGE_DATA } from '../modules/local/merge_data' include { GENE_STATISTICS } from '../modules/local/gene_statistics' - -include { parseInputDatasets } from '../subworkflows/local/utils_nfcore_stableexpression_pipeline' -include { customSoftwareVersionsToYAML } from '../subworkflows/local/utils_nfcore_stableexpression_pipeline' -include { validateInputParameters } from '../subworkflows/local/utils_nfcore_stableexpression_pipeline' -include { methodsDescriptionText } from '../subworkflows/local/utils_nfcore_stableexpression_pipeline' -include { paramsSummaryMultiqc } from '../subworkflows/nf-core/utils_nfcore_pipeline' -include { paramsSummaryMap } from 'plugin/nf-schema' - /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ RUN MAIN WORKFLOW @@ -35,80 +26,90 @@ workflow STABLEEXPRESSION { main: - ch_multiqc_files = Channel.empty() + multiqc_report = Channel.empty() - species = params.species.split(' ').join('_') + ch_species = Channel.value( params.species.split(' ').join('_') ) // ----------------------------------------------------------------- - // FETCH EXPRESSION ATLAS DATASETS IF NEEDED + // FETCH AND DOWNLOAD EXPRESSION ATLAS DATASETS IF NEEDED // ----------------------------------------------------------------- - EXPRESSIONATLAS_FETCHDATA( species ) + EXPRESSIONATLAS_FETCHDATA( ch_species ) - // putting all datasets together (local datasets + Expression Atlas datasets) - ch_input_datasets - .concat( EXPRESSIONATLAS_FETCHDATA.out.downloaded_datasets ) - .set { ch_datasets } + if ( !params.accessions_only ) { - // ----------------------------------------------------------------- - // IDMAPPING - // ----------------------------------------------------------------- + // putting all datasets together (local datasets + Expression Atlas datasets) + ch_input_datasets + .concat( EXPRESSIONATLAS_FETCHDATA.out.downloaded_datasets ) + .set { ch_datasets } - IDMAPPING ( ch_datasets, species ) + // ----------------------------------------------------------------- + // IDMAPPING + // ----------------------------------------------------------------- - // ----------------------------------------------------------------- - // NORMALISATION OF RAW COUNT DATASETS (INCLUDING RNA-SEQ DATASETS) - // ----------------------------------------------------------------- + IDMAPPING ( ch_datasets, ch_species ) - EXPRESSION_NORMALISATION( - IDMAPPING.out.datasets, - params.normalisation_method - ) + // ----------------------------------------------------------------- + // NORMALISATION OF RAW COUNT DATASETS (INCLUDING RNA-SEQ DATASETS) + // ----------------------------------------------------------------- - EXPRESSION_NORMALISATION.out.normalised_counts.set { ch_normalised_counts } - EXPRESSION_NORMALISATION.out.dataset_statistics.set { ch_dataset_statistics } + EXPRESSION_NORMALISATION( + IDMAPPING.out.datasets, + params.normalisation_method + ) - // ----------------------------------------------------------------- - // MODULE: Merge count files and design files and filter out zero counts - // ----------------------------------------------------------------- + EXPRESSION_NORMALISATION.out.normalised_counts.set { ch_normalised_counts } + EXPRESSION_NORMALISATION.out.dataset_statistics.set { ch_dataset_statistics } - MERGE_DATA( - ch_normalised_counts.map { meta, file -> [file] }.collect(), - ch_normalised_counts.map { meta, file -> [meta.design] }.collect(), - ch_dataset_statistics.map { meta, file -> [file] }.collect(), - params.nb_top_gene_candidates - ) + // ----------------------------------------------------------------- + // MERGE COUNT FILES AND DESIGN FILES AND FILTER OUT ZERO COUNTS + // ----------------------------------------------------------------- - MERGE_DATA.out.candidate_gene_counts.set { ch_candidate_gene_counts } - MERGE_DATA.out.ks_test_statistics.set { ch_ks_stats } + MERGE_DATA( + ch_normalised_counts.map { meta, file -> [file] }.collect(), + ch_normalised_counts.map { meta, file -> [meta.design] }.collect(), + ch_dataset_statistics.map { meta, file -> [file] }.collect(), + params.nb_top_gene_candidates + ) - // ----------------------------------------------------------------- - // MODULE: Gene statistics - // ----------------------------------------------------------------- + MERGE_DATA.out.candidate_gene_counts.set { ch_candidate_gene_counts } + MERGE_DATA.out.ks_test_statistics.set { ch_ks_stats } + + // ----------------------------------------------------------------- + // GENE STATISTICS + // ----------------------------------------------------------------- + + GENE_STATISTICS( + MERGE_DATA.out.all_counts, + IDMAPPING.out.gene_metadata.collect(), + IDMAPPING.out.gene_id_mapping.collect(), + params.nb_top_gene_candidates, + ch_ks_stats, + params.ks_pvalue_threshold + ) + + // ----------------------------------------------------------------- + // MULTIQC + // ----------------------------------------------------------------- - GENE_STATISTICS( - MERGE_DATA.out.all_counts, - IDMAPPING.out.gene_metadata.collect(), - IDMAPPING.out.gene_id_mapping.collect(), - params.nb_top_gene_candidates, - ch_ks_stats, - params.ks_pvalue_threshold - ) + Channel.empty() + .mix( GENE_STATISTICS.out.top_stable_genes_summary.collect() ) + .mix( GENE_STATISTICS.out.all_statistics.collect() ) + .mix( GENE_STATISTICS.out.top_stable_genes_transposed_counts.collect() ) + .mix( MERGE_DATA.out.gene_count_statistics.collect() ) + .mix( MERGE_DATA.out.skewness_statistics.collect() ) + .mix( ch_ks_stats.collect() ) + .mix ( MERGE_DATA.out.distribution_correlations.collect() ) + .set { ch_multiqc_files } - ch_multiqc_files = ch_multiqc_files - .mix( GENE_STATISTICS.out.top_stable_genes_summary.collect() ) - .mix( GENE_STATISTICS.out.all_statistics.collect() ) - .mix( GENE_STATISTICS.out.top_stable_genes_transposed_counts.collect() ) - .mix( MERGE_DATA.out.gene_count_statistics.collect() ) - .mix( MERGE_DATA.out.skewness_statistics.collect() ) - .mix( ch_ks_stats.collect() ) - .mix ( MERGE_DATA.out.distribution_correlations.collect() ) + MULTIQC_WORKFLOW( ch_multiqc_files ) - MULTIQC_WORKFLOW( ch_multiqc_files ) + MULTIQC_WORKFLOW.out.report.toList().set { multiqc_report } + } emit: - multiqc_report = MULTIQC_WORKFLOW.out.report.toList() + multiqc_report } From f14c20f23dadbb31353b27fed1198e316debdbe0 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 18 Jun 2025 11:18:30 +0200 Subject: [PATCH 015/258] add arguments for including or excluding eatlas accessions --- nextflow.config | 3 ++ nextflow_schema.json | 31 +++++++++-- nf-test.config | 2 +- .../local/expressionatlas_fetchdata/main.nf | 27 ++++++++-- .../test_data/misc/accessions_to_include.txt | 2 + tests/test_data/misc/excluded_accessions.txt | 2 + tests/workflows/stableexpression.nf.test | 53 ++++++++++++++++++- 7 files changed, 111 insertions(+), 9 deletions(-) create mode 100644 tests/test_data/misc/accessions_to_include.txt create mode 100644 tests/test_data/misc/excluded_accessions.txt diff --git a/nextflow.config b/nextflow.config index 67f97379..e730fbe8 100644 --- a/nextflow.config +++ b/nextflow.config @@ -30,6 +30,9 @@ params { eatlas_keywords = "" eatlas_accessions = "" accessions_only = false + exclude_eatlas_accessions = "" + eatlas_accessions_file = null + exclude_eatlas_accessions_file = null // Boilerplate options outdir = null diff --git a/nextflow_schema.json b/nextflow_schema.json index 384a77e8..c2e78efb 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -33,7 +33,7 @@ "mimetype": "text/csv", "pattern": "^\\S+\\.csv$", "description": "Path to comma-separated file containing information about the input count datasets and their related experimental design.", - "help_text": "The dataset file should be a comma-separated file with 3 columns, and a header row. See [usage docs](https://nf-co.re/stableexpression/usage#samplesheet-input) for more information. Before running the pipeline, you will need to create a design file with information about the samples in your experiment. Use this parameter to specify its location.", + "help_text": "The dataset file should be a comma-separated file with 3 columns, and a header row. See [usage docs](https://nf-co.re/stableexpression/usage#samplesheet-input) for more information. Before running the pipeline, you will need to create a design file with information about the samples in your experiment. Use this parameter to specify its location. Combine with --skip_fetch_eatlas_accessions if you only want to analyse local datasets.", "fa_icon": "fas fa-file-csv" }, "email": { @@ -67,20 +67,43 @@ "description": "Keywords (separated by commas) to use when retrieving specific experiments from Expression Atlas.", "fa_icon": "fas fa-highlighter", "pattern": "([a-zA-Z,]+)", - "help_text": "The pipeline will select all Expression Atlas experiments that contain the provided keywords in their description of in one of the condition names. Example: `--eatlas_keywords 'stress,flowering'`" + "help_text": "The pipeline will select all Expression Atlas experiments that contain the provided keywords in their description of in one of the condition names. Example: `--eatlas_keywords 'stress,flowering'`. This parameter is unused if --skip_fetch_eatlas_accessions is set." }, "eatlas_accessions": { "type": "string", "pattern": "([A-Z0-9-]+,?)+", - "description": "Provide directly Expression Atlas accession(s) (separated by commas) that you want to download.", + "description": "Provide directly in command line Expression Atlas accession(s) (separated by commas) that you want to download.", "fa_icon": "fas fa-id-card", - "help_text": "Example: `--eatlas_accessions 'E-MTAB-552,E-GEOD-61690'`" + "help_text": "Example: `--eatlas_accessions E-MTAB-552,E-GEOD-61690`. Combine with --skip_fetch_eatlas_accessions if you want only these accessions to be used." + }, + "eatlas_accessions_file": { + "type": "string", + "format": "file-path", + "exists": true, + "description": "File containing Expression Atlas accession(s) that you want to download. One accession per line.", + "fa_icon": "fas fa-id-card", + "help_text": "Example: `--eatlas_accessions_file included_accessions.txt`. Combine with --skip_fetch_eatlas_accessions if you want only these accessions to be used." }, "accessions_only": { "type": "boolean", "description": "Only get accessions from Expression Atlas and exit.", "fa_icon": "fas fa-id-card", "help_text": "Use this option if you want to only get Expression Atlas accessions and skip the rest of the pipeline." + }, + "exclude_eatlas_accessions": { + "type": "string", + "pattern": "([A-Z0-9-]+,?)+", + "description": "Provide directly in command line Expression Atlas accessions (separated by commas) that you want to exclude.", + "fa_icon": "fas fa-id-card", + "help_text": "Example: `--exclude_eatlas_accessions E-MTAB-552,E-GEOD-61690`" + }, + "exclude_eatlas_accessions_file": { + "type": "string", + "format": "file-path", + "exists": true, + "description": "File containing Expression Atlas accession(s) that you want to exclude. One accession per line.", + "fa_icon": "fas fa-id-card", + "help_text": "Example: `--exclude_eatlas_accessions_file excluded_accessions.txt`." } } }, diff --git a/nf-test.config b/nf-test.config index 56ad9f69..65fb719c 100644 --- a/nf-test.config +++ b/nf-test.config @@ -3,7 +3,7 @@ config { testsDir "tests" workDir ".nf-test" configFile "tests/nextflow.config" - profile "docker" + profile "apptainer" requires ( "nf-test": "0.9.2" ) diff --git a/subworkflows/local/expressionatlas_fetchdata/main.nf b/subworkflows/local/expressionatlas_fetchdata/main.nf index 3c0b405f..943b910d 100644 --- a/subworkflows/local/expressionatlas_fetchdata/main.nf +++ b/subworkflows/local/expressionatlas_fetchdata/main.nf @@ -17,7 +17,13 @@ workflow EXPRESSIONATLAS_FETCHDATA { ch_eatlas_datasets = Channel.empty() - ch_accessions = Channel.fromList( params.eatlas_accessions.tokenize(',') ) + ch_eatlas_accessions_file = params.eatlas_accessions_file ? Channel.fromPath(params.eatlas_accessions_file, checkIfExists: true) : Channel.empty() + + Channel.fromList( params.eatlas_accessions.tokenize(',') ) + .mix( ch_eatlas_accessions_file.splitText() ) + .unique() + .map { it -> it.trim() } + .set { ch_input_accessions } // fetching Expression Atlas accessions if applicable if ( !params.skip_fetch_eatlas_accessions || params.eatlas_keywords ) { @@ -29,14 +35,29 @@ workflow EXPRESSIONATLAS_FETCHDATA { params.eatlas_keywords ) + ch_exclude_eatlas_accessions_file = params.exclude_eatlas_accessions_file ? Channel.fromPath(params.exclude_eatlas_accessions_file, checkIfExists: true) : Channel.empty() + + // getting accessions to exclude and preparing in the right format + Channel.fromList( params.exclude_eatlas_accessions.tokenize(',') ) + .mix( ch_exclude_eatlas_accessions_file.splitText() ) + .unique() + .map { it -> it.trim() } + .toList() + .map { lst -> [lst] } // list of lists : mandatory when combining in the next step + .set { ch_excluded_accessions } + // appending to accessions provided by the user // ensures that no accessions is present twice (provided by the user and fetched from E. Atlas) // removing E-PROT- accessions - ch_accessions - .concat( EXPRESSIONATLAS_GETACCESSIONS.out.txt.splitText() ) + // removing excluded accessions + ch_input_accessions + .mix( EXPRESSIONATLAS_GETACCESSIONS.out.txt.splitText() ) .unique() .map { it -> it.trim() } .filter { it.startsWith('E-') && !it.startsWith('E-PROT-') } + .combine ( ch_excluded_accessions ) + .filter { accession, excluded_accessions -> !(accession in excluded_accessions) } + .map { accession, excluded_accessions -> accession } .set { ch_accessions } } diff --git a/tests/test_data/misc/accessions_to_include.txt b/tests/test_data/misc/accessions_to_include.txt new file mode 100644 index 00000000..7020d409 --- /dev/null +++ b/tests/test_data/misc/accessions_to_include.txt @@ -0,0 +1,2 @@ +E-MTAB-4252 +E-MTAB-4253 diff --git a/tests/test_data/misc/excluded_accessions.txt b/tests/test_data/misc/excluded_accessions.txt new file mode 100644 index 00000000..6c403a93 --- /dev/null +++ b/tests/test_data/misc/excluded_accessions.txt @@ -0,0 +1,2 @@ +E-MTAB-4251 +E-MTAB-4301 diff --git a/tests/workflows/stableexpression.nf.test b/tests/workflows/stableexpression.nf.test index 63bf3b26..4298fac2 100644 --- a/tests/workflows/stableexpression.nf.test +++ b/tests/workflows/stableexpression.nf.test @@ -162,7 +162,7 @@ nextflow_workflow { then { assert workflow.success with(workflow.out.multiqc_report[0]) { - assertAll( + assertAll(eatlas_accessions = "E-MTAB-552,E-GEOD-61690" { assert path(get(0)).readLines().any { it.contains('MultiQC: A modular tool') } }, { assert path(get(0)).readLines().any { it.contains('Data was processed using nf-core/stableexpression') } } ) @@ -171,4 +171,55 @@ nextflow_workflow { } + test("Accessions only") { + + tag "included_excluded_accessions" + + when { + params { + species = "solanum tuberosum" + accessions_only = true + } + workflow { + """ + input[0] = Channel.empty() + """ + } + } + + then { + assert workflow.success + } + } + + test("Included and excluded accessions") { + + tag "included_excluded_accessions" + + when { + params { + species = "solanum tuberosum" + eatlas_accessions = "E-MTAB-552,E-GEOD-61690" + exclude_eatlas_accessions = "E-MTAB-4251" + eatlas_accessions_file = file( '$projectDir/tests/test_data/misc/accessions_to_include.txt.txt', checkIfExists: true) + exclude_eatlas_accessions_file = file( '$projectDir/tests/test_data/misc/excluded_accessions.txt', checkIfExists: true) + } + workflow { + """ + input[0] = Channel.empty() + """ + } + } + + then { + assert workflow.success + with(workflow.out.multiqc_report[0]) { + assertAll( + { assert path(get(0)).readLines().any { it.contains('MultiQC: A modular tool') } }, + { assert path(get(0)).readLines().any { it.contains('Data was processed using nf-core/stableexpression') } } + ) + } + } + } + } From ebb33f4e34a3f6e2a5422a15c359a82cd50c0e23 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 18 Jun 2025 11:58:18 +0200 Subject: [PATCH 016/258] reorganize published files --- conf/modules.config | 39 +++++++++---------- conf/test.config | 2 +- conf/test_dataset.config | 2 +- conf/test_dataset_custom_mapping.config | 2 +- conf/test_full.config | 2 +- conf/test_local_and_downloaded.config | 2 +- conf/test_one_accession.config | 2 +- conf/test_one_accession_low_gene_count.config | 2 +- 8 files changed, 26 insertions(+), 27 deletions(-) diff --git a/conf/modules.config b/conf/modules.config index 58429f87..58122212 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -17,53 +17,52 @@ process { mode: params.publish_dir_mode ] - withName: FLYE { - ext.args = { - [ - meta.genome_size ? "--genome-size ${meta.genome_size}" : '', - params.flye_args - ].join(" ").trim() - } + withName: 'EXPRESSIONATLAS_GETACCESSIONS' { publishDir = [ - path: { "${params.outdir}/${meta.id}/assembly/flye/" }, - mode: params.publish_dir_mode, - saveAs: { filename -> filename.equals('versions.yml') ? null : filename } + path: { "${params.outdir}/expression_atlas/accessions/" } + ] + } + + withName: 'EXPRESSIONATLAS_GETDATA' { + publishDir = [ + path: { "${params.outdir}/expression_atlas/datasets/" } + ] + } + + withName: 'IDMAPPING_GPROFILER' { + publishDir = [ + path: { "${params.outdir}/idmapping/datasets/" } ] } withName: 'NORMALISATION_DESEQ2' { publishDir = [ - path: { "${params.outdir}/${meta.dataset}/normalised/deseq2/" }, - mode: params.publish_dir_mode + path: { "${params.outdir}/normalised/${meta.dataset}/deseq2/" } ] } withName: 'NORMALISATION_EDGER' { publishDir = [ - path: { "${params.outdir}/${meta.dataset}/normalised/edger/" }, - mode: params.publish_dir_mode + path: { "${params.outdir}/normalised/${meta.dataset}/edger/" } ] } withName: 'QUANTILE_NORMALISATION' { publishDir = [ - path: { "${params.outdir}/${meta.dataset}/quantile_normalised/" }, - mode: params.publish_dir_mode + path: { "${params.outdir}/quantile_normalised/${meta.dataset}/" } ] } withName: 'MERGE_DATA' { publishDir = [ - path: { "${params.outdir}/merged_datasets/" }, - mode: params.publish_dir_mode + path: { "${params.outdir}/merged_datasets/" } ] } withName: 'MULTIQC' { ext.args = { params.multiqc_title ? "--title \"$params.multiqc_title\"" : '' } publishDir = [ - path: { "${params.outdir}/multiqc" }, - mode: params.publish_dir_mode + path: { "${params.outdir}/multiqc" } ] } diff --git a/conf/test.config b/conf/test.config index e1092e88..af59596e 100644 --- a/conf/test.config +++ b/conf/test.config @@ -27,5 +27,5 @@ params { species = 'solanum tuberosum' eatlas_keywords = "potato,stress" eatlas_accessions = "E-MTAB-552" - outdir = "results" + outdir = "results/test" } diff --git a/conf/test_dataset.config b/conf/test_dataset.config index 5a338c0b..8fe4d0c7 100644 --- a/conf/test_dataset.config +++ b/conf/test_dataset.config @@ -18,5 +18,5 @@ params { // Input data species = 'solanum tuberosum' datasets = "tests/test_data/input_datasets/input.csv" - outdir = "results" + outdir = "results/test_dataset" } diff --git a/conf/test_dataset_custom_mapping.config b/conf/test_dataset_custom_mapping.config index 3ec42a50..aa4fefa2 100644 --- a/conf/test_dataset_custom_mapping.config +++ b/conf/test_dataset_custom_mapping.config @@ -21,5 +21,5 @@ params { skip_gprofiler = true gene_id_mapping = "tests/test_data/input_datasets/mapping.csv" gene_metadata = "tests/test_data/input_datasets/metadata.csv" - outdir = "results" + outdir = "results/test_dataset_custom_mapping" } diff --git a/conf/test_full.config b/conf/test_full.config index 718e4eb1..647d081b 100644 --- a/conf/test_full.config +++ b/conf/test_full.config @@ -18,5 +18,5 @@ params { // Input data species = 'arabidopsis thaliana' fetch_eatlas_accessions = true - outdir = "results" + outdir = "results/test_full" } diff --git a/conf/test_local_and_downloaded.config b/conf/test_local_and_downloaded.config index 7e37b6fc..f90796ec 100644 --- a/conf/test_local_and_downloaded.config +++ b/conf/test_local_and_downloaded.config @@ -28,5 +28,5 @@ params { eatlas_keywords = "potato,stress" eatlas_accessions = "E-MTAB-552" datasets = "tests/test_data/input_datasets/input.csv" - outdir = "results" + outdir = "results/test_local_and_downloaded" } diff --git a/conf/test_one_accession.config b/conf/test_one_accession.config index ba2977e3..bb6e2ec5 100644 --- a/conf/test_one_accession.config +++ b/conf/test_one_accession.config @@ -26,5 +26,5 @@ params { // Input data species = 'solanum tuberosum' eatlas_accessions = "E-MTAB-552" - outdir = "results" + outdir = "results/test_one_accession" } diff --git a/conf/test_one_accession_low_gene_count.config b/conf/test_one_accession_low_gene_count.config index eaf773b6..fa36442f 100644 --- a/conf/test_one_accession_low_gene_count.config +++ b/conf/test_one_accession_low_gene_count.config @@ -26,5 +26,5 @@ params { // Input data species = 'arabidopsis thaliana' eatlas_accessions = "E-GEOD-51720" - outdir = "results" + outdir = "results/test_one_accession_low_gene_count" } From d272af591612dd5b9c250f016a6ff45c2bf99ddd Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Thu, 19 Jun 2025 11:16:56 +0200 Subject: [PATCH 017/258] improved handling of case when no gene mapping file is given (most cases); fix idmapping tests --- modules/local/idmapping/gprofiler/main.nf | 2 + subworkflows/local/idmapping/main.nf | 2 +- .../local/idmapping/gprofiler/main.nf.test | 73 ++++++++++++------- .../idmapping/gprofiler/main.nf.test.snap | 70 +++++++++--------- 4 files changed, 86 insertions(+), 61 deletions(-) diff --git a/modules/local/idmapping/gprofiler/main.nf b/modules/local/idmapping/gprofiler/main.nf index dfcca8e6..4651d5ed 100644 --- a/modules/local/idmapping/gprofiler/main.nf +++ b/modules/local/idmapping/gprofiler/main.nf @@ -46,6 +46,8 @@ process IDMAPPING_GPROFILER { script: def custom_mapping_arg = gene_id_mapping_file ? "--custom-mappings $gene_id_mapping_file" : "" + println gene_id_mapping_file + println custom_mapping_arg """ map_ids_to_ensembl.py \ --count-file "$count_file" \ diff --git a/subworkflows/local/idmapping/main.nf b/subworkflows/local/idmapping/main.nf index a7f1520e..ba1e3bec 100644 --- a/subworkflows/local/idmapping/main.nf +++ b/subworkflows/local/idmapping/main.nf @@ -23,7 +23,7 @@ workflow IDMAPPING { IDMAPPING_GPROFILER( ch_datasets, ch_species, - params.gene_id_mapping_file ? Channel.fromPath( params.gene_id_mapping_file, checkIfExists: true ) : 'none' + params.gene_id_mapping_file ? Channel.fromPath( params.gene_id_mapping_file, checkIfExists: true ) : Channel.value( [] ) ) IDMAPPING_GPROFILER.out.renamed.set { ch_datasets } diff --git a/tests/modules/local/idmapping/gprofiler/main.nf.test b/tests/modules/local/idmapping/gprofiler/main.nf.test index 10b9cdc4..c8dd8561 100644 --- a/tests/modules/local/idmapping/gprofiler/main.nf.test +++ b/tests/modules/local/idmapping/gprofiler/main.nf.test @@ -11,10 +11,14 @@ nextflow_process { when { process { """ - meta = [] - count_file = file("$projectDir/tests/test_data/idmapping/base/counts.ensembl_ids.csv", checkIfExists: true) - input[0] = [meta, count_file, "Solanum tuberosum"] - input[1] = '' + input[0] = Channel.of( + [ + [ dataset: "test" ], + file("$projectDir/tests/test_data/idmapping/base/counts.ensembl_ids.csv", checkIfExists: true) + ] + ) + input[1] = "Solanum tuberosum" + input[2] = Channel.value([]) """ } } @@ -33,10 +37,14 @@ nextflow_process { when { process { """ - meta = [] - count_file = file("$projectDir/tests/test_data/idmapping/base/counts.ncbi_ids.csv", checkIfExists: true) - input[0] = [meta, count_file, "Arabidopsis thaliana"] - input[1] = '' + input[0] = Channel.of( + [ + [ dataset: "test" ], + file("$projectDir/tests/test_data/idmapping/base/counts.ncbi_ids.csv", checkIfExists: true) + ] + ) + input[1] = "Arabidopsis thaliana" + input[2] = Channel.value([]) """ } } @@ -56,10 +64,14 @@ nextflow_process { when { process { """ - meta = [] - count_file = file("$projectDir/tests/test_data/idmapping/base/counts.uniprot_ids.csv", checkIfExists: true) - input[0] = [meta, count_file, "Arabidopsis thaliana"] - input[1] = '' + input[0] = Channel.of( + [ + [ dataset: "test" ], + file("$projectDir/tests/test_data/idmapping/base/counts.uniprot_ids.csv", checkIfExists: true) + ] + ) + input[1] = "Arabidopsis thaliana" + input[2] = Channel.value([]) """ } } @@ -80,10 +92,14 @@ nextflow_process { when { process { """ - meta = [] - count_file = file("$projectDir/tests/test_data/idmapping/empty/counts.csv", checkIfExists: true) - input[0] = [meta, count_file, "Arabidopsis thaliana"] - input[1] = '' + input[0] = Channel.of( + [ + [ dataset: "test" ], + file("$projectDir/tests/test_data/idmapping/empty/counts.csv", checkIfExists: true) + ] + ) + input[1] = "Arabidopsis thaliana" + input[2] = Channel.value([]) """ } } @@ -109,10 +125,14 @@ nextflow_process { when { process { """ - meta = [] - count_file = file("$projectDir/tests/test_data/idmapping/not_found/counts.csv", checkIfExists: true) - input[0] = [meta, count_file, "Homo sapiens"] - input[1] = '' + input[0] = Channel.of( + [ + [ dataset: "test" ], + file("$projectDir/tests/test_data/idmapping/not_found/counts.csv", checkIfExists: true) + ] + ) + input[1] = "Homo sapiens" + input[2] = Channel.value([]) """ } } @@ -138,11 +158,14 @@ nextflow_process { when { process { """ - meta = [] - count_file = file("$projectDir/tests/test_data/idmapping/base/counts.ensembl_ids.csv", checkIfExists: true) - custom_mapping_file = file("$projectDir/tests/test_data/idmapping/custom/mapping.csv", checkIfExists: true) - input[0] = [meta, count_file, "Solanum tuberosum"] - input[1] = custom_mapping_file + input[0] = Channel.of( + [ + [ dataset: "test" ], + file("$projectDir/tests/test_data/idmapping/base/counts.ensembl_ids.csv", checkIfExists: true) + ] + ) + input[1] = "Solanum tuberosum" + input[2] = Channel.fromPath( "$projectDir/tests/test_data/idmapping/custom/mapping.csv", checkIfExists: true) """ } } diff --git a/tests/modules/local/idmapping/gprofiler/main.nf.test.snap b/tests/modules/local/idmapping/gprofiler/main.nf.test.snap index 78eb984e..83dbb3e5 100644 --- a/tests/modules/local/idmapping/gprofiler/main.nf.test.snap +++ b/tests/modules/local/idmapping/gprofiler/main.nf.test.snap @@ -4,14 +4,14 @@ { "0": [ [ - [ - - ], + { + "dataset": "test" + }, "counts.ensembl_ids.renamed.csv:md5,b2f45fd17fcb2688ea08b94527f70c1f" ] ], "1": [ - + ], "2": [ "counts.ensembl_ids.mapping.csv:md5,6ff8d8f71b9df7a1b08ff0bfda8da755" @@ -38,13 +38,13 @@ ] ], "metadata": [ - + ], "renamed": [ [ - [ - - ], + { + "dataset": "test" + }, "counts.ensembl_ids.renamed.csv:md5,b2f45fd17fcb2688ea08b94527f70c1f" ] ] @@ -52,18 +52,18 @@ ], "meta": { "nf-test": "0.9.2", - "nextflow": "25.04.0" + "nextflow": "25.04.4" }, - "timestamp": "2025-05-14T11:36:14.264437894" + "timestamp": "2025-06-19T11:14:19.745663367" }, "Map Ensembl IDs to themselves": { "content": [ { "0": [ [ - [ - - ], + { + "dataset": "test" + }, "counts.ensembl_ids.renamed.csv:md5,ef96059e3283d4305b2c004d649ae648" ] ], @@ -99,9 +99,9 @@ ], "renamed": [ [ - [ - - ], + { + "dataset": "test" + }, "counts.ensembl_ids.renamed.csv:md5,ef96059e3283d4305b2c004d649ae648" ] ] @@ -109,18 +109,18 @@ ], "meta": { "nf-test": "0.9.2", - "nextflow": "25.04.0" + "nextflow": "25.04.4" }, - "timestamp": "2025-05-14T11:35:32.690454163" + "timestamp": "2025-06-19T11:13:33.025821232" }, "Map Uniprot IDs": { "content": [ { "0": [ [ - [ - - ], + { + "dataset": "test" + }, "counts.uniprot_ids.renamed.csv:md5,a93d7145f8b35f6074cdf21c7c97de7c" ] ], @@ -156,9 +156,9 @@ ], "renamed": [ [ - [ - - ], + { + "dataset": "test" + }, "counts.uniprot_ids.renamed.csv:md5,a93d7145f8b35f6074cdf21c7c97de7c" ] ] @@ -166,18 +166,18 @@ ], "meta": { "nf-test": "0.9.2", - "nextflow": "25.04.0" + "nextflow": "25.04.4" }, - "timestamp": "2025-05-14T11:35:50.772485621" + "timestamp": "2025-06-19T11:13:53.45455109" }, "Map NCBI IDs": { "content": [ { "0": [ [ - [ - - ], + { + "dataset": "test" + }, "counts.ncbi_ids.renamed.csv:md5,a93d7145f8b35f6074cdf21c7c97de7c" ] ], @@ -213,9 +213,9 @@ ], "renamed": [ [ - [ - - ], + { + "dataset": "test" + }, "counts.ncbi_ids.renamed.csv:md5,a93d7145f8b35f6074cdf21c7c97de7c" ] ] @@ -223,8 +223,8 @@ ], "meta": { "nf-test": "0.9.2", - "nextflow": "25.04.0" + "nextflow": "25.04.4" }, - "timestamp": "2025-05-14T11:35:41.055648628" + "timestamp": "2025-06-19T11:13:42.529805072" } -} +} \ No newline at end of file From 2a5c89bdca572d20ee9c6774cb7d12353d9efe2b Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 25 Jun 2025 10:05:03 +0200 Subject: [PATCH 018/258] add when statements in modules --- modules/local/dataset_statistics/main.nf | 2 ++ modules/local/expressionatlas/getaccessions/main.nf | 2 ++ modules/local/expressionatlas/getdata/main.nf | 2 ++ modules/local/gene_statistics/main.nf | 2 ++ modules/local/idmapping/gprofiler/main.nf | 2 ++ modules/local/merge_data/main.nf | 2 ++ modules/local/normalisation/deseq2/main.nf | 2 ++ modules/local/normalisation/edger/main.nf | 2 ++ modules/local/quantile_normalisation/main.nf | 3 ++- 9 files changed, 18 insertions(+), 1 deletion(-) diff --git a/modules/local/dataset_statistics/main.nf b/modules/local/dataset_statistics/main.nf index 77fbccc9..e8df03f5 100644 --- a/modules/local/dataset_statistics/main.nf +++ b/modules/local/dataset_statistics/main.nf @@ -19,6 +19,8 @@ process DATASET_STATISTICS { tuple val("${task.process}"), val('scipy'), eval('python3 -c "import scipy; print(scipy.__version__)"'), topic: versions tuple val("${task.process}"), val('pyarrow'), eval('python3 -c "import pyarrow; print(pyarrow.__version__)"'), topic: versions + when: + task.ext.when == null || task.ext.when script: def prefix = task.ext.prefix ?: "${meta.dataset}" diff --git a/modules/local/expressionatlas/getaccessions/main.nf b/modules/local/expressionatlas/getaccessions/main.nf index 48b44fcb..a44f769e 100644 --- a/modules/local/expressionatlas/getaccessions/main.nf +++ b/modules/local/expressionatlas/getaccessions/main.nf @@ -18,6 +18,8 @@ process EXPRESSIONATLAS_GETACCESSIONS { tuple val("${task.process}"), val('requests'), eval('python3 -c "import requests; print(requests.__version__)"'), topic: versions tuple val("${task.process}"), val('nltk'), eval('python3 -c "import nltk; print(nltk.__version__)"'), topic: versions + when: + task.ext.when == null || task.ext.when script: def keywords_string = keywords.split(',').collect { it.trim() }.join(' ') diff --git a/modules/local/expressionatlas/getdata/main.nf b/modules/local/expressionatlas/getdata/main.nf index 4da386a8..b0903563 100644 --- a/modules/local/expressionatlas/getdata/main.nf +++ b/modules/local/expressionatlas/getdata/main.nf @@ -49,6 +49,8 @@ process EXPRESSIONATLAS_GETDATA { tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //"'), topic: versions tuple val("${task.process}"), val('ExpressionAtlas'), eval('Rscript -e "cat(as.character(packageVersion(\'ExpressionAtlas\')))"'), topic: versions + when: + task.ext.when == null || task.ext.when script: """ diff --git a/modules/local/gene_statistics/main.nf b/modules/local/gene_statistics/main.nf index 1953c855..456c821d 100644 --- a/modules/local/gene_statistics/main.nf +++ b/modules/local/gene_statistics/main.nf @@ -33,6 +33,8 @@ process GENE_STATISTICS { tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions + when: + task.ext.when == null || task.ext.when script: """ diff --git a/modules/local/idmapping/gprofiler/main.nf b/modules/local/idmapping/gprofiler/main.nf index 4651d5ed..6fdbe5e1 100644 --- a/modules/local/idmapping/gprofiler/main.nf +++ b/modules/local/idmapping/gprofiler/main.nf @@ -43,6 +43,8 @@ process IDMAPPING_GPROFILER { tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions tuple val("${task.process}"), val('requests'), eval('python3 -c "import requests; print(requests.__version__)"'), topic: versions + when: + task.ext.when == null || task.ext.when script: def custom_mapping_arg = gene_id_mapping_file ? "--custom-mappings $gene_id_mapping_file" : "" diff --git a/modules/local/merge_data/main.nf b/modules/local/merge_data/main.nf index db9f01f0..53da754a 100644 --- a/modules/local/merge_data/main.nf +++ b/modules/local/merge_data/main.nf @@ -24,6 +24,8 @@ process MERGE_DATA { tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions + when: + task.ext.when == null || task.ext.when script: """ diff --git a/modules/local/normalisation/deseq2/main.nf b/modules/local/normalisation/deseq2/main.nf index 71a7786c..d48077bb 100644 --- a/modules/local/normalisation/deseq2/main.nf +++ b/modules/local/normalisation/deseq2/main.nf @@ -21,6 +21,8 @@ process NORMALISATION_DESEQ2 { tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //"'), topic: versions tuple val("${task.process}"), val('DESeq2'), eval('Rscript -e "cat(as.character(packageVersion(\'DESeq2\')))"'), topic: versions + when: + task.ext.when == null || task.ext.when script: def design_file = meta.design diff --git a/modules/local/normalisation/edger/main.nf b/modules/local/normalisation/edger/main.nf index 68092bc2..3717af06 100644 --- a/modules/local/normalisation/edger/main.nf +++ b/modules/local/normalisation/edger/main.nf @@ -21,6 +21,8 @@ process NORMALISATION_EDGER { tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //"'), topic: versions tuple val("${task.process}"), val('edgeR'), eval('Rscript -e "cat(as.character(packageVersion(\'edgeR\')))"'), topic: versions + when: + task.ext.when == null || task.ext.when script: def design_file = meta.design diff --git a/modules/local/quantile_normalisation/main.nf b/modules/local/quantile_normalisation/main.nf index 59b91ca0..1f8471a5 100644 --- a/modules/local/quantile_normalisation/main.nf +++ b/modules/local/quantile_normalisation/main.nf @@ -19,13 +19,14 @@ process QUANTILE_NORMALISATION { tuple val("${task.process}"), val('scikit-learn'), eval('python3 -c "import sklearn; print(sklearn.__version__)"'), topic: versions tuple val("${task.process}"), val('pyarrow'), eval('python3 -c "import pyarrow; print(pyarrow.__version__)"'), topic: versions + when: + task.ext.when == null || task.ext.when script: """ quantile_normalise.py --counts $count_file """ - stub: """ touch count.cpm.quant_norm.parquet From ffaee5c9f8256243639853f1f034fcc65165321b Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Thu, 26 Jun 2025 05:48:06 +0200 Subject: [PATCH 019/258] first galaxy setup --- .editorconfig | 4 + galaxy/.shed.yml | 8 ++ galaxy/README.md | 5 + galaxy/test/galaxy_server.sh | 59 +++++++++ galaxy/tools/build_tool.py | 43 +++++++ galaxy/tools/config_formatter.py | 72 +++++++++++ galaxy/tools/schema_formatter.py | 201 +++++++++++++++++++++++++++++++ galaxy/tools/static_tool.xml | 50 ++++++++ galaxy/tools/tool.xml | 139 +++++++++++++++++++++ galaxy/tools/tool_conf.xml | 7 ++ nextflow_schema.json | 54 +++++---- 11 files changed, 616 insertions(+), 26 deletions(-) create mode 100644 galaxy/.shed.yml create mode 100644 galaxy/README.md create mode 100755 galaxy/test/galaxy_server.sh create mode 100644 galaxy/tools/build_tool.py create mode 100644 galaxy/tools/config_formatter.py create mode 100644 galaxy/tools/schema_formatter.py create mode 100644 galaxy/tools/static_tool.xml create mode 100644 galaxy/tools/tool.xml create mode 100644 galaxy/tools/tool_conf.xml diff --git a/.editorconfig b/.editorconfig index 6d9b74cc..283ba7c2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -35,3 +35,7 @@ indent_style = unset # ignore ro-crate metadata files [**/ro-crate-metadata.json] insert_final_newline = unset + +# ignore galaxy files +[galaxy/tools/*.xml] +indent_style = unset diff --git a/galaxy/.shed.yml b/galaxy/.shed.yml new file mode 100644 index 00000000..699a1fa9 --- /dev/null +++ b/galaxy/.shed.yml @@ -0,0 +1,8 @@ +categories: + - Transcriptomics +description: my_scription +homepage_url: my_tool_homepage +long_description: my_long_description +name: nf_core_stableexpression +owner: olivier_coen +remote_repository_url: my_github_url diff --git a/galaxy/README.md b/galaxy/README.md new file mode 100644 index 00000000..45dd586c --- /dev/null +++ b/galaxy/README.md @@ -0,0 +1,5 @@ +# Galaxy + +```bash + +``` diff --git a/galaxy/test/galaxy_server.sh b/galaxy/test/galaxy_server.sh new file mode 100755 index 00000000..054bc95d --- /dev/null +++ b/galaxy/test/galaxy_server.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash + +DOCKER_IMAGE=quay.io/bgruening/galaxy +PORT=8080 +tool_dir="$(dirname $(dirname $(readlink -f "$0")))/tools" + +status() { + if [[ $(sudo lsof -i :$PORT) ]]; then + echo "Galaxy is running !" + else + echo "Galaxy is not running !" + fi +} + +start() { + + + # launching docker compose in detached mode + echo "Launching Galaxy in detached mode." + docker run \ + -d \ + -p 8080:80 \ + -p 8021:21 \ + -p 8022:22 \ + -v $tool_dir:/local_tools \ + -e GALAXY_CONFIG_TOOL_CONFIG_FILE=/etc/galaxy/tool_conf.xml,/local_tools/tool_conf.xml \ + $DOCKER_IMAGE + echo "Galaxy started !" +} + +stop() { + docker stop $(docker ps | grep galaxy | awk '{print $1}') + echo "Galaxy stopped !" +} + +case "$1" in + 'start') + start + ;; + 'stop') + stop + ;; + 'restart') + stop + start + ;; + 'status') + status + ;; + *) + echo + echo "Usage: $0 { start | stop | restart | status }" + echo + exit 1 + ;; +esac + +exit 0 + diff --git a/galaxy/tools/build_tool.py b/galaxy/tools/build_tool.py new file mode 100644 index 00000000..fe5abae6 --- /dev/null +++ b/galaxy/tools/build_tool.py @@ -0,0 +1,43 @@ +import logging + +from schema_formatter import SchemaFormatter +from config_formatter import ConfigFormatter + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +STATIC_TOOL_FILENAME = "static_tool.xml" +OUTPUT_TOOL_FILENAME = "tool.xml" + + +def main(): + cformatter = ConfigFormatter() + sformatter = SchemaFormatter() + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # REPLACING ACTUAL PARAMS IN STATIC TOOL + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + with open(STATIC_TOOL_FILENAME, "r") as fin: + static_string = fin.read() + + tool_string = ( + static_string.replace( + "NEXTFLOW_VERSION", cformatter.package_version["nextflow"] + ) + .replace("SINGULARITY_VERSION", cformatter.package_version["singularity"]) + .replace("PIPELINE_VERSION", cformatter.pipeline_version) + .replace("DESCRIPTION", sformatter.pipeline_description) + .replace("PARAMETERS", sformatter.params_cli) + .replace("INPUTS", sformatter.inputs) + .replace("USAGE_OPTIONS", sformatter.usage_options) + ) + + with open(OUTPUT_TOOL_FILENAME, "w") as fout: + fout.write(tool_string) + + logger.info("Done") + + +if __name__ == "__main__": + main() diff --git a/galaxy/tools/config_formatter.py b/galaxy/tools/config_formatter.py new file mode 100644 index 00000000..63bd31cb --- /dev/null +++ b/galaxy/tools/config_formatter.py @@ -0,0 +1,72 @@ +from pathlib import Path +import subprocess +import re +from dataclasses import dataclass, field +from typing import ClassVar + + +@dataclass +class BaseConfigFormatter: + CONFIG_FILE: ClassVar[Path] = Path(__file__).parents[2] / "nextflow.config" + MAIN_FILE: ClassVar[Path] = Path(__file__).parents[2] / "main.nf" + NXF_VERSION_COMMAND_TEMPLATE: ClassVar[str] = ( + "micromamba search --override-channels --channel bioconda 'pkg' | grep pkg | awk '{print $2}' | sort | tail -1" + ) + PACKAGES: ClassVar[list] = ["nextflow", "singularity"] + + pipeline_version: str = field(init=False) + package_version: dict = field(init=False, default_factory=dict) + + def __post_init__(self): + # CONDA PACKAGE VERSIONS + for package in self.PACKAGES: + self.package_version[package] = self.get_package_version(package) + + # PARSING CONFIG + with open(self.CONFIG_FILE, "r") as f: + pipeline_config = f.read() + + self.pipeline_version = self.get_pipeline_version(pipeline_config) + + @classmethod + def get_package_version(cls, package_name: str) -> str: + """ + Get latest conda version of package + """ + nxf_version_command = cls.NXF_VERSION_COMMAND_TEMPLATE.replace( + "pkg", package_name + ) + result = subprocess.run( + nxf_version_command, shell=True, capture_output=True, text=True, check=True + ) + + if result.stderr: + raise RuntimeError(f"Command error: {result.stderr}") + + return result.stdout.strip("\n") + + @staticmethod + def get_pipeline_version(pipeline_config: str): + # Regular expression to find the manifest block and extract the version + manifest_pattern = re.compile(r"manifest\s*{\s*(.*?)\s*}", re.DOTALL) + manifest_match = manifest_pattern.search(pipeline_config) + version = None + + if manifest_match: + manifest_content = manifest_match.group(1) + # Regular expression to find the version field + version_pattern = re.compile(r'version\s*=\s*[\'"](.*?)[\'"]') + version_match = version_pattern.search(manifest_content) + + if version_match: + version = version_match.group(1) + + if version is None: + raise ValueError("No version found in pipeline config") + + return version + + +@dataclass +class ConfigFormatter(BaseConfigFormatter): + pass diff --git a/galaxy/tools/schema_formatter.py b/galaxy/tools/schema_formatter.py new file mode 100644 index 00000000..ae1625a0 --- /dev/null +++ b/galaxy/tools/schema_formatter.py @@ -0,0 +1,201 @@ +from pathlib import Path +import json +from dataclasses import dataclass, field +from typing import ClassVar + + +@dataclass +class BaseSchemaFormatter: + SCHEMA_FILE: ClassVar[Path] = Path(__file__).parents[2] / "nextflow_schema.json" + PARAMS_TO_IGNORE: ClassVar[list] = ["outdir", "email", "multiqc_title"] + SECTIONS_TO_IGNORE: ClassVar[list] = [ + "institutional_config_options", + "generic_options", + ] + SECTIONS_TO_EXPAND: ClassVar[list] = ["input_output_options"] + NF_TYPES_TO_GALAXY: ClassVar[dict] = { + "string": "text", + "boolean": "boolean", + "integer": "integer", + "number": "float", + } + + pipeline_description: str = field(init=False) + inputs: str = field(init=False) + params_cli: str = field(init=False) + usage_options: str = field(init=False) + _pipeline_params: dict = field(init=False) + + _inputs: list = field(init=False, default_factory=list) + _params_cli: list = field(init=False, default_factory=list) + _usage_options: list = field(init=False, default_factory=list) + + def __post_init__(self): + self.parse_schema_file() + + def parse_schema_file(self): + with open(self.SCHEMA_FILE, "r") as f: + pipeline_schema = json.load(f) + + self.pipeline_description = pipeline_schema["description"].strip("\n") + self._pipeline_params = pipeline_schema["$defs"] + + # PARSING PARAMETERS AND BUILDING STRINGS + for section, section_dict in self._pipeline_params.items(): + if section in self.SECTIONS_TO_IGNORE: + continue + + section_inputs, section_params_cli, section_usage_options = ( + self.format_input_section(section, section_dict) + ) + self._inputs += section_inputs + self._params_cli += section_params_cli + self._usage_options += section_usage_options + + self.inputs = "\n".join(self._inputs) + self.params_cli = "\n".join(self._params_cli) + self.usage_options = "\n".join(self._usage_options) + + def format_input_param(self, param: str, param_dict: dict, optional: bool) -> str: + """ + building input param + """ + + input_param_str = '\t\t\t' + param_format = "" + param_label = "" + param_help = "" + param_true_false = "" + param_value = "" + param_min = "" + param_max = "" + param_optional = ' optional="true"' if optional else ' optional="false"' + + param_type = param_dict["type"] + default_value = param_dict.get("default") + + if param_type == "string" and param_dict.get("format") == "file-path": + input_type = "data" + if pattern := param_dict.get("pattern"): + # TODO: handle multiple extensions + extension = pattern.split(".")[-1].strip("$") + param_format = f' format="{extension}"' + + # if param is an optional file with multiple possible values, it requires special handling + # see https://docs.galaxyproject.org/en/latest/dev/schema.html#id51 + + else: + input_type = self.NF_TYPES_TO_GALAXY[param_type] + + if param_type == "boolean": + param_true_false = f' truevalue="--{param}" falsevalue=""' + + elif param_type in ["integer", "number"]: + if minimum := param_dict.get("minimum"): + param_min = f' min="{minimum}"' + if maximum := param_dict.get("maximum"): + param_max = f' max="{maximum}"' + + # handle parameter with enum (options) + if options := param_dict.get("enum"): + input_type = "select" + input_param_str = input_param_str.replace(" />", ">\n") + base_option = ( + '\t\t\t\n' + ) + + for option in options: + selected_arg = ' selected="true"' if option == default_value else "" + option_param = base_option.format( + option=option, + label=option.capitalize(), + selected_arg=selected_arg, + ) + input_param_str += "\t" + option_param + + input_param_str += "\t\t\t" + + else: + if default_value: + param_value = f' value="{default_value}"' + + if description := param_dict.get("description"): + param_label = f' label="{description}"' + if help_text := param_dict.get("help_text"): + param_help = f' help="{help_text}"' + + return input_param_str.format( + param=param, + type=input_type, + label=param_label, + format=param_format, + value=param_value, + min=param_min, + max=param_max, + true_false=param_true_false, + help=param_help, + optional=param_optional, + ) + + @staticmethod + def format_input_param_cli(param: str, section: str, optional: bool) -> str: + if optional: + return f"\t\t\t#if {section}.{param}\n\t\t\t --{param} {section}.{param}\n\t\t\t#end if" + else: + return f"--{param} {section}.{param}" + + @staticmethod + def format_input_param_usage(param: str, param_dict: dict, optional: bool) -> str: + required_param = "" if optional else "[REQUIRED] " + return f'\t\t--{param} <{param_dict["type"]}> {required_param}: {param_dict["description"]}' + + def format_input_section( + self, section: str, section_dict: dict + ) -> tuple[list, list, list]: + section_inputs = [] + section_params_cli = [] + section_usage_options = [] + + section_title = "" + section_help = "" + + if title := section_dict.get("title"): + section_title = f' title="{title}"' + if description := section_dict.get("description"): + section_help = f' help="{description}"' + + section_expanded = ( + ' expanded="true"' + if section in self.SECTIONS_TO_EXPAND + else ' expanded="false"' + ) + + section_inputs.append( + f'\t\t
    ' + ) + section_usage_options.append("\n\t" + section.capitalize().replace("_", " ")) + + required_params = section_dict.get("required", []) + + for param, param_dict in section_dict["properties"].items(): + if param not in self.PARAMS_TO_IGNORE: + optional = param not in required_params + # input arguments + input_param = self.format_input_param(param, param_dict, optional) + section_inputs.append(input_param) + # cli + param_cli = self.format_input_param_cli(param, section, optional) + section_params_cli.append(param_cli) + # usage (help) + input_param_usage = self.format_input_param_usage( + param, param_dict, optional + ) + section_usage_options.append(input_param_usage) + + section_inputs.append("\t\t
    ") + + return section_inputs, section_params_cli, section_usage_options + + +class SchemaFormatter(BaseSchemaFormatter): + pass diff --git a/galaxy/tools/static_tool.xml b/galaxy/tools/static_tool.xml new file mode 100644 index 00000000..72ae4fc8 --- /dev/null +++ b/galaxy/tools/static_tool.xml @@ -0,0 +1,50 @@ + + DESCRIPTION + + + nextflow + singularity + + + + +INPUTS + + + + + + + + + + + --species --outdir [options] + +Options: +USAGE_OPTIONS + + ]]> + + + @misc{githubseqtk, + author = {LastTODO, FirstTODO}, + year = {TODO}, + title = {seqtk}, + publisher = {GitHub}, + journal = {GitHub repository}, + url = {https://github.com/lh3/seqtk}, + } + + + diff --git a/galaxy/tools/tool.xml b/galaxy/tools/tool.xml new file mode 100644 index 00000000..a36691bc --- /dev/null +++ b/galaxy/tools/tool.xml @@ -0,0 +1,139 @@ + + Pipeline dedicated to finding the most stable genes across count datasets + + + nextflow + singularity + + + + +
    + + +
    +
    + + + + + + + +
    +
    + + + +
    +
    + + + + + + +
    +
    + + + + + + + + + + --species --outdir [options] + +Options: + + Input output options + --species [REQUIRED] : Species name + --datasets : User count datasets + + Expression atlas options + --skip_fetch_eatlas_accessions : Skip fetching Expression Atlas accessions + --eatlas_keywords : Expression Atlas keywords + --eatlas_accessions : Expression Atlas accessions + --eatlas_accessions_file : File with Expression Atlas accessions + --accessions_only : Only get accessions from Expression Atlas and exit. + --exclude_eatlas_accessions : Expression Atlas accession to exclude + --exclude_eatlas_accessions_file : File with Expression Atlas accessions to exclude + + Idmapping options + --skip_gprofiler : Skip g:Profiler ID mapping step + --gene_id_mapping_file : Custom gene id mapping file + --gene_metadata : Custom gene metadata file + + Statistical options + --normalisation_method : Tool to use for normalisation + --nb_top_gene_candidates : Number of candidate genes to keep + --ks_pvalue_threshold : Threshold for KS p-value for considering samples counts as a uniform distribution + + ]]> + + + @misc{githubseqtk, + author = {LastTODO, FirstTODO}, + year = {TODO}, + title = {seqtk}, + publisher = {GitHub}, + journal = {GitHub repository}, + url = {https://github.com/lh3/seqtk}, + } + + +
    diff --git a/galaxy/tools/tool_conf.xml b/galaxy/tools/tool_conf.xml new file mode 100644 index 00000000..c596e791 --- /dev/null +++ b/galaxy/tools/tool_conf.xml @@ -0,0 +1,7 @@ + + +
    + + +
    +
    diff --git a/nextflow_schema.json b/nextflow_schema.json index c2e78efb..71ce52f8 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://raw.githubusercontent.com/nf-core/stableexpression/master/nextflow_schema.json", "title": "nf-core/stableexpression pipeline parameters", - "description": "\nThis pipeline is dedicated to finding the most stable genes across count datasets\n", + "description": "\nPipeline dedicated to finding the most stable genes across count datasets\n", "type": "object", "$defs": { "input_output_options": { @@ -14,15 +14,16 @@ "properties": { "species": { "type": "string", - "description": "Species name.", + "description": "Species name", "fa_icon": "fas fa-hippo", "pattern": "([a-zA-Z]+)[_ ]([a-zA-Z]+)", - "help_text": "e.g. `--species 'Arabidopsis thaliana'` or `--species 'homo_sapiens'`" + "help_text": "Genus and species may be separated by ` ` or `_`. Example: `--species 'Arabidopsis thaliana'` or `--species 'homo_sapiens'`" }, "outdir": { "type": "string", "format": "directory-path", - "description": "The output directory where the results will be saved. You have to use absolute paths to storage on Cloud infrastructure.", + "description": "Output directory", + "help_text": "The output directory where the results will be saved. You have to use absolute paths to storage on Cloud infrastructure.", "fa_icon": "fas fa-folder-open" }, "datasets": { @@ -32,8 +33,8 @@ "schema": "assets/schema_datasets.json", "mimetype": "text/csv", "pattern": "^\\S+\\.csv$", - "description": "Path to comma-separated file containing information about the input count datasets and their related experimental design.", - "help_text": "The dataset file should be a comma-separated file with 3 columns, and a header row. See [usage docs](https://nf-co.re/stableexpression/usage#samplesheet-input) for more information. Before running the pipeline, you will need to create a design file with information about the samples in your experiment. Use this parameter to specify its location. Combine with --skip_fetch_eatlas_accessions if you only want to analyse local datasets.", + "description": "User count datasets", + "help_text": "Path to comma-separated file containing information about the input count datasets and their related experimental design. The dataset file should be a comma-separated file with 3 columns, and a header row. See [usage docs](https://nf-co.re/stableexpression/usage#samplesheet-input) for more information. Before running the pipeline, you will need to create a design file with information about the samples in your experiment. Use this parameter to specify its location. Combine with --skip_fetch_eatlas_accessions if you only want to analyse local datasets.", "fa_icon": "fas fa-file-csv" }, "email": { @@ -45,7 +46,8 @@ }, "multiqc_title": { "type": "string", - "description": "MultiQC report title. Printed as page header, used for filename if not otherwise specified.", + "description": "MultiQC report title", + "help_text": "Printed as page header, used for filename if not otherwise specified.", "fa_icon": "fas fa-file-signature" } } @@ -59,30 +61,30 @@ "skip_fetch_eatlas_accessions": { "type": "boolean", "fa_icon": "fas fa-cloud-arrow-down", - "description": "Skip fetching Expression Atlas accessions.", + "description": "Skip fetching Expression Atlas accessions", "help_text": "Expression Atlas accessions are automatically fetched by default. You can skip this step by setting this parameter." }, "eatlas_keywords": { "type": "string", - "description": "Keywords (separated by commas) to use when retrieving specific experiments from Expression Atlas.", + "description": "Expression Atlas keywords", "fa_icon": "fas fa-highlighter", "pattern": "([a-zA-Z,]+)", - "help_text": "The pipeline will select all Expression Atlas experiments that contain the provided keywords in their description of in one of the condition names. Example: `--eatlas_keywords 'stress,flowering'`. This parameter is unused if --skip_fetch_eatlas_accessions is set." + "help_text": "Keywords (separated by commas) to use when retrieving specific experiments from Expression Atlas. The pipeline will select all Expression Atlas experiments that contain the provided keywords in their description of in one of the condition names. Example: `--eatlas_keywords 'stress,flowering'`. This parameter is unused if --skip_fetch_eatlas_accessions is set." }, "eatlas_accessions": { "type": "string", "pattern": "([A-Z0-9-]+,?)+", - "description": "Provide directly in command line Expression Atlas accession(s) (separated by commas) that you want to download.", + "description": "Expression Atlas accessions", "fa_icon": "fas fa-id-card", - "help_text": "Example: `--eatlas_accessions E-MTAB-552,E-GEOD-61690`. Combine with --skip_fetch_eatlas_accessions if you want only these accessions to be used." + "help_text": "Provide directly in command line Expression Atlas accession(s) (separated by commas) that you want to download. Example: `--eatlas_accessions E-MTAB-552,E-GEOD-61690`. Combine with --skip_fetch_eatlas_accessions if you want only these accessions to be used." }, "eatlas_accessions_file": { "type": "string", "format": "file-path", "exists": true, - "description": "File containing Expression Atlas accession(s) that you want to download. One accession per line.", + "description": "File with Expression Atlas accessions", "fa_icon": "fas fa-id-card", - "help_text": "Example: `--eatlas_accessions_file included_accessions.txt`. Combine with --skip_fetch_eatlas_accessions if you want only these accessions to be used." + "help_text": "File containing Expression Atlas accession(s) that you want to download. One accession per line.Example: `--eatlas_accessions_file included_accessions.txt`. Combine with --skip_fetch_eatlas_accessions if you want only these accessions to be used." }, "accessions_only": { "type": "boolean", @@ -93,17 +95,17 @@ "exclude_eatlas_accessions": { "type": "string", "pattern": "([A-Z0-9-]+,?)+", - "description": "Provide directly in command line Expression Atlas accessions (separated by commas) that you want to exclude.", + "description": "Expression Atlas accession to exclude", "fa_icon": "fas fa-id-card", - "help_text": "Example: `--exclude_eatlas_accessions E-MTAB-552,E-GEOD-61690`" + "help_text": "Provide directly in command line Expression Atlas accessions (separated by commas) that you want to exclude. Example: `--exclude_eatlas_accessions E-MTAB-552,E-GEOD-61690`" }, "exclude_eatlas_accessions_file": { "type": "string", "format": "file-path", "exists": true, - "description": "File containing Expression Atlas accession(s) that you want to exclude. One accession per line.", + "description": "File with Expression Atlas accessions to exclude", "fa_icon": "fas fa-id-card", - "help_text": "Example: `--exclude_eatlas_accessions_file excluded_accessions.txt`." + "help_text": "File containing Expression Atlas accession(s) that you want to exclude. One accession per line. Example: `--exclude_eatlas_accessions_file excluded_accessions.txt`." } } }, @@ -115,7 +117,7 @@ "properties": { "skip_gprofiler": { "type": "boolean", - "description": "Skip g:Profiler ID mapping step.", + "description": "Skip g:Profiler ID mapping step", "fa_icon": "fas fa-ban", "help": "If you don't want to map gene IDs with g:Profiler, you can skip this step by providing `--skip_gprofiler`. It can be in particular useful if the g:Profiler is down and if you already have a custom mapping file." }, @@ -126,8 +128,8 @@ "schema": "assets/schema_gene_id_mapping.json", "mimetype": "text/csv", "pattern": "^\\S+\\.csv$", - "description": "Path to comma-separated file containing custom gene id mappings. Each row represents a mapping from the original gene ID in your count datasets to the ensembl ID in g:Profiler.", - "help_text": "The mapping file should be a comma-separated file with 2 columns (original_gene_id and ensembl_gene_id) and a header row.", + "description": "Custom gene id mapping file", + "help_text": "Path to comma-separated file containing custom gene id mappings. Each row represents a mapping from the original gene ID in your count datasets to the ensembl ID in g:Profiler. The mapping file should be a comma-separated file with 2 columns (original_gene_id and ensembl_gene_id) and a header row.", "fa_icon": "fas fa-file-csv" }, "gene_metadata": { @@ -137,8 +139,8 @@ "schema": "assets/schema_gene_metadata.json", "mimetype": "text/csv", "pattern": "^\\S+\\.csv$", - "description": "Path to comma-separated file containing custom gene metadata information. Each row represents a gene and links its ensembl gene ID to its name and description.", - "help_text": "The metadata file should be a comma-separated file with 3 columns (ensembl_gene_id, name and description) and a header row.", + "description": "Custom gene metadata file", + "help_text": "Path to comma-separated file containing custom gene metadata information. Each row represents a gene and links its ensembl gene ID to its name and description. The metadata file should be a comma-separated file with 3 columns (ensembl_gene_id, name and description) and a header row.", "fa_icon": "fas fa-file-csv" } } @@ -151,14 +153,14 @@ "properties": { "normalisation_method": { "type": "string", - "description": "Tool to use for normalisation.", + "description": "Tool to use for normalisation", "fa_icon": "fas fa-chart-simple", "enum": ["deseq2", "edger"], "default": "deseq2" }, "nb_top_gene_candidates": { "type": "integer", - "description": "Number of candidate genes to keep in the final list.", + "description": "Number of candidate genes to keep", "fa_icon": "fas fa-chart-simple", "minimum": 1, "default": 1000, @@ -166,7 +168,7 @@ }, "ks_pvalue_threshold": { "type": "number", - "description": "Threshold for KS p-value for considering samples counts as a uniform distribution.", + "description": "Threshold for KS p-value for considering samples counts as a uniform distribution", "fa_icon": "fas fa-chart-simple", "maximum": 1, "default": 0, From 032b9881f0fd1f96cf7076dd3de147e6d7e45792 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sun, 6 Jul 2025 06:03:05 +0200 Subject: [PATCH 020/258] finalised frontend for galaxy tool --- .pre-commit-config.yaml | 2 +- assets/schema_datasets.json | 4 +- galaxy/README.md | 18 ++ galaxy/build/build_tool.py | 45 ++++ galaxy/build/formatters/__init__.py | 4 + .../formatters/config/base.py} | 38 ++-- galaxy/build/formatters/schema/base.py | 98 +++++++++ .../formatters/schema/parameter/__init__.py | 14 ++ .../build/formatters/schema/parameter/base.py | 155 ++++++++++++++ .../formatters/schema/parameter/datasets.py | 48 +++++ .../schema/parameter/default_value.py | 8 + .../formatters/schema/parameter/required.py | 8 + galaxy/build/static/tool.boilerplate.xml | 77 +++++++ galaxy/test/galaxy_server.sh | 5 +- galaxy/test/requirements.txt | 4 + galaxy/test/serve.sh | 7 + galaxy/tools/build_tool.py | 43 ---- galaxy/tools/rebuild_samplesheet.py | 69 ++++++ galaxy/tools/schema_formatter.py | 201 ------------------ galaxy/tools/static_tool.xml | 50 ----- galaxy/tools/test/input.csv | 3 + galaxy/tools/test/microarray.normalised.csv | 10 + .../test/microarray.normalised.design.csv | 13 ++ galaxy/tools/test/rnaseq.raw.csv | 10 + galaxy/tools/test/rnaseq.raw.design.csv | 13 ++ galaxy/tools/tool.xml | 185 ++++++++-------- nextflow_schema.json | 21 +- 27 files changed, 739 insertions(+), 414 deletions(-) create mode 100644 galaxy/build/build_tool.py create mode 100644 galaxy/build/formatters/__init__.py rename galaxy/{tools/config_formatter.py => build/formatters/config/base.py} (64%) create mode 100644 galaxy/build/formatters/schema/base.py create mode 100644 galaxy/build/formatters/schema/parameter/__init__.py create mode 100644 galaxy/build/formatters/schema/parameter/base.py create mode 100644 galaxy/build/formatters/schema/parameter/datasets.py create mode 100644 galaxy/build/formatters/schema/parameter/default_value.py create mode 100644 galaxy/build/formatters/schema/parameter/required.py create mode 100644 galaxy/build/static/tool.boilerplate.xml create mode 100644 galaxy/test/requirements.txt create mode 100755 galaxy/test/serve.sh delete mode 100644 galaxy/tools/build_tool.py create mode 100644 galaxy/tools/rebuild_samplesheet.py delete mode 100644 galaxy/tools/schema_formatter.py delete mode 100644 galaxy/tools/static_tool.xml create mode 100644 galaxy/tools/test/input.csv create mode 100644 galaxy/tools/test/microarray.normalised.csv create mode 100644 galaxy/tools/test/microarray.normalised.design.csv create mode 100644 galaxy/tools/test/rnaseq.raw.csv create mode 100644 galaxy/tools/test/rnaseq.raw.design.csv diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 46cf9e1d..1a131d0a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: rev: "3.1.2" hooks: - id: editorconfig-checker - exclude: '\.drawio$' + exclude: '\.drawio$|^galaxy/.*\.xml$' alias: ec - repo: https://github.com/astral-sh/ruff-pre-commit diff --git a/assets/schema_datasets.json b/assets/schema_datasets.json index 86aac3bf..3f43549d 100644 --- a/assets/schema_datasets.json +++ b/assets/schema_datasets.json @@ -11,14 +11,14 @@ "type": "string", "format": "file-path", "exists": true, - "pattern": "^\\S+\\.csv$", + "pattern": "^\\S+\\.(csv|dat)$", "errorMessage": "You must provide a count dataset file" }, "design": { "type": "string", "format": "file-path", "exists": true, - "pattern": "^\\S+\\.csv$", + "pattern": "^\\S+\\.(csv|dat)$", "errorMessage": "You must provide a design file", "meta": ["design"] }, diff --git a/galaxy/README.md b/galaxy/README.md index 45dd586c..d77ae901 100644 --- a/galaxy/README.md +++ b/galaxy/README.md @@ -1,5 +1,23 @@ # Galaxy +You need to create a virtual environment for running planemo. You cannot use conda for that (otherwise that would be too easy...) +See https://github.com/galaxyproject/galaxy/issues/20011 + +If venv and pip are not available on your system: + ```bash +sudo apt install python3-venv python3-pip -y +``` + +NB: you may need to use pyenv to install a proper Python version +then create and setup your environment: + +```bash +cd test +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt ``` + +You need micromamba installed. diff --git a/galaxy/build/build_tool.py b/galaxy/build/build_tool.py new file mode 100644 index 00000000..501abd1c --- /dev/null +++ b/galaxy/build/build_tool.py @@ -0,0 +1,45 @@ +import logging + +from formatters import SchemaFormatter, ConfigFormatter + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +STATIC_TOOL_FILENAME = "static/tool.boilerplate.xml" +OUTPUT_TOOL_FILENAME = "../tools/tool.xml" + + +def main(): + logger.info("Formatting config") + config_formatter = ConfigFormatter() + + logger.info("Formatting schema") + schema_formatter = SchemaFormatter() + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # REPLACING ACTUAL PARAMS IN STATIC TOOL + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + with open(STATIC_TOOL_FILENAME, "r") as fin: + static_string = fin.read() + + logger.info("Building tool XML file") + tool_string = ( + static_string.replace( + "NEXTFLOW_VERSION", config_formatter.package_version["nextflow"] + ) + .replace("SINGULARITY_VERSION", config_formatter.package_version["singularity"]) + .replace("PIPELINE_VERSION", config_formatter.pipeline_version) + .replace("DESCRIPTION", schema_formatter.pipeline_description) + .replace("PARAMETERS", schema_formatter.params_cli) + .replace("INPUTS", schema_formatter.inputs) + ) + + with open(OUTPUT_TOOL_FILENAME, "w") as fout: + fout.write(tool_string) + + logger.info("Done") + + +if __name__ == "__main__": + main() diff --git a/galaxy/build/formatters/__init__.py b/galaxy/build/formatters/__init__.py new file mode 100644 index 00000000..4f46ac15 --- /dev/null +++ b/galaxy/build/formatters/__init__.py @@ -0,0 +1,4 @@ +from .schema.base import SchemaFormatter +from .config.base import ConfigFormatter + +__all__ = ["SchemaFormatter", "ConfigFormatter"] diff --git a/galaxy/tools/config_formatter.py b/galaxy/build/formatters/config/base.py similarity index 64% rename from galaxy/tools/config_formatter.py rename to galaxy/build/formatters/config/base.py index 63bd31cb..66e84647 100644 --- a/galaxy/tools/config_formatter.py +++ b/galaxy/build/formatters/config/base.py @@ -1,21 +1,22 @@ from pathlib import Path -import subprocess +import requests import re from dataclasses import dataclass, field from typing import ClassVar +import logging + +logger = logging.getLogger(__name__) @dataclass class BaseConfigFormatter: - CONFIG_FILE: ClassVar[Path] = Path(__file__).parents[2] / "nextflow.config" - MAIN_FILE: ClassVar[Path] = Path(__file__).parents[2] / "main.nf" - NXF_VERSION_COMMAND_TEMPLATE: ClassVar[str] = ( - "micromamba search --override-channels --channel bioconda 'pkg' | grep pkg | awk '{print $2}' | sort | tail -1" - ) + CONFIG_FILE: ClassVar[Path] = Path(__file__).parents[4] / "nextflow.config" + MAIN_FILE: ClassVar[Path] = Path(__file__).parents[4] / "main.nf" PACKAGES: ClassVar[list] = ["nextflow", "singularity"] pipeline_version: str = field(init=False) package_version: dict = field(init=False, default_factory=dict) + executable: str = field(init=False) def __post_init__(self): # CONDA PACKAGE VERSIONS @@ -31,30 +32,27 @@ def __post_init__(self): @classmethod def get_package_version(cls, package_name: str) -> str: """ - Get latest conda version of package + Get latest pip version of package """ - nxf_version_command = cls.NXF_VERSION_COMMAND_TEMPLATE.replace( - "pkg", package_name - ) - result = subprocess.run( - nxf_version_command, shell=True, capture_output=True, text=True, check=True - ) - - if result.stderr: - raise RuntimeError(f"Command error: {result.stderr}") - - return result.stdout.strip("\n") + url = f"https://pypi.org/pypi/{package_name}/json" + try: + response = requests.get(url) + response.raise_for_status() + data = response.json() + return data["info"]["version"] + except requests.RequestException as e: + raise RuntimeError(f"Error fetching version info: {e}") @staticmethod def get_pipeline_version(pipeline_config: str): - # Regular expression to find the manifest block and extract the version + # regular expression to find the manifest block and extract the version manifest_pattern = re.compile(r"manifest\s*{\s*(.*?)\s*}", re.DOTALL) manifest_match = manifest_pattern.search(pipeline_config) version = None if manifest_match: manifest_content = manifest_match.group(1) - # Regular expression to find the version field + # regular expression to find the version field version_pattern = re.compile(r'version\s*=\s*[\'"](.*?)[\'"]') version_match = version_pattern.search(manifest_content) diff --git a/galaxy/build/formatters/schema/base.py b/galaxy/build/formatters/schema/base.py new file mode 100644 index 00000000..50a702f0 --- /dev/null +++ b/galaxy/build/formatters/schema/base.py @@ -0,0 +1,98 @@ +from pathlib import Path +import json +from dataclasses import dataclass, field +from typing import ClassVar +from . import parameter + + +@dataclass +class SchemaFormatter: + SCHEMA_FILE: ClassVar[Path] = Path(__file__).parents[4] / "nextflow_schema.json" + PARAMS_TO_IGNORE: ClassVar[list] = ["outdir", "email", "multiqc_title"] + SECTIONS_TO_IGNORE: ClassVar[list] = [ + "institutional_config_options", + "generic_options", + ] + SECTIONS_TO_EXPAND: ClassVar[list] = ["input_output_options"] + + pipeline_description: str = field(init=False) + inputs: str = field(init=False) + params_cli: str = field(init=False) + _pipeline_params: dict = field(init=False) + + _inputs: list = field(init=False, default_factory=list) + _params_cli: list = field(init=False, default_factory=list) + + def __post_init__(self): + self.parse_schema_file() + + def parse_schema_file(self): + with open(self.SCHEMA_FILE, "r") as f: + pipeline_schema = json.load(f) + + self.pipeline_description = pipeline_schema["description"].strip("\n") + self._pipeline_params = pipeline_schema["$defs"] + + # PARSING PARAMETERS AND BUILDING STRINGS + for section, section_dict in self._pipeline_params.items(): + if section in self.SECTIONS_TO_IGNORE: + continue + + section_inputs, section_params_cli, section_usage_options = ( + self.format_input_section(section, section_dict) + ) + + self._inputs += section_inputs + self._params_cli += section_params_cli + + self.inputs = "\n".join(self._inputs) + self.params_cli = "\n".join(self._params_cli) + + def format_input_section( + self, section: str, section_dict: dict + ) -> tuple[list, list, list]: + section_inputs = [] + section_params_cli = [] + section_usage_options = [] + + section_title = "" + section_help = "" + + if title := section_dict.get("title"): + section_title = f' title="{title}"' + if description := section_dict.get("description"): + section_help = f' help="{description}"' + + section_expanded = ( + ' expanded="true"' + if section in self.SECTIONS_TO_EXPAND + else ' expanded="false"' + ) + + section_inputs.append( + f'\t\t
    ' + ) + section_usage_options.append("\n\t" + section.capitalize().replace("_", " ")) + + required_params = section_dict.get("required", []) + + for param, param_dict in section_dict["properties"].items(): + if param not in self.PARAMS_TO_IGNORE: + optional = param not in required_params + + # checking if param must be parsed in a generic or in a custom way + if param in parameter.PARAMETER_TO_CUSTOM_CLASS: + class_ = parameter.PARAMETER_TO_CUSTOM_CLASS[param] + else: + class_ = parameter.BaseParameterFormatter + + param_formatter = class_(param, section, param_dict, optional) + + # input arguments + section_inputs.append(param_formatter.get_input()) + # cli + section_params_cli.append(param_formatter.get_cli()) + + section_inputs.append("\t\t
    ") + + return section_inputs, section_params_cli, section_usage_options diff --git a/galaxy/build/formatters/schema/parameter/__init__.py b/galaxy/build/formatters/schema/parameter/__init__.py new file mode 100644 index 00000000..b75b8c65 --- /dev/null +++ b/galaxy/build/formatters/schema/parameter/__init__.py @@ -0,0 +1,14 @@ +from .base import BaseParameterFormatter +from .datasets import DatasetsParameterFormatter +from .required import RequiredParameterFormatter +from .default_value import DefaultValueParameterFormatter + +PARAMETER_TO_CUSTOM_CLASS = { + "datasets": DatasetsParameterFormatter, + "normalisation_method": RequiredParameterFormatter, + "nb_top_gene_candidates": RequiredParameterFormatter, + "ks_pvalue_threshold": RequiredParameterFormatter, + "species": DefaultValueParameterFormatter, +} + +__all__ = ["BaseParameterFormatter"] diff --git a/galaxy/build/formatters/schema/parameter/base.py b/galaxy/build/formatters/schema/parameter/base.py new file mode 100644 index 00000000..8955e736 --- /dev/null +++ b/galaxy/build/formatters/schema/parameter/base.py @@ -0,0 +1,155 @@ +from dataclasses import dataclass +from typing import ClassVar + + +@dataclass +class Validator: + PATTERN: ClassVar[str] = ( + '\t\t\t{expression}\n' + ) + + type: str + message: str + expression: str + + def __str__(self): + return self.PATTERN.format( + type=self.type, message=self.message, expression=self.expression + ) + + +@dataclass +class Option: + PATTERN: ClassVar[str] = ( + '\t\t\t\n' + ) + + value: str + default_value: str + optional: bool + + def __str__(self): + selected_arg = ' selected="true"' if self.value == self.default_value else "" + return self.PATTERN.format( + option=self.value, label=self.value.capitalize(), selected_arg=selected_arg + ) + + +@dataclass +class BaseParameterFormatter: + NF_TYPES_TO_GALAXY: ClassVar[dict] = { + "string": "text", + "boolean": "boolean", + "integer": "integer", + "number": "float", + } + + param: str + section: str + param_dict: dict + optional: bool + + @staticmethod + def enrich_input_param(input_param_str: str, args: list[str]) -> str: + # opening param for enrichment + input_param_str = input_param_str.replace(" />", ">\n") + # adding each arg in a separate line + for arg in args: + input_param_str += "\t" + arg + # closing + input_param_str += "\t\t\t" + return input_param_str + + def get_input(self) -> str: + """ + building input param + """ + + input_param_str = '\t\t\t' + + param_format = "" + param_label = "" + param_help = "" + param_true_false = "" + param_value = "" + param_min = "" + param_max = "" + param_optional = ' optional="true"' if self.optional else ' optional="false"' + + param_type = self.param_dict["type"] + default_value = self.param_dict.get("default") + + if param_type == "string" and self.param_dict.get("format") == "file-path": + input_type = "data" + # removing extension check as files are renamed in .dat files by Galaxy + """ + if pattern := self.param_dict.get("pattern"): + # TODO: handle multiple extensions + extension = pattern.split(".")[-1].strip("$") + param_format = f' format="{extension}"' + """ + + else: + input_type = self.NF_TYPES_TO_GALAXY[param_type] + + if param_type == "boolean": + param_true_false = f' truevalue="--{self.param}" falsevalue=""' + + elif param_type in ["integer", "number"]: + if minimum := self.param_dict.get("minimum"): + param_min = f' min="{minimum}"' + if maximum := self.param_dict.get("maximum"): + param_max = f' max="{maximum}"' + + elif param_type == "string": + # TODO: handle (rare) case where bot enum and pattern are given + if pattern := self.param_dict.get("pattern"): # regex + msg = f"must match regular expression {pattern}" + validator = Validator(type="regex", message=msg, expression=pattern) + input_param_str = self.enrich_input_param( + input_param_str, args=[str(validator)] + ) + + # handle parameter with enum (options) + if option_values := self.param_dict.get("enum"): + input_type = "select" + options = [ + Option(value, default_value, self.optional) for value in option_values + ] + input_param_str = self.enrich_input_param( + input_param_str, args=[str(option) for option in options] + ) + + else: + if default_value is not None: + param_value = f' value="{default_value}"' + + if description := self.param_dict.get("description"): + param_label = f'label="{description}"' + if help_text := self.param_dict.get("help_text"): + param_help = f' help="{help_text}"' + + return input_param_str.format( + param=self.param, + type=input_type, + label=param_label, + format=param_format, + value=param_value, + min=param_min, + max=param_max, + true_false=param_true_false, + help=param_help, + optional=param_optional, + ) + + def get_cli(self) -> str: + # extra quotes if string parameter + value = ( + f'"${self.section}.{self.param}"' + if self.param_dict["type"] == "string" + else f"${self.section}.{self.param}" + ) + if self.optional: + return f"\t\t\t#if ${self.section}.{self.param}\n\t\t\t --{self.param} {value}\n\t\t\t#end if" + else: + return f"\t\t\t--{self.param} {value}" diff --git a/galaxy/build/formatters/schema/parameter/datasets.py b/galaxy/build/formatters/schema/parameter/datasets.py new file mode 100644 index 00000000..a65ee95a --- /dev/null +++ b/galaxy/build/formatters/schema/parameter/datasets.py @@ -0,0 +1,48 @@ +import re +from dataclasses import dataclass +from typing import ClassVar +from .base import BaseParameterFormatter + + +@dataclass +class DatasetsParameterFormatter(BaseParameterFormatter): + # if param is an optional file with multiple possible values, it requires special handling + # see https://docs.galaxyproject.org/en/latest/dev/schema.html#id51 + + SAMPLESHEET_PARAM_NAME: ClassVar[str] = "samplesheet" + CONDITION_NAME: ClassVar[str] = "datasets" + + def get_input(self) -> str: + input_param_str = super().get_input() + # setting to required + # changing param name + input_param_str = input_param_str.replace( + 'optional="true"', 'optional="false"' + ).replace(self.param, self.SAMPLESHEET_PARAM_NAME) + # changing label + input_param_str = re.sub( + r'label="[\s\w]*"', 'label="Samplesheet"', input_param_str + ) + + # adding conditional statement + return f""" \t\t\t + + + {input_param_str} + + + + + + """ + + def get_cli(self) -> str: + # see https://planemo.readthedocs.io/en/latest/writing_advanced.html#consuming-collections + s = """ + \t#if $SAMPLESHEET: + \t\t--datasets renamed_samplesheet.csv + \t#end if""" + return s.replace( + "SAMPLESHEET", + f"{self.section}.{self.CONDITION_NAME}.{self.SAMPLESHEET_PARAM_NAME}", + ) diff --git a/galaxy/build/formatters/schema/parameter/default_value.py b/galaxy/build/formatters/schema/parameter/default_value.py new file mode 100644 index 00000000..e141f461 --- /dev/null +++ b/galaxy/build/formatters/schema/parameter/default_value.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass +from .base import BaseParameterFormatter + + +@dataclass +class DefaultValueParameterFormatter(BaseParameterFormatter): + def __post_init__(self): + self.param_dict["default"] = "Solanum tuberosum" diff --git a/galaxy/build/formatters/schema/parameter/required.py b/galaxy/build/formatters/schema/parameter/required.py new file mode 100644 index 00000000..52cdbb82 --- /dev/null +++ b/galaxy/build/formatters/schema/parameter/required.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass +from .base import BaseParameterFormatter + + +@dataclass +class RequiredParameterFormatter(BaseParameterFormatter): + def __post_init__(self): + self.optional = False diff --git a/galaxy/build/static/tool.boilerplate.xml b/galaxy/build/static/tool.boilerplate.xml new file mode 100644 index 00000000..e7b4ed3f --- /dev/null +++ b/galaxy/build/static/tool.boilerplate.xml @@ -0,0 +1,77 @@ + + DESCRIPTION + + + nextflow + singularity + + + + +INPUTS + + + + + + + + + + + + + + @misc{nf-core/stableexpression, + author = {Coen, Olivier}, + year = {2025}, + title = {nf-core/stableexpression}, + publisher = {GitHub}, + journal = {GitHub repository}, + url = {https://github.com/OlivierCoen/stableexpression}, + } + + + diff --git a/galaxy/test/galaxy_server.sh b/galaxy/test/galaxy_server.sh index 054bc95d..05d01ceb 100755 --- a/galaxy/test/galaxy_server.sh +++ b/galaxy/test/galaxy_server.sh @@ -2,7 +2,9 @@ DOCKER_IMAGE=quay.io/bgruening/galaxy PORT=8080 -tool_dir="$(dirname $(dirname $(readlink -f "$0")))/tools" +galaxy_dir="$(dirname $(dirname $(readlink -f "$0")))" +tool_dir="${galaxy_dir}/tools" +static_dir="${galaxy_dir}/static" status() { if [[ $(sudo lsof -i :$PORT) ]]; then @@ -23,6 +25,7 @@ start() { -p 8021:21 \ -p 8022:22 \ -v $tool_dir:/local_tools \ + -v ${static_dir}/galaxy.yml:/etc/galaxy/galaxy.yml \ -e GALAXY_CONFIG_TOOL_CONFIG_FILE=/etc/galaxy/tool_conf.xml,/local_tools/tool_conf.xml \ $DOCKER_IMAGE echo "Galaxy started !" diff --git a/galaxy/test/requirements.txt b/galaxy/test/requirements.txt new file mode 100644 index 00000000..28fdee6a --- /dev/null +++ b/galaxy/test/requirements.txt @@ -0,0 +1,4 @@ +nextflow +singularity +planemo +pandas diff --git a/galaxy/test/serve.sh b/galaxy/test/serve.sh new file mode 100755 index 00000000..a98fa4bb --- /dev/null +++ b/galaxy/test/serve.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +galaxy_dir="$(dirname $(dirname $(readlink -f "$0")))" +tool_dir="${galaxy_dir}/tools" + +planemo serve $tool_dir + diff --git a/galaxy/tools/build_tool.py b/galaxy/tools/build_tool.py deleted file mode 100644 index fe5abae6..00000000 --- a/galaxy/tools/build_tool.py +++ /dev/null @@ -1,43 +0,0 @@ -import logging - -from schema_formatter import SchemaFormatter -from config_formatter import ConfigFormatter - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -STATIC_TOOL_FILENAME = "static_tool.xml" -OUTPUT_TOOL_FILENAME = "tool.xml" - - -def main(): - cformatter = ConfigFormatter() - sformatter = SchemaFormatter() - - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # REPLACING ACTUAL PARAMS IN STATIC TOOL - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - with open(STATIC_TOOL_FILENAME, "r") as fin: - static_string = fin.read() - - tool_string = ( - static_string.replace( - "NEXTFLOW_VERSION", cformatter.package_version["nextflow"] - ) - .replace("SINGULARITY_VERSION", cformatter.package_version["singularity"]) - .replace("PIPELINE_VERSION", cformatter.pipeline_version) - .replace("DESCRIPTION", sformatter.pipeline_description) - .replace("PARAMETERS", sformatter.params_cli) - .replace("INPUTS", sformatter.inputs) - .replace("USAGE_OPTIONS", sformatter.usage_options) - ) - - with open(OUTPUT_TOOL_FILENAME, "w") as fout: - fout.write(tool_string) - - logger.info("Done") - - -if __name__ == "__main__": - main() diff --git a/galaxy/tools/rebuild_samplesheet.py b/galaxy/tools/rebuild_samplesheet.py new file mode 100644 index 00000000..82ea47f5 --- /dev/null +++ b/galaxy/tools/rebuild_samplesheet.py @@ -0,0 +1,69 @@ +#!/usr/env/bin python +""" +Script dedicated to renaming files in the samplesheet provided. +In Galaxy, data files provided by users are given a new file name. +However, original file names can be retrieved from the name attribute of the file object (inside the tool XML file). +In this script, we replace the original name with the actual Galaxy path. + +""" + +import argparse +import logging +from pathlib import Path +import csv + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--in", dest="samplesheet", type=Path, required=True) + parser.add_argument("--count-files", dest="count_files", type=str, required=True) + parser.add_argument( + "--count-filenames", dest="count_filenames", type=str, nargs="+", required=True + ) + parser.add_argument("--design-files", dest="design_files", type=str, required=True) + parser.add_argument( + "--design-filenames", + dest="design_filenames", + type=str, + nargs="+", + required=True, + ) + parser.add_argument("--out", dest="outfile", type=Path, required=True) + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + + # files and names arrive in the same order + count_files = args.count_files.split(",") + design_files = args.design_files.split(",") + + count_names_to_files = { + name: file for file, name in zip(count_files, args.count_filenames) + } + design_names_to_files = { + name: file for file, name in zip(design_files, args.design_filenames) + } + + renamed_rows = [] + with open(args.samplesheet, "r", newline="") as fin: + reader = csv.DictReader(fin) + header = reader.fieldnames + for row in reader: + original_count_filename = Path(row["counts"]).name + row["counts"] = count_names_to_files[original_count_filename] + if "design" in row: + original_design_filename = Path(row["design"]).name + row["design"] = design_names_to_files[original_design_filename] + renamed_rows.append(row) + + with open(args.outfile, "w", newline="") as fout: + writer = csv.DictWriter(fout, fieldnames=header) + + writer.writeheader() + for row in renamed_rows: + writer.writerow(row) diff --git a/galaxy/tools/schema_formatter.py b/galaxy/tools/schema_formatter.py deleted file mode 100644 index ae1625a0..00000000 --- a/galaxy/tools/schema_formatter.py +++ /dev/null @@ -1,201 +0,0 @@ -from pathlib import Path -import json -from dataclasses import dataclass, field -from typing import ClassVar - - -@dataclass -class BaseSchemaFormatter: - SCHEMA_FILE: ClassVar[Path] = Path(__file__).parents[2] / "nextflow_schema.json" - PARAMS_TO_IGNORE: ClassVar[list] = ["outdir", "email", "multiqc_title"] - SECTIONS_TO_IGNORE: ClassVar[list] = [ - "institutional_config_options", - "generic_options", - ] - SECTIONS_TO_EXPAND: ClassVar[list] = ["input_output_options"] - NF_TYPES_TO_GALAXY: ClassVar[dict] = { - "string": "text", - "boolean": "boolean", - "integer": "integer", - "number": "float", - } - - pipeline_description: str = field(init=False) - inputs: str = field(init=False) - params_cli: str = field(init=False) - usage_options: str = field(init=False) - _pipeline_params: dict = field(init=False) - - _inputs: list = field(init=False, default_factory=list) - _params_cli: list = field(init=False, default_factory=list) - _usage_options: list = field(init=False, default_factory=list) - - def __post_init__(self): - self.parse_schema_file() - - def parse_schema_file(self): - with open(self.SCHEMA_FILE, "r") as f: - pipeline_schema = json.load(f) - - self.pipeline_description = pipeline_schema["description"].strip("\n") - self._pipeline_params = pipeline_schema["$defs"] - - # PARSING PARAMETERS AND BUILDING STRINGS - for section, section_dict in self._pipeline_params.items(): - if section in self.SECTIONS_TO_IGNORE: - continue - - section_inputs, section_params_cli, section_usage_options = ( - self.format_input_section(section, section_dict) - ) - self._inputs += section_inputs - self._params_cli += section_params_cli - self._usage_options += section_usage_options - - self.inputs = "\n".join(self._inputs) - self.params_cli = "\n".join(self._params_cli) - self.usage_options = "\n".join(self._usage_options) - - def format_input_param(self, param: str, param_dict: dict, optional: bool) -> str: - """ - building input param - """ - - input_param_str = '\t\t\t' - param_format = "" - param_label = "" - param_help = "" - param_true_false = "" - param_value = "" - param_min = "" - param_max = "" - param_optional = ' optional="true"' if optional else ' optional="false"' - - param_type = param_dict["type"] - default_value = param_dict.get("default") - - if param_type == "string" and param_dict.get("format") == "file-path": - input_type = "data" - if pattern := param_dict.get("pattern"): - # TODO: handle multiple extensions - extension = pattern.split(".")[-1].strip("$") - param_format = f' format="{extension}"' - - # if param is an optional file with multiple possible values, it requires special handling - # see https://docs.galaxyproject.org/en/latest/dev/schema.html#id51 - - else: - input_type = self.NF_TYPES_TO_GALAXY[param_type] - - if param_type == "boolean": - param_true_false = f' truevalue="--{param}" falsevalue=""' - - elif param_type in ["integer", "number"]: - if minimum := param_dict.get("minimum"): - param_min = f' min="{minimum}"' - if maximum := param_dict.get("maximum"): - param_max = f' max="{maximum}"' - - # handle parameter with enum (options) - if options := param_dict.get("enum"): - input_type = "select" - input_param_str = input_param_str.replace(" />", ">\n") - base_option = ( - '\t\t\t\n' - ) - - for option in options: - selected_arg = ' selected="true"' if option == default_value else "" - option_param = base_option.format( - option=option, - label=option.capitalize(), - selected_arg=selected_arg, - ) - input_param_str += "\t" + option_param - - input_param_str += "\t\t\t" - - else: - if default_value: - param_value = f' value="{default_value}"' - - if description := param_dict.get("description"): - param_label = f' label="{description}"' - if help_text := param_dict.get("help_text"): - param_help = f' help="{help_text}"' - - return input_param_str.format( - param=param, - type=input_type, - label=param_label, - format=param_format, - value=param_value, - min=param_min, - max=param_max, - true_false=param_true_false, - help=param_help, - optional=param_optional, - ) - - @staticmethod - def format_input_param_cli(param: str, section: str, optional: bool) -> str: - if optional: - return f"\t\t\t#if {section}.{param}\n\t\t\t --{param} {section}.{param}\n\t\t\t#end if" - else: - return f"--{param} {section}.{param}" - - @staticmethod - def format_input_param_usage(param: str, param_dict: dict, optional: bool) -> str: - required_param = "" if optional else "[REQUIRED] " - return f'\t\t--{param} <{param_dict["type"]}> {required_param}: {param_dict["description"]}' - - def format_input_section( - self, section: str, section_dict: dict - ) -> tuple[list, list, list]: - section_inputs = [] - section_params_cli = [] - section_usage_options = [] - - section_title = "" - section_help = "" - - if title := section_dict.get("title"): - section_title = f' title="{title}"' - if description := section_dict.get("description"): - section_help = f' help="{description}"' - - section_expanded = ( - ' expanded="true"' - if section in self.SECTIONS_TO_EXPAND - else ' expanded="false"' - ) - - section_inputs.append( - f'\t\t
    ' - ) - section_usage_options.append("\n\t" + section.capitalize().replace("_", " ")) - - required_params = section_dict.get("required", []) - - for param, param_dict in section_dict["properties"].items(): - if param not in self.PARAMS_TO_IGNORE: - optional = param not in required_params - # input arguments - input_param = self.format_input_param(param, param_dict, optional) - section_inputs.append(input_param) - # cli - param_cli = self.format_input_param_cli(param, section, optional) - section_params_cli.append(param_cli) - # usage (help) - input_param_usage = self.format_input_param_usage( - param, param_dict, optional - ) - section_usage_options.append(input_param_usage) - - section_inputs.append("\t\t
    ") - - return section_inputs, section_params_cli, section_usage_options - - -class SchemaFormatter(BaseSchemaFormatter): - pass diff --git a/galaxy/tools/static_tool.xml b/galaxy/tools/static_tool.xml deleted file mode 100644 index 72ae4fc8..00000000 --- a/galaxy/tools/static_tool.xml +++ /dev/null @@ -1,50 +0,0 @@ - - DESCRIPTION - - - nextflow - singularity - - - - -INPUTS - - - - - - - - - - - --species --outdir [options] - -Options: -USAGE_OPTIONS - - ]]> - - - @misc{githubseqtk, - author = {LastTODO, FirstTODO}, - year = {TODO}, - title = {seqtk}, - publisher = {GitHub}, - journal = {GitHub repository}, - url = {https://github.com/lh3/seqtk}, - } - - - diff --git a/galaxy/tools/test/input.csv b/galaxy/tools/test/input.csv new file mode 100644 index 00000000..94b8a05c --- /dev/null +++ b/galaxy/tools/test/input.csv @@ -0,0 +1,3 @@ +counts,design,platform,normalised +/path/to/count1.csv,/path/to/design1.csv,microarray,true +/path/to/count2.csv,/path/to/design2.csv,rnaseq,false diff --git a/galaxy/tools/test/microarray.normalised.csv b/galaxy/tools/test/microarray.normalised.csv new file mode 100644 index 00000000..60869917 --- /dev/null +++ b/galaxy/tools/test/microarray.normalised.csv @@ -0,0 +1,10 @@ +,GSM1528575,GSM1528576,GSM1528579,GSM1528583,GSM1528584,GSM1528585,GSM1528580,GSM1528586,GSM1528582,GSM1528578,GSM1528581,GSM1528577 +ENSRNA049453121,20925.1255070264,136184.261516502,144325.370645564,89427.0987612997,164143.182734208,34178.6378088171,28842.7323281157,76973.395782103,41906.9367255656,44756.5602263121,252562.049703724,6953.65643340122 +ENSRNA049453138,196173.051628372,16607.8367703051,344972.83715281,22602.4535330758,13678.598561184,104546.421532852,15451.4637472048,71664.8857281649,160643.257448002,91459.0578537683,88396.7173963033,281623.08555275 +ENSRNA049454388,91547.4240932405,11625.4857392136,84483.143792525,80582.6604222701,218857.576978944,58304.7350856292,42234.0009090266,88475.1675656357,87306.1181782617,17513.436610296,90922.3378933406,76490.2207674135 +ENSRNA049454416,20925.1255070264,106290.155329953,193607.204524536,47170.3378081581,392119.825420608,190998.270108096,90648.5873169351,81397.1541603848,83813.8734511313,165404.67909724,111127.301869638,194702.380135234 +ENSRNA049454647,99394.3461583754,91343.1022366783,3520.13099135521,71738.2220832404,118547.854196928,20105.0810640101,81377.7090686122,15040.7784861581,66352.6498154789,110918.431865208,55563.6509348192,111258.50293442 +ENSRNA049454661,175247.926121346,66431.3470812206,24640.9169394865,52083.9146631746,360203.095444512,36189.1459152181,70046.6356539953,85820.9125386666,13968.9789085219,50594.3724297441,25256.2049703724,52152.4232505092 +ENSRNA049454747,117703.830977024,154452.881963838,281610.479308417,29481.4611300988,191500.379856576,152798.616086476,53565.0743236435,14156.0268105017,293348.557078959,155674.99209152,63140.5124259309,243377.975169043 +ENSRNA049454887,2615.6406883783,164417.584026021,28161.0479308417,82548.0911642767,50154.861391008,136714.551235268,97859.270398964,64586.872322914,328271.004350264,159566.866893808,151537.229822234,86920.7054175153 +ENSRNA049454931,177863.566809724,81378.4001744952,235848.776420799,88444.3833902964,18238.131414912,120630.48638406,82407.8066517592,50430.8455124123,118736.320722436,68107.8090400402,232357.085727426,163410.926184929 diff --git a/galaxy/tools/test/microarray.normalised.design.csv b/galaxy/tools/test/microarray.normalised.design.csv new file mode 100644 index 00000000..d31e5cef --- /dev/null +++ b/galaxy/tools/test/microarray.normalised.design.csv @@ -0,0 +1,13 @@ +sample,condition +GSM1528575,g1 +GSM1528576,g1 +GSM1528579,g1 +GSM1528583,g2 +GSM1528584,g2 +GSM1528585,g2 +GSM1528580,g3 +GSM1528586,g3 +GSM1528582,g3 +GSM1528578,g4 +GSM1528581,g4 +GSM1528577,g4 diff --git a/galaxy/tools/test/rnaseq.raw.csv b/galaxy/tools/test/rnaseq.raw.csv new file mode 100644 index 00000000..a9a6bdb4 --- /dev/null +++ b/galaxy/tools/test/rnaseq.raw.csv @@ -0,0 +1,10 @@ +,ESM1528575,ESM1528576,ESM1528579,ESM1528583,ESM1528584,ESM1528585,ESM1528580,ESM1528586,ESM1528582,ESM1528578,ESM1528581,ESM1528577 +ENSRNA049453121,1,82,8,82,4,68,88,73,46,57,25,22 +ENSRNA049453138,68,93,41,84,36,18,28,92,84,85,92,32 +ENSRNA049454388,38,10,0,23,11,17,95,57,25,82,10,70 +ENSRNA049454416,75,55,7,30,79,60,15,97,12,35,60,56 +ENSRNA049454647,35,64,55,91,48,95,68,100,24,26,100,47 +ENSRNA049454661,8,99,80,48,86,29,80,17,19,9,44,2 +ENSRNA049454747,67,7,98,53,3,10,52,87,4,80,22,15 +ENSRNA049454887,8,40,24,90,42,52,79,81,94,23,35,81 +ENSRNA049454931,45,49,67,73,26,76,41,16,34,47,36,25 diff --git a/galaxy/tools/test/rnaseq.raw.design.csv b/galaxy/tools/test/rnaseq.raw.design.csv new file mode 100644 index 00000000..469751d2 --- /dev/null +++ b/galaxy/tools/test/rnaseq.raw.design.csv @@ -0,0 +1,13 @@ +sample,condition +ESM1528575,g1 +ESM1528576,g1 +ESM1528579,g1 +ESM1528583,g2 +ESM1528584,g2 +ESM1528585,g2 +ESM1528580,g3 +ESM1528586,g3 +ESM1528582,g3 +ESM1528578,g4 +ESM1528581,g4 +ESM1528577,g4 diff --git a/galaxy/tools/tool.xml b/galaxy/tools/tool.xml index a36691bc..e6e63fe0 100644 --- a/galaxy/tools/tool.xml +++ b/galaxy/tools/tool.xml @@ -1,92 +1,132 @@ - + Pipeline dedicated to finding the most stable genes across count datasets - nextflow - singularity + nextflow + singularity
    - - + + ([a-zA-Z]+)[_ ]([a-zA-Z]+) + + + + + + + + + + + +
    - - - - - - - + + ([a-zA-Z,]+) + + + ([A-Z0-9-]+,?)+ + + + + + ([A-Z0-9-]+,?)+ + +
    - - - + + +
    - + - - + +
    - + @@ -96,43 +136,18 @@ VERSION="1.0dev"; echo "$VERSION" --species --outdir [options] - -Options: - - Input output options - --species [REQUIRED] : Species name - --datasets : User count datasets - - Expression atlas options - --skip_fetch_eatlas_accessions : Skip fetching Expression Atlas accessions - --eatlas_keywords : Expression Atlas keywords - --eatlas_accessions : Expression Atlas accessions - --eatlas_accessions_file : File with Expression Atlas accessions - --accessions_only : Only get accessions from Expression Atlas and exit. - --exclude_eatlas_accessions : Expression Atlas accession to exclude - --exclude_eatlas_accessions_file : File with Expression Atlas accessions to exclude - - Idmapping options - --skip_gprofiler : Skip g:Profiler ID mapping step - --gene_id_mapping_file : Custom gene id mapping file - --gene_metadata : Custom gene metadata file - - Statistical options - --normalisation_method : Tool to use for normalisation - --nb_top_gene_candidates : Number of candidate genes to keep - --ks_pvalue_threshold : Threshold for KS p-value for considering samples counts as a uniform distribution +See https://nf-co.re/stableexpression/ for detailed documentation ]]> - @misc{githubseqtk, - author = {LastTODO, FirstTODO}, - year = {TODO}, - title = {seqtk}, + @misc{nf-core/stableexpression, + author = {Coen, Olivier}, + year = {2025}, + title = {nf-core/stableexpression}, publisher = {GitHub}, journal = {GitHub repository}, - url = {https://github.com/lh3/seqtk}, + url = {https://github.com/OlivierCoen/stableexpression}, } diff --git a/nextflow_schema.json b/nextflow_schema.json index 71ce52f8..bbee13e4 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -31,12 +31,17 @@ "format": "file-path", "exists": true, "schema": "assets/schema_datasets.json", - "mimetype": "text/csv", - "pattern": "^\\S+\\.csv$", + "pattern": "^\\S+\\.(csv|dat)$", "description": "User count datasets", - "help_text": "Path to comma-separated file containing information about the input count datasets and their related experimental design. The dataset file should be a comma-separated file with 3 columns, and a header row. See [usage docs](https://nf-co.re/stableexpression/usage#samplesheet-input) for more information. Before running the pipeline, you will need to create a design file with information about the samples in your experiment. Use this parameter to specify its location. Combine with --skip_fetch_eatlas_accessions if you only want to analyse local datasets.", + "help_text": "Path to CSV file containing information about the input count datasets and their related experimental design. The dataset file should be a comma-separated file with 3 columns, and a header row. See [usage docs](https://nf-co.re/stableexpression/usage#samplesheet-input) for more information. Before running the pipeline, and for each user count dataset, you will need to create a design file with information about the samples in your experiment. Use this parameter to specify its location. Combine with --skip_fetch_eatlas_accessions if you only want to analyse your own count datasets.", "fa_icon": "fas fa-file-csv" }, + "skip_fetch_eatlas_accessions": { + "type": "boolean", + "fa_icon": "fas fa-cloud-arrow-down", + "description": "Skip fetching Expression Atlas accessions", + "help_text": "Expression Atlas accessions are automatically fetched by default. Set this parameter to use your own count datasets only." + }, "email": { "type": "string", "description": "Email address for completion summary.", @@ -58,12 +63,6 @@ "fa_icon": "fas fa-book-atlas", "description": "Options for fetching datasets from Expression Atlas.", "properties": { - "skip_fetch_eatlas_accessions": { - "type": "boolean", - "fa_icon": "fas fa-cloud-arrow-down", - "description": "Skip fetching Expression Atlas accessions", - "help_text": "Expression Atlas accessions are automatically fetched by default. You can skip this step by setting this parameter." - }, "eatlas_keywords": { "type": "string", "description": "Expression Atlas keywords", @@ -127,7 +126,7 @@ "exists": true, "schema": "assets/schema_gene_id_mapping.json", "mimetype": "text/csv", - "pattern": "^\\S+\\.csv$", + "pattern": "^\\S+\\.(csv|dat)$", "description": "Custom gene id mapping file", "help_text": "Path to comma-separated file containing custom gene id mappings. Each row represents a mapping from the original gene ID in your count datasets to the ensembl ID in g:Profiler. The mapping file should be a comma-separated file with 2 columns (original_gene_id and ensembl_gene_id) and a header row.", "fa_icon": "fas fa-file-csv" @@ -138,7 +137,7 @@ "exists": true, "schema": "assets/schema_gene_metadata.json", "mimetype": "text/csv", - "pattern": "^\\S+\\.csv$", + "pattern": "^\\S+\\.(csv|dat)$", "description": "Custom gene metadata file", "help_text": "Path to comma-separated file containing custom gene metadata information. Each row represents a gene and links its ensembl gene ID to its name and description. The metadata file should be a comma-separated file with 3 columns (ensembl_gene_id, name and description) and a header row.", "fa_icon": "fas fa-file-csv" From 7122c15479c814691b99b139c4b23ff520d551a5 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sun, 6 Jul 2025 16:57:54 +0200 Subject: [PATCH 021/258] update galaxy conf --- galaxy/build/static/tool.boilerplate.xml | 4 +++- galaxy/tools/test/input.csv | 3 --- galaxy/tools/test/microarray.normalised.csv | 10 ---------- galaxy/tools/test/microarray.normalised.design.csv | 13 ------------- galaxy/tools/test/rnaseq.raw.csv | 10 ---------- galaxy/tools/test/rnaseq.raw.design.csv | 13 ------------- galaxy/tools/tool.xml | 4 ++-- galaxy/tools/tool_dependencies.xml | 12 ++++++++++++ 8 files changed, 17 insertions(+), 52 deletions(-) delete mode 100644 galaxy/tools/test/input.csv delete mode 100644 galaxy/tools/test/microarray.normalised.csv delete mode 100644 galaxy/tools/test/microarray.normalised.design.csv delete mode 100644 galaxy/tools/test/rnaseq.raw.csv delete mode 100644 galaxy/tools/test/rnaseq.raw.design.csv create mode 100644 galaxy/tools/tool_dependencies.xml diff --git a/galaxy/build/static/tool.boilerplate.xml b/galaxy/build/static/tool.boilerplate.xml index e7b4ed3f..19839efb 100644 --- a/galaxy/build/static/tool.boilerplate.xml +++ b/galaxy/build/static/tool.boilerplate.xml @@ -6,6 +6,7 @@ VERSION="PIPELINE_VERSION"; echo "$VERSION" nextflow singularity + openjdk + + + + + NXF_OFFLINE=false + curl -s https://get.nextflow.io | bash + chmod +x nextflow + + + + From e679699f8a1dd21f5785dad318c1b287eab48749 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sun, 6 Jul 2025 16:59:25 +0200 Subject: [PATCH 022/258] allow not providing design data --- assets/schema_datasets.json | 2 +- bin/merge_data.py | 31 +----------- bin/normalise_with_deseq2.R | 48 ++++++++++++++----- bin/normalise_with_edger.R | 25 ++++++++-- conf/test.config | 17 ++----- ...dataset.config => test_eatlas_only.config} | 17 +++++-- modules/local/idmapping/gprofiler/main.nf | 2 - modules/local/merge_data/main.nf | 3 -- modules/local/normalisation/deseq2/main.nf | 4 +- modules/local/normalisation/edger/main.nf | 4 +- nextflow.config | 3 +- .../local/expressionatlas_fetchdata/main.nf | 2 +- .../local/normalisation/deseq2/main.nf.test | 43 ++++++++++++++--- .../normalisation/deseq2/main.nf.test.snap | 21 +++++++- .../local/normalisation/edger/main.nf.test | 21 ++++++++ .../normalisation/edger/main.nf.test.snap | 17 +++++++ tests/test_data/input_datasets/input.csv | 2 + .../input_datasets/microarray2.normalised.csv | 10 ++++ .../test_data/input_datasets/rnaseq2.raw.csv | 10 ++++ workflows/stableexpression.nf | 7 ++- 20 files changed, 202 insertions(+), 87 deletions(-) rename conf/{test_dataset.config => test_eatlas_only.config} (64%) create mode 100644 tests/test_data/input_datasets/microarray2.normalised.csv create mode 100644 tests/test_data/input_datasets/rnaseq2.raw.csv diff --git a/assets/schema_datasets.json b/assets/schema_datasets.json index 3f43549d..5995a1c2 100644 --- a/assets/schema_datasets.json +++ b/assets/schema_datasets.json @@ -35,6 +35,6 @@ "meta": ["normalised"] } }, - "required": ["counts", "design", "platform", "normalised"] + "required": ["counts", "platform", "normalised"] } } diff --git a/bin/merge_data.py b/bin/merge_data.py index 1cc63f43..55757f41 100755 --- a/bin/merge_data.py +++ b/bin/merge_data.py @@ -12,7 +12,6 @@ logger = logging.getLogger(__name__) ALL_COUNTS_PARQUET_OUTFILENAME = "all_counts.parquet" -ALL_DESIGNS_OUTFILENAME = "all_designs.csv" GENE_COUNT_STATS_OUTFILENAME = "gene_count_statistics.csv" SKEWNESS_STATS_OUTFILENAME = "skewness_statistics.csv" KS_TEST_STATS_OUTFILENAME = "ks_test_statistics.csv" @@ -50,9 +49,6 @@ def parse_args(): parser.add_argument( "--counts", type=str, dest="count_files", required=True, help="Count files" ) - parser.add_argument( - "--designs", type=str, dest="design_files", required=True, help="Design files" - ) parser.add_argument( "--stats", type=str, @@ -157,24 +153,6 @@ def get_nb_rows(lf: pl.LazyFrame) -> int: return lf.select(pl.len()).collect().item() -##################################################### -# DESIGNS -##################################################### - - -def parse_design_file(design_file: Path) -> pl.DataFrame: - design_df = pl.read_csv(design_file, has_header=True) - # adding batch name from file stem if not present - if "batch" not in design_df.columns: - design_df = design_df.with_columns(pl.lit(design_file.stem).alias("batch")) - return design_df.select("batch", "condition", "sample") - - -def merge_designs(design_files: list[Path]) -> pl.DataFrame: - design_dfs = [parse_design_file(design_file) for design_file in design_files] - return pl.concat(design_dfs, how="vertical") - - ##################################################### # STATISTICS ##################################################### @@ -237,7 +215,6 @@ def get_candidate_gene_counts( def export_data( count_df: pl.DataFrame, - design_df: pl.DataFrame, candidate_gene_counts_df: pl.DataFrame, corr_df: pl.DataFrame, ): @@ -245,9 +222,6 @@ def export_data( logger.info(f"Exporting normalised counts to: {ALL_COUNTS_PARQUET_OUTFILENAME}") count_df.write_parquet(ALL_COUNTS_PARQUET_OUTFILENAME) - logger.info(f"Exporting designs to: {ALL_DESIGNS_OUTFILENAME}") - design_df.write_csv(ALL_DESIGNS_OUTFILENAME) - logger.info( f"Exporting candidate gene counts to: {CANDIDATE_GENE_COUNTS_PARQUET_OUTFILENAME}" ) @@ -279,13 +253,10 @@ def export_individual_statistics(dataset_stats_df: pl.DataFrame): def main(): args = parse_args() count_files = [Path(file) for file in args.count_files.split(" ")] - design_files = [Path(file) for file in args.design_files.split(" ")] dataset_stat_files = [Path(file) for file in args.dataset_stat_files.split(" ")] # putting all counts into a single dataframe count_df = get_counts(count_files) - # putting all design data into a single dataframe - design_df = merge_designs(design_files) # putting all stats data into a single dataframe dataset_stats_df = merge_stats(dataset_stat_files) @@ -296,7 +267,7 @@ def main(): # adding stat about divergence to mean distribution corr_df = compute_distances_to_mean(count_df) - export_data(count_df, design_df, candidate_gene_counts_df, corr_df) + export_data(count_df, candidate_gene_counts_df, corr_df) export_individual_statistics(dataset_stats_df) diff --git a/bin/normalise_with_deseq2.R b/bin/normalise_with_deseq2.R index f4f3652a..168fb5d6 100755 --- a/bin/normalise_with_deseq2.R +++ b/bin/normalise_with_deseq2.R @@ -29,18 +29,27 @@ get_args <- function() { check_samples <- function(count_matrix, design_data) { # check if the column names of count_matrix match the sample names - if (!all(colnames(count_matrix) == design_data$sample)) { + if (!all( colnames(count_matrix) == design_data$sample )) { stop("Sample names in the count matrix do not match the design data.") } + # check for extra samples + extra_samples <- setdiff( colnames(count_matrix), design_data$sample ) + if (length(extra_samples) > 0) { + warning("The following samples are in the count matrix but not in design: ", paste(extra_samples, collapse = ", ")) + } } prefilter_counts <- function(count_matrix, design_data) { - # see https://bioconductor.org/packages/devel/bioc/vignettes/DESeq2/inst/doc/DESeq2.html - # getting size of smallest group - group_sizes <- table(design_data$condition) - smallest_group_size <- min(group_sizes) - # keep genes with at least 10 counts over a certain number of samples - keep <- rowSums(count_matrix >= 10) >= smallest_group_size + if (is.null(design_data)) { + keep <- rowSums(count_matrix >= 10) >= 1 + } else { + # see https://bioconductor.org/packages/devel/bioc/vignettes/DESeq2/inst/doc/DESeq2.html + # getting size of smallest group + group_sizes <- table(design_data$condition) + smallest_group_size <- min(group_sizes) + # keep genes with at least 10 counts over a certain number of samples + keep <- rowSums(count_matrix >= 10) >= smallest_group_size + } filtered_count_matrix <- count_matrix[keep,] return(filtered_count_matrix) } @@ -86,15 +95,26 @@ get_normalised_cpm_counts <- function(count_file, design_file) { # DESeq2 does not accept that so we must convert them into integers count_data[] <- lapply(count_data, as.integer) - design_data <- read.csv(design_file) - count_matrix <- as.matrix(count_data) # in some rare datasets, columns can contain only zeros # we do not consider these columns count_matrix <- remove_all_zero_columns(count_matrix) - # getting design data - design_data <- design_data[design_data$sample %in% colnames(count_matrix), ] + if ( is.null(design_file) ) { + + # faking a design table + design_data <- data.frame( + sample = colnames(count_matrix), + condition = rep("A", ncol(count_matrix)) + ) + + } else { + + # getting design data + design_data <- read.csv(design_file) + # removing extra samples in design table + design_data <- design_data[design_data$sample %in% colnames(count_matrix), ] + } # check if the column names of count_matrix match the sample names check_samples(count_matrix, design_data) @@ -104,6 +124,11 @@ get_normalised_cpm_counts <- function(count_file, design_file) { condition = factor(design_data$condition) ) + # reorder count matrix columns to match design row order + # this is absolutely mandatory + # see https://bioconductor.org/packages/devel/bioc/vignettes/DESeq2/inst/doc/DESeq2.html at part "Count matrix input" + count_matrix <- count_matrix[, design_data$sample ] + # pre-filter genes with low counts filtered_count_matrix <- prefilter_counts(count_matrix, design_data) @@ -116,7 +141,6 @@ get_normalised_cpm_counts <- function(count_file, design_file) { # add a small pseudocount to avoid zero counts filtered_count_matrix <- replace_zero_counts_with_pseudocounts(filtered_count_matrix) - # create DESeq2 object # if the number of distinct conditions is only 1, DESeq2 returns an error num_unique_conditions <- length(unique(design_data$condition)) if (num_unique_conditions == 1) { diff --git a/bin/normalise_with_edger.R b/bin/normalise_with_edger.R index 65f2c17e..e9d02cad 100755 --- a/bin/normalise_with_edger.R +++ b/bin/normalise_with_edger.R @@ -34,9 +34,14 @@ remove_all_zero_columns <- function(df) { check_samples <- function(count_matrix, design_data) { # check if the column names of count_matrix match the sample names - if (!all(colnames(count_matrix) == design_data$sample)) { + if (!all( colnames(count_matrix) == design_data$sample )) { stop("Sample names in the count matrix do not match the design data.") } + # check for extra samples + extra_samples <- setdiff( colnames(count_matrix), design_data$sample ) + if (length(extra_samples) > 0) { + warning("The following samples are in the count matrix but not in design: ", paste(extra_samples, collapse = ", ")) + } } prefilter_counts <- function(count_matrix) { @@ -73,15 +78,27 @@ get_normalised_cpm_counts <- function(count_file, design_file) { print(paste('Normalizing counts in:', count_file)) count_data <- read.csv(args$count_file, row.names = 1) - design_data <- read.csv(design_file) count_matrix <- as.matrix(count_data) # in some rare datasets, columns can contain only zeros # we do not consider these columns count_matrix <- remove_all_zero_columns(count_matrix) - # getting design data - design_data <- design_data[design_data$sample %in% colnames(count_matrix), ] + if ( is.null(design_file) ) { + + # faking a design table + design_data <- data.frame( + sample = colnames(count_matrix), + condition = rep("A", ncol(count_matrix)) + ) + + } else { + + # getting design data + design_data <- read.csv(design_file) + # removing extra samples in design table + design_data <- design_data[design_data$sample %in% colnames(count_matrix), ] + } # check if the column names of count_matrix match the sample names check_samples(count_matrix, design_data) diff --git a/conf/test.config b/conf/test.config index af59596e..8fe4d0c7 100644 --- a/conf/test.config +++ b/conf/test.config @@ -6,26 +6,17 @@ It tests the different ways to use the pipeline, with small data Use as follows: - nextflow run nf-core/stableexpression -profile test, --outdir + nextflow run nf-core/stableexpression -profile test_dataset, --outdir ---------------------------------------------------------------------------------------- */ -process { - resourceLimits = [ - cpus: 4, - memory: '15.GB', - time: '1.h' - ] -} - params { - config_profile_name = 'Test profile' + config_profile_name = 'Test dataset profile' config_profile_description = 'Minimal test dataset to check pipeline function' // Input data species = 'solanum tuberosum' - eatlas_keywords = "potato,stress" - eatlas_accessions = "E-MTAB-552" - outdir = "results/test" + datasets = "tests/test_data/input_datasets/input.csv" + outdir = "results/test_dataset" } diff --git a/conf/test_dataset.config b/conf/test_eatlas_only.config similarity index 64% rename from conf/test_dataset.config rename to conf/test_eatlas_only.config index 8fe4d0c7..af59596e 100644 --- a/conf/test_dataset.config +++ b/conf/test_eatlas_only.config @@ -6,17 +6,26 @@ It tests the different ways to use the pipeline, with small data Use as follows: - nextflow run nf-core/stableexpression -profile test_dataset, --outdir + nextflow run nf-core/stableexpression -profile test, --outdir ---------------------------------------------------------------------------------------- */ +process { + resourceLimits = [ + cpus: 4, + memory: '15.GB', + time: '1.h' + ] +} + params { - config_profile_name = 'Test dataset profile' + config_profile_name = 'Test profile' config_profile_description = 'Minimal test dataset to check pipeline function' // Input data species = 'solanum tuberosum' - datasets = "tests/test_data/input_datasets/input.csv" - outdir = "results/test_dataset" + eatlas_keywords = "potato,stress" + eatlas_accessions = "E-MTAB-552" + outdir = "results/test" } diff --git a/modules/local/idmapping/gprofiler/main.nf b/modules/local/idmapping/gprofiler/main.nf index 6fdbe5e1..4b628300 100644 --- a/modules/local/idmapping/gprofiler/main.nf +++ b/modules/local/idmapping/gprofiler/main.nf @@ -48,8 +48,6 @@ process IDMAPPING_GPROFILER { script: def custom_mapping_arg = gene_id_mapping_file ? "--custom-mappings $gene_id_mapping_file" : "" - println gene_id_mapping_file - println custom_mapping_arg """ map_ids_to_ensembl.py \ --count-file "$count_file" \ diff --git a/modules/local/merge_data/main.nf b/modules/local/merge_data/main.nf index 53da754a..e11affc5 100644 --- a/modules/local/merge_data/main.nf +++ b/modules/local/merge_data/main.nf @@ -9,13 +9,11 @@ process MERGE_DATA { input: path count_files, stageAs: "?/*" - path design_files, stageAs: "?/*" path dataset_stat_files, stageAs: "?/*" val nb_candidate_genes output: path 'all_counts.parquet', emit: all_counts - path 'all_designs.csv', emit: all_designs path 'gene_count_statistics.csv', emit: gene_count_statistics path 'skewness_statistics.csv', emit: skewness_statistics path 'ks_test_statistics.csv', emit: ks_test_statistics @@ -31,7 +29,6 @@ process MERGE_DATA { """ merge_data.py \ --counts "$count_files" \ - --designs "$design_files" \ --stats "$dataset_stat_files" \ --nb-candidate-genes $nb_candidate_genes """ diff --git a/modules/local/normalisation/deseq2/main.nf b/modules/local/normalisation/deseq2/main.nf index d48077bb..58a525ff 100644 --- a/modules/local/normalisation/deseq2/main.nf +++ b/modules/local/normalisation/deseq2/main.nf @@ -25,9 +25,9 @@ process NORMALISATION_DESEQ2 { task.ext.when == null || task.ext.when script: - def design_file = meta.design + def design_arg = meta.design ? "--design ${meta.design}" : "" """ - normalise_with_deseq2.R --counts "$count_file" --design "$design_file" + normalise_with_deseq2.R --counts $count_file $design_arg """ diff --git a/modules/local/normalisation/edger/main.nf b/modules/local/normalisation/edger/main.nf index 3717af06..43735c7c 100644 --- a/modules/local/normalisation/edger/main.nf +++ b/modules/local/normalisation/edger/main.nf @@ -25,9 +25,9 @@ process NORMALISATION_EDGER { task.ext.when == null || task.ext.when script: - def design_file = meta.design + def design_arg = meta.design ? "--design ${meta.design}" : "" """ - normalise_with_edger.R --counts "$count_file" --design "$design_file" + normalise_with_edger.R --counts $count_file $design_arg """ } diff --git a/nextflow.config b/nextflow.config index e730fbe8..115dbb18 100644 --- a/nextflow.config +++ b/nextflow.config @@ -182,9 +182,10 @@ profiles { ] } } + test { includeConfig 'conf/test.config' } + test_eatlas_only { includeConfig 'conf/test_eatlas_only.config' } test_full { includeConfig 'conf/test_full.config' } - test_dataset { includeConfig 'conf/test_dataset.config' } test_dataset_custom_mapping { includeConfig 'conf/test_dataset_custom_mapping.config' } test_one_accession { includeConfig 'conf/test_one_accession.config' } test_one_accession_low_gene_count { includeConfig 'conf/test_one_accession_low_gene_count.config' } diff --git a/subworkflows/local/expressionatlas_fetchdata/main.nf b/subworkflows/local/expressionatlas_fetchdata/main.nf index 943b910d..fd67474e 100644 --- a/subworkflows/local/expressionatlas_fetchdata/main.nf +++ b/subworkflows/local/expressionatlas_fetchdata/main.nf @@ -115,7 +115,7 @@ def groupFilesByDatasetId(ch_design, ch_counts) { .filter { it.get(1).size() == 2 // only groups with two files } - .filter { // only groups with first file as design file and second one as count file + .filter { // only groups with first file as design file and second one as count fileWARN: java.net.ConnectException: Connexion refusée meta, files -> files.get(0).name.endsWith('.design.csv') && !files.get(1).name.endsWith('.design.csv') } diff --git a/tests/modules/local/normalisation/deseq2/main.nf.test b/tests/modules/local/normalisation/deseq2/main.nf.test index f03421bb..fa0fbc36 100644 --- a/tests/modules/local/normalisation/deseq2/main.nf.test +++ b/tests/modules/local/normalisation/deseq2/main.nf.test @@ -13,8 +13,10 @@ nextflow_process { process { """ - meta = [accession: "accession", design: file('$projectDir/tests/test_data/normalisation/base/design.csv')] - input[0] = [meta, file('$projectDir/tests/test_data/normalisation/base/counts.csv')] + input[0] = [ + [ accession: "accession", design: file('$projectDir/tests/test_data/normalisation/base/design.csv') ], + file('$projectDir/tests/test_data/normalisation/base/counts.csv') + ] """ } } @@ -36,8 +38,10 @@ nextflow_process { process { """ - meta = [accession: "accession", design: file('$projectDir/tests/test_data/normalisation/many_zeros/design.csv')] - input[0] = [meta, file('$projectDir/tests/test_data/normalisation/many_zeros/counts.csv')] + input[0] = [ + [ accession: "accession", design: file('$projectDir/tests/test_data/normalisation/many_zeros/design.csv') ], + file('$projectDir/tests/test_data/normalisation/many_zeros/counts.csv') + ] """ } } @@ -57,8 +61,35 @@ nextflow_process { process { """ - meta = [accession: "accession", design: file('$projectDir/tests/test_data/normalisation/one_group/design.csv')] - input[0] = [meta, file('$projectDir/tests/test_data/normalisation/one_group/counts.csv')] + input[0] = [ + [ accession: "accession", design: file('$projectDir/tests/test_data/normalisation/one_group/design.csv') ], + file('$projectDir/tests/test_data/normalisation/one_group/counts.csv') + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out.cpm).match() } + ) + } + + } + + test("Without design") { + + tag "deseq2_wo_design" + + when { + + process { + """ + input[0] = [ + [ accession: "accession" ], + file('$projectDir/tests/test_data/normalisation/base/counts.csv') + ] """ } } diff --git a/tests/modules/local/normalisation/deseq2/main.nf.test.snap b/tests/modules/local/normalisation/deseq2/main.nf.test.snap index 8f6af170..65faed35 100644 --- a/tests/modules/local/normalisation/deseq2/main.nf.test.snap +++ b/tests/modules/local/normalisation/deseq2/main.nf.test.snap @@ -13,9 +13,9 @@ ], "meta": { "nf-test": "0.9.2", - "nextflow": "24.10.3" + "nextflow": "25.04.4" }, - "timestamp": "2025-01-29T14:11:50.643097087" + "timestamp": "2025-07-06T07:33:14.191843839" }, "One group": { "content": [ @@ -35,6 +35,23 @@ }, "timestamp": "2025-01-29T14:20:12.392982447" }, + "Without design": { + "content": [ + [ + [ + { + "accession": "accession" + }, + "counts.cpm.csv:md5,f7885ec8e4301f9650e2a0dbcc2e7467" + ] + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.4" + }, + "timestamp": "2025-07-06T07:37:42.255878918" + }, "Rows with many zeros": { "content": [ [ diff --git a/tests/modules/local/normalisation/edger/main.nf.test b/tests/modules/local/normalisation/edger/main.nf.test index 6df60a75..6bed711c 100644 --- a/tests/modules/local/normalisation/edger/main.nf.test +++ b/tests/modules/local/normalisation/edger/main.nf.test @@ -72,4 +72,25 @@ nextflow_process { } + test("Without design") { + + when { + + process { + """ + meta = [ accession: "accession" ] + input[0] = [meta, file('$projectDir/tests/test_data/normalisation/base/counts.csv')] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out.cpm).match() } + ) + } + + } + } diff --git a/tests/modules/local/normalisation/edger/main.nf.test.snap b/tests/modules/local/normalisation/edger/main.nf.test.snap index e4bc5c2e..a7f057b7 100644 --- a/tests/modules/local/normalisation/edger/main.nf.test.snap +++ b/tests/modules/local/normalisation/edger/main.nf.test.snap @@ -35,6 +35,23 @@ }, "timestamp": "2025-01-29T14:20:58.681438521" }, + "Without design": { + "content": [ + [ + [ + { + "accession": "accession" + }, + "counts.cpm.csv:md5,b4050f70b78f20b06186e8c8daff0d59" + ] + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.4" + }, + "timestamp": "2025-07-06T16:38:32.394644808" + }, "Rows with many zeros": { "content": [ [ diff --git a/tests/test_data/input_datasets/input.csv b/tests/test_data/input_datasets/input.csv index 6ea4aa16..50a5b28e 100644 --- a/tests/test_data/input_datasets/input.csv +++ b/tests/test_data/input_datasets/input.csv @@ -1,3 +1,5 @@ counts,design,platform,normalised tests/test_data/input_datasets/microarray.normalised.csv,tests/test_data/input_datasets/microarray.normalised.design.csv,microarray,true tests/test_data/input_datasets/rnaseq.raw.csv,tests/test_data/input_datasets/rnaseq.raw.design.csv,rnaseq,false +tests/test_data/input_datasets/microarray2.normalised.csv,,microarray,true +tests/test_data/input_datasets/rnaseq2.raw.csv,,rnaseq,false diff --git a/tests/test_data/input_datasets/microarray2.normalised.csv b/tests/test_data/input_datasets/microarray2.normalised.csv new file mode 100644 index 00000000..e7164776 --- /dev/null +++ b/tests/test_data/input_datasets/microarray2.normalised.csv @@ -0,0 +1,10 @@ +,FSM1528575,FSM1528576,FSM1528579,FSM1528583,FSM1528584,FSM1528585,FSM1528580,FSM1528586,FSM1528582,FSM1528578,FSM1528581,FSM1528577 +ENSRNA049453121,20925.1255070264,136184.261516502,144325.370645564,89427.0987612997,164143.182734208,34178.6378088171,28842.7323281157,76973.395782103,41906.9367255656,44756.5602263121,252562.049703724,6953.65643340122 +ENSRNA049453138,196173.051628372,16607.8367703051,344972.83715281,22602.4535330758,13678.598561184,104546.428532852,15451.4637472048,71664.8857281649,160643.257448002,91459.0578537683,88396.7173963033,281623.08555275 +ENSRNA049454388,91547.4240932405,11625.4857392136,84483.143792525,80582.6604222701,218857.576978944,58304.7350856292,42234.0009090266,88475.1675656357,87306.1181782617,17513.436610296,90922.3378933406,76490.2207674135 +ENSRNA049454416,20925.1255070264,106290.155329953,193607.204524536,47170.3378081581,392119.825420608,190998.270108096,90648.5873169351,81397.1541603848,83813.8734511313,165404.67909724,111127.301869638,194702.380135234 +ENSRNA049454647,99394.3461583754,91343.1022366783,3520.13099135521,71738.2220832404,118547.854196928,20105.0810640101,81377.7090686122,15040.7784861581,66352.6498154789,110918.431865208,55563.6509348192,111258.50293442 +ENSRNA049454661,175247.926121346,66431.3470812206,24640.9169394865,52083.9146631746,360203.095444512,36189.1459152181,70046.6356539953,85820.9125386666,13968.9789085219,50594.3724297441,25256.2049703724,52152.4232505092 +ENSRNA049454747,117703.830977024,154452.881963838,281610.479308417,29481.4611380988,191500.379856576,152798.616086476,53565.0743236435,14156.0268105017,293348.557078959,155674.99209152,63140.5124259309,243377.975169043 +ENSRNA049454887,2615.6406883783,164417.584026021,28161.0479308417,82548.0911642767,50154.861391008,136714.551235268,97859.279398964,64586.872322914,328271.004350264,159566.866893808,151537.229822234,86920.7054175153 +ENSRNA049454931,177863.566809724,81378.4001744952,235848.776420799,88444.3833902964,18238.131414912,120630.48638406,82407.8066517592,50430.8455124123,118736.320722436,68107.8090400402,232357.085727426,163410.926184929 diff --git a/tests/test_data/input_datasets/rnaseq2.raw.csv b/tests/test_data/input_datasets/rnaseq2.raw.csv new file mode 100644 index 00000000..79c6d156 --- /dev/null +++ b/tests/test_data/input_datasets/rnaseq2.raw.csv @@ -0,0 +1,10 @@ +,DSM1528575,DSM1528576,DSM1528579,DSM1528583,DSM1528584,DSM1528585,DSM1528580,DSM1528586,DSM1528582,DSM1528578,DSM1528581,DSM1528577 +ENSRNA049453121,1,82,8,82,4,68,88,73,46,57,25,22 +ENSRNA049453138,67,93,41,84,36,18,28,96,84,85,92,32 +ENSRNA049454388,38,10,0,23,11,17,95,57,25,82,10,70 +ENSRNA049454416,75,55,7,30,79,60,15,97,12,35,60,56 +ENSRNA049454647,35,64,55,91,48,95,68,100,24,26,100,47 +ENSRNA049454661,8,99,80,48,86,29,80,17,19,9,48,2 +ENSRNA049454747,67,7,98,53,3,10,52,87,4,80,22,15 +ENSRNA049454887,8,40,27,90,42,52,79,81,94,28,35,81 +ENSRNA049454931,42,49,67,73,26,76,41,16,34,47,36,25 diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 177e9632..f2e38251 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -66,9 +66,8 @@ workflow STABLEEXPRESSION { // ----------------------------------------------------------------- MERGE_DATA( - ch_normalised_counts.map { meta, file -> [file] }.collect(), - ch_normalised_counts.map { meta, file -> [meta.design] }.collect(), - ch_dataset_statistics.map { meta, file -> [file] }.collect(), + ch_normalised_counts.map { meta, file -> [file] }.collect(), + ch_dataset_statistics.map { meta, file -> [file] }.collect(), params.nb_top_gene_candidates ) @@ -99,7 +98,7 @@ workflow STABLEEXPRESSION { .mix( MERGE_DATA.out.gene_count_statistics.collect() ) .mix( MERGE_DATA.out.skewness_statistics.collect() ) .mix( ch_ks_stats.collect() ) - .mix ( MERGE_DATA.out.distribution_correlations.collect() ) + .mix( MERGE_DATA.out.distribution_correlations.collect() ) .set { ch_multiqc_files } MULTIQC_WORKFLOW( ch_multiqc_files ) From 97d7d3029a82ec55654811e3c705343ae8312c03 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sun, 6 Jul 2025 17:21:20 +0200 Subject: [PATCH 023/258] fix issue in eatlas fetdata workflow --- .../local/expressionatlas_fetchdata/main.nf | 51 ++++++++++--------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/subworkflows/local/expressionatlas_fetchdata/main.nf b/subworkflows/local/expressionatlas_fetchdata/main.nf index fd67474e..87396cfb 100644 --- a/subworkflows/local/expressionatlas_fetchdata/main.nf +++ b/subworkflows/local/expressionatlas_fetchdata/main.nf @@ -16,6 +16,7 @@ workflow EXPRESSIONATLAS_FETCHDATA { main: ch_eatlas_datasets = Channel.empty() + ch_fetched_accessions = Channel.empty() ch_eatlas_accessions_file = params.eatlas_accessions_file ? Channel.fromPath(params.eatlas_accessions_file, checkIfExists: true) : Channel.empty() @@ -34,33 +35,35 @@ workflow EXPRESSIONATLAS_FETCHDATA { ch_species, params.eatlas_keywords ) + EXPRESSIONATLAS_GETACCESSIONS.out.txt.set { ch_fetched_accessions } - ch_exclude_eatlas_accessions_file = params.exclude_eatlas_accessions_file ? Channel.fromPath(params.exclude_eatlas_accessions_file, checkIfExists: true) : Channel.empty() - - // getting accessions to exclude and preparing in the right format - Channel.fromList( params.exclude_eatlas_accessions.tokenize(',') ) - .mix( ch_exclude_eatlas_accessions_file.splitText() ) - .unique() - .map { it -> it.trim() } - .toList() - .map { lst -> [lst] } // list of lists : mandatory when combining in the next step - .set { ch_excluded_accessions } - - // appending to accessions provided by the user - // ensures that no accessions is present twice (provided by the user and fetched from E. Atlas) - // removing E-PROT- accessions - // removing excluded accessions - ch_input_accessions - .mix( EXPRESSIONATLAS_GETACCESSIONS.out.txt.splitText() ) - .unique() - .map { it -> it.trim() } - .filter { it.startsWith('E-') && !it.startsWith('E-PROT-') } - .combine ( ch_excluded_accessions ) - .filter { accession, excluded_accessions -> !(accession in excluded_accessions) } - .map { accession, excluded_accessions -> accession } - .set { ch_accessions } } + ch_exclude_eatlas_accessions_file = params.exclude_eatlas_accessions_file ? Channel.fromPath(params.exclude_eatlas_accessions_file, checkIfExists: true) : Channel.empty() + + // getting accessions to exclude and preparing in the right format + Channel.fromList( params.exclude_eatlas_accessions.tokenize(',') ) + .mix( ch_exclude_eatlas_accessions_file.splitText() ) + .unique() + .map { it -> it.trim() } + .toList() + .map { lst -> [lst] } // list of lists : mandatory when combining in the next step + .set { ch_excluded_accessions } + + // appending to accessions provided by the user + // ensures that no accessions is present twice (provided by the user and fetched from E. Atlas) + // removing E-PROT- accessions + // removing excluded accessions + ch_input_accessions + .mix( ch_fetched_accessions.splitText() ) + .unique() + .map { it -> it.trim() } + .filter { it.startsWith('E-') && !it.startsWith('E-PROT-') } + .combine ( ch_excluded_accessions ) + .filter { accession, excluded_accessions -> !(accession in excluded_accessions) } + .map { accession, excluded_accessions -> accession } + .set { ch_accessions } + if ( !params.accessions_only ) { // Downloading Expression Atlas data for each accession in ch_accessions From e7e76c042709cf2e78e90094c02452fcfa2b5a54 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sun, 6 Jul 2025 18:04:13 +0200 Subject: [PATCH 024/258] finalised and tested local development galaxy tool update --- galaxy/.shed.yml | 13 +- galaxy/build/build_tool.py | 7 +- galaxy/build/formatters/config/base.py | 21 +- .../formatters/schema/parameter/datasets.py | 16 +- .../nf_core_stableexpression.boilerplate.xml | 132 + galaxy/build/static/tool.boilerplate.xml | 79 - galaxy/test/galaxy_server.sh | 62 - galaxy/test/requirements.txt | 4 - galaxy/test/test.sh | 8 + galaxy/test/tool_test_output.html | 18659 ++++++++++++++++ galaxy/test/tool_test_output.json | 463 + galaxy/tool/nf_core_stableexpression.xml | 211 + galaxy/{tools => tool}/rebuild_samplesheet.py | 0 galaxy/tool/test-data/input.csv | 3 + .../tool/test-data/microarray.normalised.csv | 10 + .../microarray.normalised.design.csv | 13 + galaxy/tool/test-data/rnaseq.raw.csv | 10 + galaxy/tool/test-data/rnaseq.raw.design.csv | 13 + galaxy/{tools => tool}/tool.xml | 46 +- galaxy/tools/tool_conf.xml | 7 - galaxy/tools/tool_dependencies.xml | 12 - 21 files changed, 19588 insertions(+), 201 deletions(-) create mode 100644 galaxy/build/static/nf_core_stableexpression.boilerplate.xml delete mode 100644 galaxy/build/static/tool.boilerplate.xml delete mode 100755 galaxy/test/galaxy_server.sh delete mode 100644 galaxy/test/requirements.txt create mode 100755 galaxy/test/test.sh create mode 100644 galaxy/test/tool_test_output.html create mode 100644 galaxy/test/tool_test_output.json create mode 100644 galaxy/tool/nf_core_stableexpression.xml rename galaxy/{tools => tool}/rebuild_samplesheet.py (100%) create mode 100644 galaxy/tool/test-data/input.csv create mode 100644 galaxy/tool/test-data/microarray.normalised.csv create mode 100644 galaxy/tool/test-data/microarray.normalised.design.csv create mode 100644 galaxy/tool/test-data/rnaseq.raw.csv create mode 100644 galaxy/tool/test-data/rnaseq.raw.design.csv rename galaxy/{tools => tool}/tool.xml (84%) delete mode 100644 galaxy/tools/tool_conf.xml delete mode 100644 galaxy/tools/tool_dependencies.xml diff --git a/galaxy/.shed.yml b/galaxy/.shed.yml index 699a1fa9..4de9762f 100644 --- a/galaxy/.shed.yml +++ b/galaxy/.shed.yml @@ -1,8 +1,11 @@ categories: - Transcriptomics -description: my_scription -homepage_url: my_tool_homepage -long_description: my_long_description -name: nf_core_stableexpression +description: Pipeline dedicated to finding the most stable genes across count datasets +homepage_url: https://nf-co.re/stableexpression/ +long_description: | + nf-core/stableexpression is a bioinformatics pipeline that aims at finding the most stable genes among a single or multiple public / local count datasets. + It takes as input a species name (mandatory), keywords for expression atlas search (optional) and / or a CSV input file listing local raw / normalised count datasets (optional). + A typical usage is to find the most suitable qPCR housekeeping genes for a specific species (and optionally specific conditions). +name: nf-core/stableexpression owner: olivier_coen -remote_repository_url: my_github_url +remote_repository_url: https://github.com/OlivierCoen/stableexpression/dev/galaxy/.shed.yml diff --git a/galaxy/build/build_tool.py b/galaxy/build/build_tool.py index 501abd1c..786726f9 100644 --- a/galaxy/build/build_tool.py +++ b/galaxy/build/build_tool.py @@ -5,8 +5,8 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -STATIC_TOOL_FILENAME = "static/tool.boilerplate.xml" -OUTPUT_TOOL_FILENAME = "../tools/tool.xml" +STATIC_TOOL_FILENAME = "static/nf_core_stableexpression.boilerplate.xml" +OUTPUT_TOOL_FILENAME = "../tool/nf_core_stableexpression.xml" def main(): @@ -28,7 +28,8 @@ def main(): static_string.replace( "NEXTFLOW_VERSION", config_formatter.package_version["nextflow"] ) - .replace("SINGULARITY_VERSION", config_formatter.package_version["singularity"]) + .replace("APPTAINER_VERSION", config_formatter.package_version["apptainer"]) + .replace("OPENJDK_VERSION", config_formatter.package_version["openjdk"]) .replace("PIPELINE_VERSION", config_formatter.pipeline_version) .replace("DESCRIPTION", schema_formatter.pipeline_description) .replace("PARAMETERS", schema_formatter.params_cli) diff --git a/galaxy/build/formatters/config/base.py b/galaxy/build/formatters/config/base.py index 66e84647..0a3651f7 100644 --- a/galaxy/build/formatters/config/base.py +++ b/galaxy/build/formatters/config/base.py @@ -3,6 +3,7 @@ import re from dataclasses import dataclass, field from typing import ClassVar +from packaging.version import parse as vparse import logging logger = logging.getLogger(__name__) @@ -12,7 +13,11 @@ class BaseConfigFormatter: CONFIG_FILE: ClassVar[Path] = Path(__file__).parents[4] / "nextflow.config" MAIN_FILE: ClassVar[Path] = Path(__file__).parents[4] / "main.nf" - PACKAGES: ClassVar[list] = ["nextflow", "singularity"] + PACKAGES_REPOS: ClassVar[dict] = { + "nextflow": "bioconda", + "apptainer": "conda-forge", + "openjdk": "conda-forge", + } pipeline_version: str = field(init=False) package_version: dict = field(init=False, default_factory=dict) @@ -20,8 +25,8 @@ class BaseConfigFormatter: def __post_init__(self): # CONDA PACKAGE VERSIONS - for package in self.PACKAGES: - self.package_version[package] = self.get_package_version(package) + for package, repo in self.PACKAGES_REPOS.items(): + self.package_version[package] = self.get_package_version(package, repo) # PARSING CONFIG with open(self.CONFIG_FILE, "r") as f: @@ -30,16 +35,20 @@ def __post_init__(self): self.pipeline_version = self.get_pipeline_version(pipeline_config) @classmethod - def get_package_version(cls, package_name: str) -> str: + def get_package_version(cls, package: str, repo: str) -> str: """ Get latest pip version of package """ - url = f"https://pypi.org/pypi/{package_name}/json" + logger.info(f"Getting latest version of package {package}") + url = f" https://api.anaconda.org/package/{repo}/{package}" try: response = requests.get(url) response.raise_for_status() data = response.json() - return data["info"]["version"] + versions = sorted( + data["versions"], reverse=True, key=vparse + ) # from latest to oldest + return versions[0] # most recent except requests.RequestException as e: raise RuntimeError(f"Error fetching version info: {e}") diff --git a/galaxy/build/formatters/schema/parameter/datasets.py b/galaxy/build/formatters/schema/parameter/datasets.py index a65ee95a..067ac516 100644 --- a/galaxy/build/formatters/schema/parameter/datasets.py +++ b/galaxy/build/formatters/schema/parameter/datasets.py @@ -1,6 +1,5 @@ import re from dataclasses import dataclass -from typing import ClassVar from .base import BaseParameterFormatter @@ -9,23 +8,20 @@ class DatasetsParameterFormatter(BaseParameterFormatter): # if param is an optional file with multiple possible values, it requires special handling # see https://docs.galaxyproject.org/en/latest/dev/schema.html#id51 - SAMPLESHEET_PARAM_NAME: ClassVar[str] = "samplesheet" - CONDITION_NAME: ClassVar[str] = "datasets" - def get_input(self) -> str: input_param_str = super().get_input() # setting to required # changing param name input_param_str = input_param_str.replace( 'optional="true"', 'optional="false"' - ).replace(self.param, self.SAMPLESHEET_PARAM_NAME) + ).replace(self.param, "samplesheet") # changing label input_param_str = re.sub( r'label="[\s\w]*"', 'label="Samplesheet"', input_param_str ) # adding conditional statement - return f""" \t\t\t + return f""" \t\t\t {input_param_str} @@ -38,11 +34,7 @@ def get_input(self) -> str: def get_cli(self) -> str: # see https://planemo.readthedocs.io/en/latest/writing_advanced.html#consuming-collections - s = """ - \t#if $SAMPLESHEET: + return f""" + \t#if ${self.section}.datasets.provide_datasets == "true": \t\t--datasets renamed_samplesheet.csv \t#end if""" - return s.replace( - "SAMPLESHEET", - f"{self.section}.{self.CONDITION_NAME}.{self.SAMPLESHEET_PARAM_NAME}", - ) diff --git a/galaxy/build/static/nf_core_stableexpression.boilerplate.xml b/galaxy/build/static/nf_core_stableexpression.boilerplate.xml new file mode 100644 index 00000000..d24fc740 --- /dev/null +++ b/galaxy/build/static/nf_core_stableexpression.boilerplate.xml @@ -0,0 +1,132 @@ + + DESCRIPTION + + + nextflow + apptainer + openjdk + + + + +INPUTS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @misc{nf-core/stableexpression, + author = {Coen, Olivier}, + year = {2025}, + title = {nf-core/stableexpression}, + publisher = {GitHub}, + journal = {GitHub repository}, + url = {https://github.com/OlivierCoen/stableexpression}, + } + + + diff --git a/galaxy/build/static/tool.boilerplate.xml b/galaxy/build/static/tool.boilerplate.xml deleted file mode 100644 index 19839efb..00000000 --- a/galaxy/build/static/tool.boilerplate.xml +++ /dev/null @@ -1,79 +0,0 @@ - - DESCRIPTION - - - nextflow - singularity - openjdk - - - - -INPUTS - - - - - - - - - - - - - - @misc{nf-core/stableexpression, - author = {Coen, Olivier}, - year = {2025}, - title = {nf-core/stableexpression}, - publisher = {GitHub}, - journal = {GitHub repository}, - url = {https://github.com/OlivierCoen/stableexpression}, - } - - - diff --git a/galaxy/test/galaxy_server.sh b/galaxy/test/galaxy_server.sh deleted file mode 100755 index 05d01ceb..00000000 --- a/galaxy/test/galaxy_server.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -DOCKER_IMAGE=quay.io/bgruening/galaxy -PORT=8080 -galaxy_dir="$(dirname $(dirname $(readlink -f "$0")))" -tool_dir="${galaxy_dir}/tools" -static_dir="${galaxy_dir}/static" - -status() { - if [[ $(sudo lsof -i :$PORT) ]]; then - echo "Galaxy is running !" - else - echo "Galaxy is not running !" - fi -} - -start() { - - - # launching docker compose in detached mode - echo "Launching Galaxy in detached mode." - docker run \ - -d \ - -p 8080:80 \ - -p 8021:21 \ - -p 8022:22 \ - -v $tool_dir:/local_tools \ - -v ${static_dir}/galaxy.yml:/etc/galaxy/galaxy.yml \ - -e GALAXY_CONFIG_TOOL_CONFIG_FILE=/etc/galaxy/tool_conf.xml,/local_tools/tool_conf.xml \ - $DOCKER_IMAGE - echo "Galaxy started !" -} - -stop() { - docker stop $(docker ps | grep galaxy | awk '{print $1}') - echo "Galaxy stopped !" -} - -case "$1" in - 'start') - start - ;; - 'stop') - stop - ;; - 'restart') - stop - start - ;; - 'status') - status - ;; - *) - echo - echo "Usage: $0 { start | stop | restart | status }" - echo - exit 1 - ;; -esac - -exit 0 - diff --git a/galaxy/test/requirements.txt b/galaxy/test/requirements.txt deleted file mode 100644 index 28fdee6a..00000000 --- a/galaxy/test/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -nextflow -singularity -planemo -pandas diff --git a/galaxy/test/test.sh b/galaxy/test/test.sh new file mode 100755 index 00000000..635d6946 --- /dev/null +++ b/galaxy/test/test.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +galaxy_dir="$(dirname $(dirname $(readlink -f "$0")))" +tool_file="${galaxy_dir}/tool/nf_core_stableexpression.xml" + +# add --update_test_data to create output file +planemo test $tool_file + diff --git a/galaxy/test/tool_test_output.html b/galaxy/test/tool_test_output.html new file mode 100644 index 00000000..114f72be --- /dev/null +++ b/galaxy/test/tool_test_output.html @@ -0,0 +1,18659 @@ + + + + + + + Test Results (powered by Planemo) + + + + + + + + +
    +
    +
    + + + + + + + diff --git a/galaxy/test/tool_test_output.json b/galaxy/test/tool_test_output.json new file mode 100644 index 00000000..5aec7b73 --- /dev/null +++ b/galaxy/test/tool_test_output.json @@ -0,0 +1,463 @@ +{ + "summary": { + "num_errors": 0, + "num_failures": 0, + "num_skips": 0, + "num_tests": 3 + }, + "tests": [ + { + "data": { + "inputs": { + "input_output_options|datasets|count_datasets": [ + { + "id": "d77c1c1553d8ae81", + "src": "hda" + }, + { + "id": "8f49e6aa49bd8f4e", + "src": "hda" + } + ], + "input_output_options|datasets|experimental_designs": [ + { + "id": "b93aa8e2a35b8857", + "src": "hda" + }, + { + "id": "d10e079854ce5c69", + "src": "hda" + } + ], + "input_output_options|datasets|provide_datasets": true, + "input_output_options|datasets|samplesheet": { + "id": "f14d691d132db03a", + "src": "hda" + }, + "input_output_options|skip_fetch_eatlas_accessions": true, + "input_output_options|species": "solanum tuberosum" + }, + "job": { + "command_line": "python '/home/olivier/repositories/nf-core-stableexpression/galaxy/tool/rebuild_samplesheet.py' --in /tmp/tmppbklvq_q/files/3/5/f/dataset_35f02710-5dc7-48d3-beeb-6670fe961d3c.dat --out renamed_samplesheet.csv --count-files /tmp/tmppbklvq_q/files/a/0/7/dataset_a07a6342-8e00-47d9-83d1-ebd4c14037e0.dat,/tmp/tmppbklvq_q/files/3/4/9/dataset_34937997-5658-430c-b9d7-521063b31b61.dat --count-filenames microarray.normalised.csv rnaseq.raw.csv --design-files /tmp/tmppbklvq_q/files/3/2/5/dataset_32558adb-41a2-4e78-a0fb-dd3fb60e5cfc.dat,/tmp/tmppbklvq_q/files/c/1/e/dataset_c1e00074-715d-4205-af0b-d52abd151ae5.dat --design-filenames microarray.normalised.design.csv rnaseq.raw.design.csv && nextflow run OlivierCoen/stableexpression -r dev -latest -profile apptainer -ansi-log false --outdir ./results --species \"solanum tuberosum\" --datasets renamed_samplesheet.csv --skip_fetch_eatlas_accessions --skip_fetch_eatlas_accessions --normalisation_method \"deseq2\" --nb_top_gene_candidates 1000 --ks_pvalue_threshold 0.0", + "command_version": "1.0dev", + "copied_from_job_id": null, + "create_time": "2025-07-07T13:34:47.305076", + "dependencies": [], + "exit_code": 0, + "external_id": "2189932", + "galaxy_version": "25.0", + "handler": null, + "history_id": "f14d691d132db03a", + "id": "b53dd89ac6a51dd9", + "inputs": { + "input_output_options|datasets|count_datasets": { + "id": "d77c1c1553d8ae81", + "src": "hda", + "uuid": "a07a6342-8e00-47d9-83d1-ebd4c14037e0" + }, + "input_output_options|datasets|count_datasets1": { + "id": "d77c1c1553d8ae81", + "src": "hda", + "uuid": "a07a6342-8e00-47d9-83d1-ebd4c14037e0" + }, + "input_output_options|datasets|count_datasets2": { + "id": "8f49e6aa49bd8f4e", + "src": "hda", + "uuid": "34937997-5658-430c-b9d7-521063b31b61" + }, + "input_output_options|datasets|experimental_designs": { + "id": "b93aa8e2a35b8857", + "src": "hda", + "uuid": "32558adb-41a2-4e78-a0fb-dd3fb60e5cfc" + }, + "input_output_options|datasets|experimental_designs1": { + "id": "b93aa8e2a35b8857", + "src": "hda", + "uuid": "32558adb-41a2-4e78-a0fb-dd3fb60e5cfc" + }, + "input_output_options|datasets|experimental_designs2": { + "id": "d10e079854ce5c69", + "src": "hda", + "uuid": "c1e00074-715d-4205-af0b-d52abd151ae5" + }, + "input_output_options|datasets|samplesheet": { + "id": "f14d691d132db03a", + "src": "hda", + "uuid": "35f02710-5dc7-48d3-beeb-6670fe961d3c" + } + }, + "job_messages": [], + "job_metrics": [], + "job_runner_name": null, + "job_stderr": "", + "job_stdout": "", + "model_class": "Job", + "output_collections": {}, + "outputs": { + "multiqc_report": { + "id": "b53dd89ac6a51dd9", + "src": "hda", + "uuid": "02b60d90-cbc8-4a41-8656-71abf653b621" + }, + "top_stable_genes_summary": { + "id": "4e217783f5b35092", + "src": "hda", + "uuid": "7c59b178-207b-462b-8808-ec7425201f75" + } + }, + "params": { + "__input_ext": "\"txt\"", + "chromInfo": "\"/tmp/tmppbklvq_q/galaxy-dev/tool-data/shared/ucsc/chrom/?.len\"", + "dbkey": "\"?\"", + "expression_atlas_options": "{\"accessions_only\": false, \"eatlas_accessions\": null, \"eatlas_accessions_file\": null, \"eatlas_keywords\": null, \"exclude_eatlas_accessions\": null, \"exclude_eatlas_accessions_file\": null}", + "idmapping_options": "{\"gene_id_mapping_file\": null, \"gene_metadata\": null, \"skip_gprofiler\": false}", + "input_output_options": "{\"datasets\": {\"__current_case__\": 0, \"count_datasets\": {\"values\": [{\"id\": 2, \"src\": \"hda\"}, {\"id\": 3, \"src\": \"hda\"}]}, \"experimental_designs\": {\"values\": [{\"id\": 4, \"src\": \"hda\"}, {\"id\": 5, \"src\": \"hda\"}]}, \"provide_datasets\": true, \"samplesheet\": {\"values\": [{\"id\": 1, \"src\": \"hda\"}]}}, \"skip_fetch_eatlas_accessions\": true, \"species\": \"solanum tuberosum\"}", + "statistical_options": "{\"ks_pvalue_threshold\": \"0.0\", \"nb_top_gene_candidates\": \"1000\", \"normalisation_method\": \"deseq2\"}" + }, + "state": "ok", + "stderr": "", + "stdout": "N E X T F L O W ~ version 25.04.6\nPulling OlivierCoen/stableexpression ...\n Already-up-to-date\nLaunching `https://github.com/OlivierCoen/stableexpression` [infallible_sanger] DSL2 - revision: 139de1d916 [dev]\n\n-\u001b[2m----------------------------------------------------\u001b[0m-\n \u001b[0;32m,--.\u001b[0;30m/\u001b[0;32m,-.\u001b[0m\n\u001b[0;34m ___ __ __ __ ___ \u001b[0;32m/,-._.--~'\u001b[0m\n\u001b[0;34m |\\ | |__ __ / ` / \\ |__) |__ \u001b[0;33m} {\u001b[0m\n\u001b[0;34m | \\| | \\__, \\__/ | \\ |___ \u001b[0;32m\\`-._,-`-,\u001b[0m\n \u001b[0;32m`._,._,'\u001b[0m\n\u001b[0;35m nf-core/stableexpression 1.0dev\u001b[0m\n-\u001b[2m----------------------------------------------------\u001b[0m-\n\u001b[1mInput/output options\u001b[0m\n \u001b[0;34mspecies : \u001b[0;32msolanum tuberosum\u001b[0m\n \u001b[0;34moutdir : \u001b[0;32m./results\u001b[0m\n \u001b[0;34mdatasets : \u001b[0;32mrenamed_samplesheet.csv\u001b[0m\n \u001b[0;34mskip_fetch_eatlas_accessions: \u001b[0;32mtrue\u001b[0m\n\n\u001b[1mStatistics options\u001b[0m\n \u001b[0;34mks_pvalue_threshold : \u001b[0;32m0.0\u001b[0m\n\n\u001b[1mGeneric options\u001b[0m\n \u001b[0;34mmax_multiqc_email_size : \u001b[0;32m25.MB\u001b[0m\n \u001b[0;34mtrace_report_suffix : \u001b[0;32m2025-07-07_15-34-55\u001b[0m\n\n\u001b[1mCore Nextflow options\u001b[0m\n \u001b[0;34mrevision : \u001b[0;32mdev\u001b[0m\n \u001b[0;34mrunName : \u001b[0;32minfallible_sanger\u001b[0m\n \u001b[0;34mcontainerEngine : \u001b[0;32mapptainer\u001b[0m\n \u001b[0;34mlaunchDir : \u001b[0;32m/tmp/tmppbklvq_q/job_working_directory/000/6/working\u001b[0m\n \u001b[0;34mworkDir : \u001b[0;32m/tmp/tmppbklvq_q/job_working_directory/000/6/working/work\u001b[0m\n \u001b[0;34mprojectDir : \u001b[0;32m/home/olivier/.nextflow/assets/OlivierCoen/stableexpression\u001b[0m\n \u001b[0;34muserName : \u001b[0;32molivier\u001b[0m\n \u001b[0;34mprofile : \u001b[0;32mapptainer\u001b[0m\n \u001b[0;34mconfigFiles : \u001b[0;32m\u001b[0m\n\n!! Only displaying parameters that differ from the pipeline defaults !!\n-\u001b[2m----------------------------------------------------\u001b[0m-\n* The nf-core framework\n https://doi.org/10.1038/s41587-020-0439-x\n\n* Software dependencies\n https://github.com/nf-core/stableexpression/blob/main/CITATIONS.md\n\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_python_requests:8c6da05a2935a952 [cache /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity/community.wave.seqera.io-library-pandas_python_requests-8c6da05a2935a952.img]\n[fa/056a72] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (dataset_a07a6342-8e00-47d9-83d1-ebd4c14037e0)\n[07/8e178c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (dataset_34937997-5658-430c-b9d7-521063b31b61)\nPulling Apptainer image docker://community.wave.seqera.io/library/bioconductor-deseq2_r-base_r-optparse:c84cd7ffdb298fa7 [cache /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity/community.wave.seqera.io-library-bioconductor-deseq2_r-base_r-optparse-c84cd7ffdb298fa7.img]\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_pyarrow_python_scikit-learn:6f85e3c4d1706e81 [cache /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity/community.wave.seqera.io-library-pandas_pyarrow_python_scikit-learn-6f85e3c4d1706e81.img]\n[dd/9ac587] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (dataset_a07a6342-8e00-47d9-83d1-ebd4c14037e0)\n[18/8d0d97] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (dataset_34937997-5658-430c-b9d7-521063b31b61)\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_pyarrow_python_scipy:7cad0d297a717147 [cache /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity/community.wave.seqera.io-library-pandas_pyarrow_python_scipy-7cad0d297a717147.img]\n[4e/9c4b12] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (dataset_a07a6342-8e00-47d9-83d1-ebd4c14037e0)\n[58/0c2003] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (dataset_34937997-5658-430c-b9d7-521063b31b61)\n[f0/d63cd8] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (dataset_34937997-5658-430c-b9d7-521063b31b61)\nPulling Apptainer image docker://community.wave.seqera.io/library/polars_python:cab787b788e5eba7 [cache /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity/community.wave.seqera.io-library-polars_python-cab787b788e5eba7.img]\n[19/9b4dd5] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:MERGE_DATA\n[2b/8ceec2] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:GENE_STATISTICS\nPulling Apptainer image docker://quay.io/biocontainers/multiqc:1.27--pyhdfd78af_0 [cache /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity/quay.io-biocontainers-multiqc-1.27--pyhdfd78af_0.img]\n[d3/a84151] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:MULTIQC_WORKFLOW:MULTIQC\n-\u001b[0;35m[nf-core/stableexpression]\u001b[0;32m Pipeline completed successfully\u001b[0m-\n", + "tool_id": "nf_core_stableexpression", + "tool_stderr": "", + "tool_stdout": "N E X T F L O W ~ version 25.04.6\nPulling OlivierCoen/stableexpression ...\n Already-up-to-date\nLaunching `https://github.com/OlivierCoen/stableexpression` [infallible_sanger] DSL2 - revision: 139de1d916 [dev]\n\n-\u001b[2m----------------------------------------------------\u001b[0m-\n \u001b[0;32m,--.\u001b[0;30m/\u001b[0;32m,-.\u001b[0m\n\u001b[0;34m ___ __ __ __ ___ \u001b[0;32m/,-._.--~'\u001b[0m\n\u001b[0;34m |\\ | |__ __ / ` / \\ |__) |__ \u001b[0;33m} {\u001b[0m\n\u001b[0;34m | \\| | \\__, \\__/ | \\ |___ \u001b[0;32m\\`-._,-`-,\u001b[0m\n \u001b[0;32m`._,._,'\u001b[0m\n\u001b[0;35m nf-core/stableexpression 1.0dev\u001b[0m\n-\u001b[2m----------------------------------------------------\u001b[0m-\n\u001b[1mInput/output options\u001b[0m\n \u001b[0;34mspecies : \u001b[0;32msolanum tuberosum\u001b[0m\n \u001b[0;34moutdir : \u001b[0;32m./results\u001b[0m\n \u001b[0;34mdatasets : \u001b[0;32mrenamed_samplesheet.csv\u001b[0m\n \u001b[0;34mskip_fetch_eatlas_accessions: \u001b[0;32mtrue\u001b[0m\n\n\u001b[1mStatistics options\u001b[0m\n \u001b[0;34mks_pvalue_threshold : \u001b[0;32m0.0\u001b[0m\n\n\u001b[1mGeneric options\u001b[0m\n \u001b[0;34mmax_multiqc_email_size : \u001b[0;32m25.MB\u001b[0m\n \u001b[0;34mtrace_report_suffix : \u001b[0;32m2025-07-07_15-34-55\u001b[0m\n\n\u001b[1mCore Nextflow options\u001b[0m\n \u001b[0;34mrevision : \u001b[0;32mdev\u001b[0m\n \u001b[0;34mrunName : \u001b[0;32minfallible_sanger\u001b[0m\n \u001b[0;34mcontainerEngine : \u001b[0;32mapptainer\u001b[0m\n \u001b[0;34mlaunchDir : \u001b[0;32m/tmp/tmppbklvq_q/job_working_directory/000/6/working\u001b[0m\n \u001b[0;34mworkDir : \u001b[0;32m/tmp/tmppbklvq_q/job_working_directory/000/6/working/work\u001b[0m\n \u001b[0;34mprojectDir : \u001b[0;32m/home/olivier/.nextflow/assets/OlivierCoen/stableexpression\u001b[0m\n \u001b[0;34muserName : \u001b[0;32molivier\u001b[0m\n \u001b[0;34mprofile : \u001b[0;32mapptainer\u001b[0m\n \u001b[0;34mconfigFiles : \u001b[0;32m\u001b[0m\n\n!! Only displaying parameters that differ from the pipeline defaults !!\n-\u001b[2m----------------------------------------------------\u001b[0m-\n* The nf-core framework\n https://doi.org/10.1038/s41587-020-0439-x\n\n* Software dependencies\n https://github.com/nf-core/stableexpression/blob/main/CITATIONS.md\n\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_python_requests:8c6da05a2935a952 [cache /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity/community.wave.seqera.io-library-pandas_python_requests-8c6da05a2935a952.img]\n[fa/056a72] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (dataset_a07a6342-8e00-47d9-83d1-ebd4c14037e0)\n[07/8e178c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (dataset_34937997-5658-430c-b9d7-521063b31b61)\nPulling Apptainer image docker://community.wave.seqera.io/library/bioconductor-deseq2_r-base_r-optparse:c84cd7ffdb298fa7 [cache /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity/community.wave.seqera.io-library-bioconductor-deseq2_r-base_r-optparse-c84cd7ffdb298fa7.img]\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_pyarrow_python_scikit-learn:6f85e3c4d1706e81 [cache /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity/community.wave.seqera.io-library-pandas_pyarrow_python_scikit-learn-6f85e3c4d1706e81.img]\n[dd/9ac587] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (dataset_a07a6342-8e00-47d9-83d1-ebd4c14037e0)\n[18/8d0d97] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (dataset_34937997-5658-430c-b9d7-521063b31b61)\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_pyarrow_python_scipy:7cad0d297a717147 [cache /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity/community.wave.seqera.io-library-pandas_pyarrow_python_scipy-7cad0d297a717147.img]\n[4e/9c4b12] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (dataset_a07a6342-8e00-47d9-83d1-ebd4c14037e0)\n[58/0c2003] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (dataset_34937997-5658-430c-b9d7-521063b31b61)\n[f0/d63cd8] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (dataset_34937997-5658-430c-b9d7-521063b31b61)\nPulling Apptainer image docker://community.wave.seqera.io/library/polars_python:cab787b788e5eba7 [cache /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity/community.wave.seqera.io-library-polars_python-cab787b788e5eba7.img]\n[19/9b4dd5] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:MERGE_DATA\n[2b/8ceec2] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:GENE_STATISTICS\nPulling Apptainer image docker://quay.io/biocontainers/multiqc:1.27--pyhdfd78af_0 [cache /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity/quay.io-biocontainers-multiqc-1.27--pyhdfd78af_0.img]\n[d3/a84151] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:MULTIQC_WORKFLOW:MULTIQC\n-\u001b[0;35m[nf-core/stableexpression]\u001b[0;32m Pipeline completed successfully\u001b[0m-\n", + "update_time": "2025-07-07T13:35:48.617986", + "user_email": "planemo@galaxyproject.org", + "user_id": "f14d691d132db03a" + }, + "status": "success", + "test_index": 0, + "time_seconds": 79.66745114326477, + "tool_id": "nf_core_stableexpression", + "tool_version": "1.0dev" + }, + "has_data": true, + "id": "nf_core_stableexpression-0" + }, + { + "data": { + "inputs": { + "input_output_options|species": "solanum tuberosum" + }, + "job": { + "command_line": "echo \"No user dataset provided\" && nextflow run OlivierCoen/stableexpression -r dev -latest -profile apptainer -ansi-log false --outdir ./results --species \"solanum tuberosum\" --normalisation_method \"deseq2\" --nb_top_gene_candidates 1000 --ks_pvalue_threshold 0.0", + "command_version": "1.0dev", + "copied_from_job_id": null, + "create_time": "2025-07-07T13:35:49.408354", + "dependencies": [ + { + "cacheable": false, + "dependency_resolver": { + "auto_init": true, + "auto_install": true, + "can_uninstall_dependencies": true, + "ensure_channels": "conda-forge,bioconda", + "model_class": "CondaDependencyResolver", + "prefix": "/home/olivier/miniconda3", + "read_only": false, + "resolver_type": "conda", + "resolves_simple_dependencies": true, + "use_local": false, + "versionless": false + }, + "dependency_type": "conda", + "environment_path": "/home/olivier/miniconda3/envs/mulled-v1-07f6cfb22b452fc0be855ecba9cd73b57bf68b9e0f328b7d8a5bb8363ec2f8bf", + "exact": true, + "model_class": "MergedCondaDependency", + "name": "nextflow", + "version": "25.04.6" + }, + { + "cacheable": false, + "dependency_resolver": { + "auto_init": true, + "auto_install": true, + "can_uninstall_dependencies": true, + "ensure_channels": "conda-forge,bioconda", + "model_class": "CondaDependencyResolver", + "prefix": "/home/olivier/miniconda3", + "read_only": false, + "resolver_type": "conda", + "resolves_simple_dependencies": true, + "use_local": false, + "versionless": false + }, + "dependency_type": "conda", + "environment_path": "/home/olivier/miniconda3/envs/mulled-v1-07f6cfb22b452fc0be855ecba9cd73b57bf68b9e0f328b7d8a5bb8363ec2f8bf", + "exact": true, + "model_class": "MergedCondaDependency", + "name": "apptainer", + "version": "1.4.1" + }, + { + "cacheable": false, + "dependency_resolver": { + "auto_init": true, + "auto_install": true, + "can_uninstall_dependencies": true, + "ensure_channels": "conda-forge,bioconda", + "model_class": "CondaDependencyResolver", + "prefix": "/home/olivier/miniconda3", + "read_only": false, + "resolver_type": "conda", + "resolves_simple_dependencies": true, + "use_local": false, + "versionless": false + }, + "dependency_type": "conda", + "environment_path": "/home/olivier/miniconda3/envs/mulled-v1-07f6cfb22b452fc0be855ecba9cd73b57bf68b9e0f328b7d8a5bb8363ec2f8bf", + "exact": true, + "model_class": "MergedCondaDependency", + "name": "openjdk", + "version": "23.0.2" + } + ], + "exit_code": 0, + "external_id": "2198635", + "galaxy_version": "25.0", + "handler": null, + "history_id": "d77c1c1553d8ae81", + "id": "4e217783f5b35092", + "inputs": {}, + "job_messages": [], + "job_metrics": [], + "job_runner_name": null, + "job_stderr": "", + "job_stdout": "", + "model_class": "Job", + "output_collections": {}, + "outputs": { + "multiqc_report": { + "id": "2f686cc81a8947ff", + "src": "hda", + "uuid": "f802e5f0-25e1-486a-b305-4426ff321221" + }, + "top_stable_genes_summary": { + "id": "74b2595b34860659", + "src": "hda", + "uuid": "c8db12f8-d089-425b-8d7f-0cebf8c352a5" + } + }, + "params": { + "__input_ext": "\"data\"", + "chromInfo": "\"/tmp/tmppbklvq_q/galaxy-dev/tool-data/shared/ucsc/chrom/?.len\"", + "dbkey": "\"?\"", + "expression_atlas_options": "{\"accessions_only\": false, \"eatlas_accessions\": null, \"eatlas_accessions_file\": null, \"eatlas_keywords\": null, \"exclude_eatlas_accessions\": null, \"exclude_eatlas_accessions_file\": null}", + "idmapping_options": "{\"gene_id_mapping_file\": null, \"gene_metadata\": null, \"skip_gprofiler\": false}", + "input_output_options": "{\"datasets\": {\"__current_case__\": 1, \"provide_datasets\": false}, \"skip_fetch_eatlas_accessions\": false, \"species\": \"solanum tuberosum\"}", + "statistical_options": "{\"ks_pvalue_threshold\": \"0.0\", \"nb_top_gene_candidates\": \"1000\", \"normalisation_method\": \"deseq2\"}" + }, + "state": "ok", + "stderr": "", + "stdout": "No user dataset provided\nN E X T F L O W ~ version 25.04.6\nPulling OlivierCoen/stableexpression ...\n Already-up-to-date\nLaunching `https://github.com/OlivierCoen/stableexpression` [intergalactic_pauling] DSL2 - revision: 139de1d916 [dev]\n\n-\u001b[2m----------------------------------------------------\u001b[0m-\n \u001b[0;32m,--.\u001b[0;30m/\u001b[0;32m,-.\u001b[0m\n\u001b[0;34m ___ __ __ __ ___ \u001b[0;32m/,-._.--~'\u001b[0m\n\u001b[0;34m |\\ | |__ __ / ` / \\ |__) |__ \u001b[0;33m} {\u001b[0m\n\u001b[0;34m | \\| | \\__, \\__/ | \\ |___ \u001b[0;32m\\`-._,-`-,\u001b[0m\n \u001b[0;32m`._,._,'\u001b[0m\n\u001b[0;35m nf-core/stableexpression 1.0dev\u001b[0m\n-\u001b[2m----------------------------------------------------\u001b[0m-\n\u001b[1mInput/output options\u001b[0m\n \u001b[0;34mspecies : \u001b[0;32msolanum tuberosum\u001b[0m\n \u001b[0;34moutdir : \u001b[0;32m./results\u001b[0m\n\n\u001b[1mStatistics options\u001b[0m\n \u001b[0;34mks_pvalue_threshold : \u001b[0;32m0.0\u001b[0m\n\n\u001b[1mGeneric options\u001b[0m\n \u001b[0;34mmax_multiqc_email_size: \u001b[0;32m25.MB\u001b[0m\n \u001b[0;34mtrace_report_suffix : \u001b[0;32m2025-07-07_15-35-57\u001b[0m\n\n\u001b[1mCore Nextflow options\u001b[0m\n \u001b[0;34mrevision : \u001b[0;32mdev\u001b[0m\n \u001b[0;34mrunName : \u001b[0;32mintergalactic_pauling\u001b[0m\n \u001b[0;34mcontainerEngine : \u001b[0;32mapptainer\u001b[0m\n \u001b[0;34mlaunchDir : \u001b[0;32m/tmp/tmppbklvq_q/job_working_directory/000/7/working\u001b[0m\n \u001b[0;34mworkDir : \u001b[0;32m/tmp/tmppbklvq_q/job_working_directory/000/7/working/work\u001b[0m\n \u001b[0;34mprojectDir : \u001b[0;32m/home/olivier/.nextflow/assets/OlivierCoen/stableexpression\u001b[0m\n \u001b[0;34muserName : \u001b[0;32molivier\u001b[0m\n \u001b[0;34mprofile : \u001b[0;32mapptainer\u001b[0m\n \u001b[0;34mconfigFiles : \u001b[0;32m\u001b[0m\n\n!! Only displaying parameters that differ from the pipeline defaults !!\n-\u001b[2m----------------------------------------------------\u001b[0m-\n* The nf-core framework\n https://doi.org/10.1038/s41587-020-0439-x\n\n* Software dependencies\n https://github.com/nf-core/stableexpression/blob/main/CITATIONS.md\n\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/nltk_pandas_python_pyyaml_pruned:2218f9c10723fbf3 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-nltk_pandas_python_pyyaml_pruned-2218f9c10723fbf3.img]\n[1f/c49a85] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETACCESSIONS\nPulling Apptainer image docker://community.wave.seqera.io/library/bioconductor-expressionatlas_r-base_r-optparse:ca0f8cd9d3f44af9 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-bioconductor-expressionatlas_r-base_r-optparse-ca0f8cd9d3f44af9.img]\n[9a/9a4cd2] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-4251)\n[f9/c5afe9] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-7711)\n[ff/ca3fdb] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-4301)\n[cf/054e1d] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-5038)\n[b9/4a7438] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-552)\n[e0/6636bb] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-GEOD-61690)\n[87/3dd640] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-GEOD-77826)\n[36/e011ea] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-5215)\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_python_requests:8c6da05a2935a952 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-pandas_python_requests-8c6da05a2935a952.img]\n[34/4b621f] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_GEOD_77826_rnaseq)\n[59/26bbb6] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_4251_rnaseq)\n[d3/ba509a] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_7711_rnaseq)\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/bioconductor-deseq2_r-base_r-optparse:c84cd7ffdb298fa7 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-bioconductor-deseq2_r-base_r-optparse-c84cd7ffdb298fa7.img]\n[b4/3cb17c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_5038_rnaseq)\n[af/419079] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_GEOD_61690_rnaseq)\n[31/91c25c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_4301_rnaseq)\n[20/eb8d95] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_5215_rnaseq)\n[9f/9131f9] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_552_rnaseq)\n[04/09f0f7] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_GEOD_77826_rnaseq)\n[22/23cbcf] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_4251_rnaseq)\n[a5/75e84d] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_7711_rnaseq)\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_pyarrow_python_scikit-learn:6f85e3c4d1706e81 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-pandas_pyarrow_python_scikit-learn-6f85e3c4d1706e81.img]\n[c8/f80f4e] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_5038_rnaseq)\n[a5/6663fb] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_GEOD_61690_rnaseq)\n[31/a97a9d] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_4301_rnaseq)\n[5e/e3acd4] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_5215_rnaseq)\n[ce/aee630] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_552_rnaseq)\n[e6/616f1a] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_4251_rnaseq)\n[63/3c20fe] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_GEOD_77826_rnaseq)\n[0b/88927c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_7711_rnaseq)\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_pyarrow_python_scipy:7cad0d297a717147 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-pandas_pyarrow_python_scipy-7cad0d297a717147.img]\n[27/d62eca] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_5038_rnaseq)\n[28/3ddfd3] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_GEOD_61690_rnaseq)\n[4c/314be3] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_4301_rnaseq)\n[aa/b72868] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_5215_rnaseq)\n[33/301a93] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_552_rnaseq)\n[48/e9e634] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_4251_rnaseq)\n[de/22b91c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_GEOD_77826_rnaseq)\n[02/28863b] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_7711_rnaseq)\n[70/48c1c4] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_5038_rnaseq)\n[d6/3320ec] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_GEOD_61690_rnaseq)\n[ac/3b0515] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_4301_rnaseq)\n[88/5c00b6] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_5215_rnaseq)\n[c7/15b347] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_552_rnaseq)\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/polars_python:cab787b788e5eba7 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-polars_python-cab787b788e5eba7.img]\n[f8/fa4aca] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:MERGE_DATA\n[2c/a6954a] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:GENE_STATISTICS\nPulling Apptainer image docker://quay.io/biocontainers/multiqc:1.27--pyhdfd78af_0 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/quay.io-biocontainers-multiqc-1.27--pyhdfd78af_0.img]\n[5c/c1a568] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:MULTIQC_WORKFLOW:MULTIQC\n-\u001b[0;35m[nf-core/stableexpression]\u001b[0;32m Pipeline completed successfully\u001b[0m-\n", + "tool_id": "nf_core_stableexpression", + "tool_stderr": "", + "tool_stdout": "No user dataset provided\nN E X T F L O W ~ version 25.04.6\nPulling OlivierCoen/stableexpression ...\n Already-up-to-date\nLaunching `https://github.com/OlivierCoen/stableexpression` [intergalactic_pauling] DSL2 - revision: 139de1d916 [dev]\n\n-\u001b[2m----------------------------------------------------\u001b[0m-\n \u001b[0;32m,--.\u001b[0;30m/\u001b[0;32m,-.\u001b[0m\n\u001b[0;34m ___ __ __ __ ___ \u001b[0;32m/,-._.--~'\u001b[0m\n\u001b[0;34m |\\ | |__ __ / ` / \\ |__) |__ \u001b[0;33m} {\u001b[0m\n\u001b[0;34m | \\| | \\__, \\__/ | \\ |___ \u001b[0;32m\\`-._,-`-,\u001b[0m\n \u001b[0;32m`._,._,'\u001b[0m\n\u001b[0;35m nf-core/stableexpression 1.0dev\u001b[0m\n-\u001b[2m----------------------------------------------------\u001b[0m-\n\u001b[1mInput/output options\u001b[0m\n \u001b[0;34mspecies : \u001b[0;32msolanum tuberosum\u001b[0m\n \u001b[0;34moutdir : \u001b[0;32m./results\u001b[0m\n\n\u001b[1mStatistics options\u001b[0m\n \u001b[0;34mks_pvalue_threshold : \u001b[0;32m0.0\u001b[0m\n\n\u001b[1mGeneric options\u001b[0m\n \u001b[0;34mmax_multiqc_email_size: \u001b[0;32m25.MB\u001b[0m\n \u001b[0;34mtrace_report_suffix : \u001b[0;32m2025-07-07_15-35-57\u001b[0m\n\n\u001b[1mCore Nextflow options\u001b[0m\n \u001b[0;34mrevision : \u001b[0;32mdev\u001b[0m\n \u001b[0;34mrunName : \u001b[0;32mintergalactic_pauling\u001b[0m\n \u001b[0;34mcontainerEngine : \u001b[0;32mapptainer\u001b[0m\n \u001b[0;34mlaunchDir : \u001b[0;32m/tmp/tmppbklvq_q/job_working_directory/000/7/working\u001b[0m\n \u001b[0;34mworkDir : \u001b[0;32m/tmp/tmppbklvq_q/job_working_directory/000/7/working/work\u001b[0m\n \u001b[0;34mprojectDir : \u001b[0;32m/home/olivier/.nextflow/assets/OlivierCoen/stableexpression\u001b[0m\n \u001b[0;34muserName : \u001b[0;32molivier\u001b[0m\n \u001b[0;34mprofile : \u001b[0;32mapptainer\u001b[0m\n \u001b[0;34mconfigFiles : \u001b[0;32m\u001b[0m\n\n!! Only displaying parameters that differ from the pipeline defaults !!\n-\u001b[2m----------------------------------------------------\u001b[0m-\n* The nf-core framework\n https://doi.org/10.1038/s41587-020-0439-x\n\n* Software dependencies\n https://github.com/nf-core/stableexpression/blob/main/CITATIONS.md\n\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/nltk_pandas_python_pyyaml_pruned:2218f9c10723fbf3 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-nltk_pandas_python_pyyaml_pruned-2218f9c10723fbf3.img]\n[1f/c49a85] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETACCESSIONS\nPulling Apptainer image docker://community.wave.seqera.io/library/bioconductor-expressionatlas_r-base_r-optparse:ca0f8cd9d3f44af9 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-bioconductor-expressionatlas_r-base_r-optparse-ca0f8cd9d3f44af9.img]\n[9a/9a4cd2] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-4251)\n[f9/c5afe9] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-7711)\n[ff/ca3fdb] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-4301)\n[cf/054e1d] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-5038)\n[b9/4a7438] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-552)\n[e0/6636bb] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-GEOD-61690)\n[87/3dd640] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-GEOD-77826)\n[36/e011ea] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-5215)\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_python_requests:8c6da05a2935a952 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-pandas_python_requests-8c6da05a2935a952.img]\n[34/4b621f] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_GEOD_77826_rnaseq)\n[59/26bbb6] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_4251_rnaseq)\n[d3/ba509a] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_7711_rnaseq)\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/bioconductor-deseq2_r-base_r-optparse:c84cd7ffdb298fa7 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-bioconductor-deseq2_r-base_r-optparse-c84cd7ffdb298fa7.img]\n[b4/3cb17c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_5038_rnaseq)\n[af/419079] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_GEOD_61690_rnaseq)\n[31/91c25c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_4301_rnaseq)\n[20/eb8d95] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_5215_rnaseq)\n[9f/9131f9] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_552_rnaseq)\n[04/09f0f7] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_GEOD_77826_rnaseq)\n[22/23cbcf] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_4251_rnaseq)\n[a5/75e84d] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_7711_rnaseq)\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_pyarrow_python_scikit-learn:6f85e3c4d1706e81 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-pandas_pyarrow_python_scikit-learn-6f85e3c4d1706e81.img]\n[c8/f80f4e] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_5038_rnaseq)\n[a5/6663fb] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_GEOD_61690_rnaseq)\n[31/a97a9d] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_4301_rnaseq)\n[5e/e3acd4] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_5215_rnaseq)\n[ce/aee630] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_552_rnaseq)\n[e6/616f1a] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_4251_rnaseq)\n[63/3c20fe] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_GEOD_77826_rnaseq)\n[0b/88927c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_7711_rnaseq)\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_pyarrow_python_scipy:7cad0d297a717147 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-pandas_pyarrow_python_scipy-7cad0d297a717147.img]\n[27/d62eca] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_5038_rnaseq)\n[28/3ddfd3] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_GEOD_61690_rnaseq)\n[4c/314be3] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_4301_rnaseq)\n[aa/b72868] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_5215_rnaseq)\n[33/301a93] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_552_rnaseq)\n[48/e9e634] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_4251_rnaseq)\n[de/22b91c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_GEOD_77826_rnaseq)\n[02/28863b] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_7711_rnaseq)\n[70/48c1c4] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_5038_rnaseq)\n[d6/3320ec] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_GEOD_61690_rnaseq)\n[ac/3b0515] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_4301_rnaseq)\n[88/5c00b6] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_5215_rnaseq)\n[c7/15b347] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_552_rnaseq)\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/polars_python:cab787b788e5eba7 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-polars_python-cab787b788e5eba7.img]\n[f8/fa4aca] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:MERGE_DATA\n[2c/a6954a] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:GENE_STATISTICS\nPulling Apptainer image docker://quay.io/biocontainers/multiqc:1.27--pyhdfd78af_0 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/quay.io-biocontainers-multiqc-1.27--pyhdfd78af_0.img]\n[5c/c1a568] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:MULTIQC_WORKFLOW:MULTIQC\n-\u001b[0;35m[nf-core/stableexpression]\u001b[0;32m Pipeline completed successfully\u001b[0m-\n", + "update_time": "2025-07-07T13:40:17.207952", + "user_email": "planemo@galaxyproject.org", + "user_id": "f14d691d132db03a" + }, + "status": "success", + "test_index": 1, + "time_seconds": 268.3443777561188, + "tool_id": "nf_core_stableexpression", + "tool_version": "1.0dev" + }, + "has_data": true, + "id": "nf_core_stableexpression-1" + }, + { + "data": { + "inputs": { + "input_output_options|datasets|count_datasets": [ + { + "id": "1749056fc9e975ac", + "src": "hda" + }, + { + "id": "ace316e9ce92c65a", + "src": "hda" + } + ], + "input_output_options|datasets|experimental_designs": [ + { + "id": "093c5f960045e43a", + "src": "hda" + }, + { + "id": "063a55387997889b", + "src": "hda" + } + ], + "input_output_options|datasets|provide_datasets": true, + "input_output_options|datasets|samplesheet": { + "id": "e7b79b392ebf8da8", + "src": "hda" + }, + "input_output_options|species": "solanum tuberosum" + }, + "job": { + "command_line": "python '/home/olivier/repositories/nf-core-stableexpression/galaxy/tool/rebuild_samplesheet.py' --in /tmp/tmppbklvq_q/files/7/6/2/dataset_762f4e43-a746-4274-9f6a-ce01101735cd.dat --out renamed_samplesheet.csv --count-files /tmp/tmppbklvq_q/files/6/9/d/dataset_69d3f92c-2be0-4deb-a415-fea0c6b418d1.dat,/tmp/tmppbklvq_q/files/3/6/7/dataset_3671e188-4fd4-41ab-ad36-517ad81c4465.dat --count-filenames microarray.normalised.csv rnaseq.raw.csv --design-files /tmp/tmppbklvq_q/files/e/f/9/dataset_ef9df148-00cb-485d-b28e-6caf887a48e6.dat,/tmp/tmppbklvq_q/files/8/6/3/dataset_863e9d57-3c97-4a70-b305-688a48c3642d.dat --design-filenames microarray.normalised.design.csv rnaseq.raw.design.csv && nextflow run OlivierCoen/stableexpression -r dev -latest -profile apptainer -ansi-log false --outdir ./results --species \"solanum tuberosum\" --datasets renamed_samplesheet.csv --normalisation_method \"deseq2\" --nb_top_gene_candidates 1000 --ks_pvalue_threshold 0.0", + "command_version": "1.0dev", + "copied_from_job_id": null, + "create_time": "2025-07-07T13:40:34.899637", + "dependencies": [ + { + "cacheable": false, + "dependency_resolver": { + "auto_init": true, + "auto_install": true, + "can_uninstall_dependencies": true, + "ensure_channels": "conda-forge,bioconda", + "model_class": "CondaDependencyResolver", + "prefix": "/home/olivier/miniconda3", + "read_only": false, + "resolver_type": "conda", + "resolves_simple_dependencies": true, + "use_local": false, + "versionless": false + }, + "dependency_type": "conda", + "environment_path": "/home/olivier/miniconda3/envs/mulled-v1-07f6cfb22b452fc0be855ecba9cd73b57bf68b9e0f328b7d8a5bb8363ec2f8bf", + "exact": true, + "model_class": "MergedCondaDependency", + "name": "nextflow", + "version": "25.04.6" + }, + { + "cacheable": false, + "dependency_resolver": { + "auto_init": true, + "auto_install": true, + "can_uninstall_dependencies": true, + "ensure_channels": "conda-forge,bioconda", + "model_class": "CondaDependencyResolver", + "prefix": "/home/olivier/miniconda3", + "read_only": false, + "resolver_type": "conda", + "resolves_simple_dependencies": true, + "use_local": false, + "versionless": false + }, + "dependency_type": "conda", + "environment_path": "/home/olivier/miniconda3/envs/mulled-v1-07f6cfb22b452fc0be855ecba9cd73b57bf68b9e0f328b7d8a5bb8363ec2f8bf", + "exact": true, + "model_class": "MergedCondaDependency", + "name": "apptainer", + "version": "1.4.1" + }, + { + "cacheable": false, + "dependency_resolver": { + "auto_init": true, + "auto_install": true, + "can_uninstall_dependencies": true, + "ensure_channels": "conda-forge,bioconda", + "model_class": "CondaDependencyResolver", + "prefix": "/home/olivier/miniconda3", + "read_only": false, + "resolver_type": "conda", + "resolves_simple_dependencies": true, + "use_local": false, + "versionless": false + }, + "dependency_type": "conda", + "environment_path": "/home/olivier/miniconda3/envs/mulled-v1-07f6cfb22b452fc0be855ecba9cd73b57bf68b9e0f328b7d8a5bb8363ec2f8bf", + "exact": true, + "model_class": "MergedCondaDependency", + "name": "openjdk", + "version": "23.0.2" + } + ], + "exit_code": 0, + "external_id": "2239136", + "galaxy_version": "25.0", + "handler": null, + "history_id": "8f49e6aa49bd8f4e", + "id": "093c5f960045e43a", + "inputs": { + "input_output_options|datasets|count_datasets": { + "id": "1749056fc9e975ac", + "src": "hda", + "uuid": "69d3f92c-2be0-4deb-a415-fea0c6b418d1" + }, + "input_output_options|datasets|count_datasets1": { + "id": "1749056fc9e975ac", + "src": "hda", + "uuid": "69d3f92c-2be0-4deb-a415-fea0c6b418d1" + }, + "input_output_options|datasets|count_datasets2": { + "id": "ace316e9ce92c65a", + "src": "hda", + "uuid": "3671e188-4fd4-41ab-ad36-517ad81c4465" + }, + "input_output_options|datasets|experimental_designs": { + "id": "093c5f960045e43a", + "src": "hda", + "uuid": "ef9df148-00cb-485d-b28e-6caf887a48e6" + }, + "input_output_options|datasets|experimental_designs1": { + "id": "093c5f960045e43a", + "src": "hda", + "uuid": "ef9df148-00cb-485d-b28e-6caf887a48e6" + }, + "input_output_options|datasets|experimental_designs2": { + "id": "063a55387997889b", + "src": "hda", + "uuid": "863e9d57-3c97-4a70-b305-688a48c3642d" + }, + "input_output_options|datasets|samplesheet": { + "id": "e7b79b392ebf8da8", + "src": "hda", + "uuid": "762f4e43-a746-4274-9f6a-ce01101735cd" + } + }, + "job_messages": [], + "job_metrics": [], + "job_runner_name": null, + "job_stderr": "", + "job_stdout": "", + "model_class": "Job", + "output_collections": {}, + "outputs": { + "multiqc_report": { + "id": "c5287a5f0f3a1faf", + "src": "hda", + "uuid": "86ef096d-8ff5-4e37-a58e-169dd3c38c49" + }, + "top_stable_genes_summary": { + "id": "60efb04c06cc3091", + "src": "hda", + "uuid": "d2ab28b3-8f62-4bba-9eb5-322e16024164" + } + }, + "params": { + "__input_ext": "\"txt\"", + "chromInfo": "\"/tmp/tmppbklvq_q/galaxy-dev/tool-data/shared/ucsc/chrom/?.len\"", + "dbkey": "\"?\"", + "expression_atlas_options": "{\"accessions_only\": false, \"eatlas_accessions\": null, \"eatlas_accessions_file\": null, \"eatlas_keywords\": null, \"exclude_eatlas_accessions\": null, \"exclude_eatlas_accessions_file\": null}", + "idmapping_options": "{\"gene_id_mapping_file\": null, \"gene_metadata\": null, \"skip_gprofiler\": false}", + "input_output_options": "{\"datasets\": {\"__current_case__\": 0, \"count_datasets\": {\"values\": [{\"id\": 11, \"src\": \"hda\"}, {\"id\": 12, \"src\": \"hda\"}]}, \"experimental_designs\": {\"values\": [{\"id\": 13, \"src\": \"hda\"}, {\"id\": 14, \"src\": \"hda\"}]}, \"provide_datasets\": true, \"samplesheet\": {\"values\": [{\"id\": 10, \"src\": \"hda\"}]}}, \"skip_fetch_eatlas_accessions\": false, \"species\": \"solanum tuberosum\"}", + "statistical_options": "{\"ks_pvalue_threshold\": \"0.0\", \"nb_top_gene_candidates\": \"1000\", \"normalisation_method\": \"deseq2\"}" + }, + "state": "ok", + "stderr": "", + "stdout": "N E X T F L O W ~ version 25.04.6\nPulling OlivierCoen/stableexpression ...\n Already-up-to-date\nLaunching `https://github.com/OlivierCoen/stableexpression` [grave_hodgkin] DSL2 - revision: 139de1d916 [dev]\n\n-\u001b[2m----------------------------------------------------\u001b[0m-\n \u001b[0;32m,--.\u001b[0;30m/\u001b[0;32m,-.\u001b[0m\n\u001b[0;34m ___ __ __ __ ___ \u001b[0;32m/,-._.--~'\u001b[0m\n\u001b[0;34m |\\ | |__ __ / ` / \\ |__) |__ \u001b[0;33m} {\u001b[0m\n\u001b[0;34m | \\| | \\__, \\__/ | \\ |___ \u001b[0;32m\\`-._,-`-,\u001b[0m\n \u001b[0;32m`._,._,'\u001b[0m\n\u001b[0;35m nf-core/stableexpression 1.0dev\u001b[0m\n-\u001b[2m----------------------------------------------------\u001b[0m-\n\u001b[1mInput/output options\u001b[0m\n \u001b[0;34mspecies : \u001b[0;32msolanum tuberosum\u001b[0m\n \u001b[0;34moutdir : \u001b[0;32m./results\u001b[0m\n \u001b[0;34mdatasets : \u001b[0;32mrenamed_samplesheet.csv\u001b[0m\n\n\u001b[1mStatistics options\u001b[0m\n \u001b[0;34mks_pvalue_threshold : \u001b[0;32m0.0\u001b[0m\n\n\u001b[1mGeneric options\u001b[0m\n \u001b[0;34mmax_multiqc_email_size: \u001b[0;32m25.MB\u001b[0m\n \u001b[0;34mtrace_report_suffix : \u001b[0;32m2025-07-07_15-40-39\u001b[0m\n\n\u001b[1mCore Nextflow options\u001b[0m\n \u001b[0;34mrevision : \u001b[0;32mdev\u001b[0m\n \u001b[0;34mrunName : \u001b[0;32mgrave_hodgkin\u001b[0m\n \u001b[0;34mcontainerEngine : \u001b[0;32mapptainer\u001b[0m\n \u001b[0;34mlaunchDir : \u001b[0;32m/tmp/tmppbklvq_q/job_working_directory/000/13/working\u001b[0m\n \u001b[0;34mworkDir : \u001b[0;32m/tmp/tmppbklvq_q/job_working_directory/000/13/working/work\u001b[0m\n \u001b[0;34mprojectDir : \u001b[0;32m/home/olivier/.nextflow/assets/OlivierCoen/stableexpression\u001b[0m\n \u001b[0;34muserName : \u001b[0;32molivier\u001b[0m\n \u001b[0;34mprofile : \u001b[0;32mapptainer\u001b[0m\n \u001b[0;34mconfigFiles : \u001b[0;32m\u001b[0m\n\n!! Only displaying parameters that differ from the pipeline defaults !!\n-\u001b[2m----------------------------------------------------\u001b[0m-\n* The nf-core framework\n https://doi.org/10.1038/s41587-020-0439-x\n\n* Software dependencies\n https://github.com/nf-core/stableexpression/blob/main/CITATIONS.md\n\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/nltk_pandas_python_pyyaml_pruned:2218f9c10723fbf3 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-nltk_pandas_python_pyyaml_pruned-2218f9c10723fbf3.img]\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_python_requests:8c6da05a2935a952 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-pandas_python_requests-8c6da05a2935a952.img]\n[86/9fbf39] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (dataset_3671e188-4fd4-41ab-ad36-517ad81c4465)\n[e5/47d907] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (dataset_69d3f92c-2be0-4deb-a415-fea0c6b418d1)\n[87/da389d] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETACCESSIONS\nPulling Apptainer image docker://community.wave.seqera.io/library/bioconductor-deseq2_r-base_r-optparse:c84cd7ffdb298fa7 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-bioconductor-deseq2_r-base_r-optparse-c84cd7ffdb298fa7.img]\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_pyarrow_python_scikit-learn:6f85e3c4d1706e81 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-pandas_pyarrow_python_scikit-learn-6f85e3c4d1706e81.img]\n[51/7cb214] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (dataset_69d3f92c-2be0-4deb-a415-fea0c6b418d1)\n[30/76ffa2] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (dataset_3671e188-4fd4-41ab-ad36-517ad81c4465)\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_pyarrow_python_scipy:7cad0d297a717147 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-pandas_pyarrow_python_scipy-7cad0d297a717147.img]\n[8b/cb4e47] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (dataset_69d3f92c-2be0-4deb-a415-fea0c6b418d1)\nPulling Apptainer image docker://community.wave.seqera.io/library/bioconductor-expressionatlas_r-base_r-optparse:ca0f8cd9d3f44af9 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-bioconductor-expressionatlas_r-base_r-optparse-ca0f8cd9d3f44af9.img]\n[50/62910a] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-552)\n[99/0032bc] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-4251)\n[b0/fea57e] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-4301)\n[5d/78028e] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-GEOD-61690)\n[b3/c3b0b4] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-GEOD-77826)\n[ef/e42508] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-5038)\n[bd/3fa292] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-7711)\n[8f/a81318] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-5215)\n[51/2fe86b] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_4301_rnaseq)\n[fb/1074e5] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_552_rnaseq)\n[43/8d985d] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_GEOD_61690_rnaseq)\n[92/138d42] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_GEOD_77826_rnaseq)\n[db/dd3fbf] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_4251_rnaseq)\n[44/dc08c9] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_5038_rnaseq)\n[0c/f4f46e] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_5215_rnaseq)\n[cd/61f87c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_7711_rnaseq)\n[65/24c205] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_552_rnaseq)\n[f6/f1cf3f] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_4301_rnaseq)\n[9b/fe7347] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_GEOD_61690_rnaseq)\n[ed/bd7ad2] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_GEOD_77826_rnaseq)\n[47/b13d0c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_4251_rnaseq)\n[c7/3de644] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_5038_rnaseq)\n[26/a49898] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_5215_rnaseq)\n[5c/fa0cee] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_7711_rnaseq)\n[2b/6ee60b] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (dataset_3671e188-4fd4-41ab-ad36-517ad81c4465)\n[b2/a196a7] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_552_rnaseq)\n[41/7dc9e1] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_4301_rnaseq)\n[01/673e26] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_GEOD_61690_rnaseq)\n[b8/8fb0eb] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_GEOD_77826_rnaseq)\n[30/ee19b6] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_4251_rnaseq)\n[f9/f2b9b6] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_5038_rnaseq)\n[ca/18206e] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_5215_rnaseq)\n[22/7e80e7] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_7711_rnaseq)\n[bc/d70f60] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (dataset_3671e188-4fd4-41ab-ad36-517ad81c4465)\n[58/2d50b7] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_552_rnaseq)\n[1e/aeb2b1] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_4301_rnaseq)\n[29/8ca339] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_GEOD_61690_rnaseq)\n[af/164cba] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_GEOD_77826_rnaseq)\n[77/7abd5c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_4251_rnaseq)\n[20/cc6ed1] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_5038_rnaseq)\n[77/61b951] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_5215_rnaseq)\n[21/f6cbd4] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_7711_rnaseq)\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/polars_python:cab787b788e5eba7 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-polars_python-cab787b788e5eba7.img]\n[f7/b8e320] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:MERGE_DATA\n[cb/7f5612] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:GENE_STATISTICS\nPulling Apptainer image docker://quay.io/biocontainers/multiqc:1.27--pyhdfd78af_0 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/quay.io-biocontainers-multiqc-1.27--pyhdfd78af_0.img]\n[61/43e626] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:MULTIQC_WORKFLOW:MULTIQC\n-\u001b[0;35m[nf-core/stableexpression]\u001b[0;32m Pipeline completed successfully\u001b[0m-\n", + "tool_id": "nf_core_stableexpression", + "tool_stderr": "", + "tool_stdout": "N E X T F L O W ~ version 25.04.6\nPulling OlivierCoen/stableexpression ...\n Already-up-to-date\nLaunching `https://github.com/OlivierCoen/stableexpression` [grave_hodgkin] DSL2 - revision: 139de1d916 [dev]\n\n-\u001b[2m----------------------------------------------------\u001b[0m-\n \u001b[0;32m,--.\u001b[0;30m/\u001b[0;32m,-.\u001b[0m\n\u001b[0;34m ___ __ __ __ ___ \u001b[0;32m/,-._.--~'\u001b[0m\n\u001b[0;34m |\\ | |__ __ / ` / \\ |__) |__ \u001b[0;33m} {\u001b[0m\n\u001b[0;34m | \\| | \\__, \\__/ | \\ |___ \u001b[0;32m\\`-._,-`-,\u001b[0m\n \u001b[0;32m`._,._,'\u001b[0m\n\u001b[0;35m nf-core/stableexpression 1.0dev\u001b[0m\n-\u001b[2m----------------------------------------------------\u001b[0m-\n\u001b[1mInput/output options\u001b[0m\n \u001b[0;34mspecies : \u001b[0;32msolanum tuberosum\u001b[0m\n \u001b[0;34moutdir : \u001b[0;32m./results\u001b[0m\n \u001b[0;34mdatasets : \u001b[0;32mrenamed_samplesheet.csv\u001b[0m\n\n\u001b[1mStatistics options\u001b[0m\n \u001b[0;34mks_pvalue_threshold : \u001b[0;32m0.0\u001b[0m\n\n\u001b[1mGeneric options\u001b[0m\n \u001b[0;34mmax_multiqc_email_size: \u001b[0;32m25.MB\u001b[0m\n \u001b[0;34mtrace_report_suffix : \u001b[0;32m2025-07-07_15-40-39\u001b[0m\n\n\u001b[1mCore Nextflow options\u001b[0m\n \u001b[0;34mrevision : \u001b[0;32mdev\u001b[0m\n \u001b[0;34mrunName : \u001b[0;32mgrave_hodgkin\u001b[0m\n \u001b[0;34mcontainerEngine : \u001b[0;32mapptainer\u001b[0m\n \u001b[0;34mlaunchDir : \u001b[0;32m/tmp/tmppbklvq_q/job_working_directory/000/13/working\u001b[0m\n \u001b[0;34mworkDir : \u001b[0;32m/tmp/tmppbklvq_q/job_working_directory/000/13/working/work\u001b[0m\n \u001b[0;34mprojectDir : \u001b[0;32m/home/olivier/.nextflow/assets/OlivierCoen/stableexpression\u001b[0m\n \u001b[0;34muserName : \u001b[0;32molivier\u001b[0m\n \u001b[0;34mprofile : \u001b[0;32mapptainer\u001b[0m\n \u001b[0;34mconfigFiles : \u001b[0;32m\u001b[0m\n\n!! Only displaying parameters that differ from the pipeline defaults !!\n-\u001b[2m----------------------------------------------------\u001b[0m-\n* The nf-core framework\n https://doi.org/10.1038/s41587-020-0439-x\n\n* Software dependencies\n https://github.com/nf-core/stableexpression/blob/main/CITATIONS.md\n\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/nltk_pandas_python_pyyaml_pruned:2218f9c10723fbf3 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-nltk_pandas_python_pyyaml_pruned-2218f9c10723fbf3.img]\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_python_requests:8c6da05a2935a952 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-pandas_python_requests-8c6da05a2935a952.img]\n[86/9fbf39] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (dataset_3671e188-4fd4-41ab-ad36-517ad81c4465)\n[e5/47d907] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (dataset_69d3f92c-2be0-4deb-a415-fea0c6b418d1)\n[87/da389d] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETACCESSIONS\nPulling Apptainer image docker://community.wave.seqera.io/library/bioconductor-deseq2_r-base_r-optparse:c84cd7ffdb298fa7 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-bioconductor-deseq2_r-base_r-optparse-c84cd7ffdb298fa7.img]\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_pyarrow_python_scikit-learn:6f85e3c4d1706e81 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-pandas_pyarrow_python_scikit-learn-6f85e3c4d1706e81.img]\n[51/7cb214] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (dataset_69d3f92c-2be0-4deb-a415-fea0c6b418d1)\n[30/76ffa2] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (dataset_3671e188-4fd4-41ab-ad36-517ad81c4465)\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_pyarrow_python_scipy:7cad0d297a717147 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-pandas_pyarrow_python_scipy-7cad0d297a717147.img]\n[8b/cb4e47] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (dataset_69d3f92c-2be0-4deb-a415-fea0c6b418d1)\nPulling Apptainer image docker://community.wave.seqera.io/library/bioconductor-expressionatlas_r-base_r-optparse:ca0f8cd9d3f44af9 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-bioconductor-expressionatlas_r-base_r-optparse-ca0f8cd9d3f44af9.img]\n[50/62910a] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-552)\n[99/0032bc] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-4251)\n[b0/fea57e] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-4301)\n[5d/78028e] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-GEOD-61690)\n[b3/c3b0b4] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-GEOD-77826)\n[ef/e42508] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-5038)\n[bd/3fa292] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-7711)\n[8f/a81318] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-5215)\n[51/2fe86b] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_4301_rnaseq)\n[fb/1074e5] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_552_rnaseq)\n[43/8d985d] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_GEOD_61690_rnaseq)\n[92/138d42] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_GEOD_77826_rnaseq)\n[db/dd3fbf] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_4251_rnaseq)\n[44/dc08c9] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_5038_rnaseq)\n[0c/f4f46e] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_5215_rnaseq)\n[cd/61f87c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_7711_rnaseq)\n[65/24c205] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_552_rnaseq)\n[f6/f1cf3f] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_4301_rnaseq)\n[9b/fe7347] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_GEOD_61690_rnaseq)\n[ed/bd7ad2] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_GEOD_77826_rnaseq)\n[47/b13d0c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_4251_rnaseq)\n[c7/3de644] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_5038_rnaseq)\n[26/a49898] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_5215_rnaseq)\n[5c/fa0cee] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_7711_rnaseq)\n[2b/6ee60b] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (dataset_3671e188-4fd4-41ab-ad36-517ad81c4465)\n[b2/a196a7] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_552_rnaseq)\n[41/7dc9e1] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_4301_rnaseq)\n[01/673e26] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_GEOD_61690_rnaseq)\n[b8/8fb0eb] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_GEOD_77826_rnaseq)\n[30/ee19b6] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_4251_rnaseq)\n[f9/f2b9b6] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_5038_rnaseq)\n[ca/18206e] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_5215_rnaseq)\n[22/7e80e7] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_7711_rnaseq)\n[bc/d70f60] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (dataset_3671e188-4fd4-41ab-ad36-517ad81c4465)\n[58/2d50b7] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_552_rnaseq)\n[1e/aeb2b1] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_4301_rnaseq)\n[29/8ca339] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_GEOD_61690_rnaseq)\n[af/164cba] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_GEOD_77826_rnaseq)\n[77/7abd5c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_4251_rnaseq)\n[20/cc6ed1] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_5038_rnaseq)\n[77/61b951] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_5215_rnaseq)\n[21/f6cbd4] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_7711_rnaseq)\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/polars_python:cab787b788e5eba7 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-polars_python-cab787b788e5eba7.img]\n[f7/b8e320] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:MERGE_DATA\n[cb/7f5612] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:GENE_STATISTICS\nPulling Apptainer image docker://quay.io/biocontainers/multiqc:1.27--pyhdfd78af_0 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/quay.io-biocontainers-multiqc-1.27--pyhdfd78af_0.img]\n[61/43e626] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:MULTIQC_WORKFLOW:MULTIQC\n-\u001b[0;35m[nf-core/stableexpression]\u001b[0;32m Pipeline completed successfully\u001b[0m-\n", + "update_time": "2025-07-07T13:45:08.624371", + "user_email": "planemo@galaxyproject.org", + "user_id": "f14d691d132db03a" + }, + "status": "success", + "test_index": 2, + "time_seconds": 291.48228931427, + "tool_id": "nf_core_stableexpression", + "tool_version": "1.0dev" + }, + "has_data": true, + "id": "nf_core_stableexpression-2" + } + ], + "version": "0.1" +} diff --git a/galaxy/tool/nf_core_stableexpression.xml b/galaxy/tool/nf_core_stableexpression.xml new file mode 100644 index 00000000..33a51020 --- /dev/null +++ b/galaxy/tool/nf_core_stableexpression.xml @@ -0,0 +1,211 @@ + + Pipeline dedicated to finding the most stable genes across count datasets + + + nextflow + apptainer + openjdk + + + + +
    + + ([a-zA-Z]+)[_ ]([a-zA-Z]+) + + + + + + + + + + + + +
    +
    + + ([a-zA-Z,]+) + + + ([A-Z0-9-]+,?)+ + + + + + ([A-Z0-9-]+,?)+ + + +
    +
    + + + +
    +
    + + + + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @misc{nf-core/stableexpression, + author = {Coen, Olivier}, + year = {2025}, + title = {nf-core/stableexpression}, + publisher = {GitHub}, + journal = {GitHub repository}, + url = {https://github.com/OlivierCoen/stableexpression}, + } + + +
    diff --git a/galaxy/tools/rebuild_samplesheet.py b/galaxy/tool/rebuild_samplesheet.py similarity index 100% rename from galaxy/tools/rebuild_samplesheet.py rename to galaxy/tool/rebuild_samplesheet.py diff --git a/galaxy/tool/test-data/input.csv b/galaxy/tool/test-data/input.csv new file mode 100644 index 00000000..6ea4aa16 --- /dev/null +++ b/galaxy/tool/test-data/input.csv @@ -0,0 +1,3 @@ +counts,design,platform,normalised +tests/test_data/input_datasets/microarray.normalised.csv,tests/test_data/input_datasets/microarray.normalised.design.csv,microarray,true +tests/test_data/input_datasets/rnaseq.raw.csv,tests/test_data/input_datasets/rnaseq.raw.design.csv,rnaseq,false diff --git a/galaxy/tool/test-data/microarray.normalised.csv b/galaxy/tool/test-data/microarray.normalised.csv new file mode 100644 index 00000000..60869917 --- /dev/null +++ b/galaxy/tool/test-data/microarray.normalised.csv @@ -0,0 +1,10 @@ +,GSM1528575,GSM1528576,GSM1528579,GSM1528583,GSM1528584,GSM1528585,GSM1528580,GSM1528586,GSM1528582,GSM1528578,GSM1528581,GSM1528577 +ENSRNA049453121,20925.1255070264,136184.261516502,144325.370645564,89427.0987612997,164143.182734208,34178.6378088171,28842.7323281157,76973.395782103,41906.9367255656,44756.5602263121,252562.049703724,6953.65643340122 +ENSRNA049453138,196173.051628372,16607.8367703051,344972.83715281,22602.4535330758,13678.598561184,104546.421532852,15451.4637472048,71664.8857281649,160643.257448002,91459.0578537683,88396.7173963033,281623.08555275 +ENSRNA049454388,91547.4240932405,11625.4857392136,84483.143792525,80582.6604222701,218857.576978944,58304.7350856292,42234.0009090266,88475.1675656357,87306.1181782617,17513.436610296,90922.3378933406,76490.2207674135 +ENSRNA049454416,20925.1255070264,106290.155329953,193607.204524536,47170.3378081581,392119.825420608,190998.270108096,90648.5873169351,81397.1541603848,83813.8734511313,165404.67909724,111127.301869638,194702.380135234 +ENSRNA049454647,99394.3461583754,91343.1022366783,3520.13099135521,71738.2220832404,118547.854196928,20105.0810640101,81377.7090686122,15040.7784861581,66352.6498154789,110918.431865208,55563.6509348192,111258.50293442 +ENSRNA049454661,175247.926121346,66431.3470812206,24640.9169394865,52083.9146631746,360203.095444512,36189.1459152181,70046.6356539953,85820.9125386666,13968.9789085219,50594.3724297441,25256.2049703724,52152.4232505092 +ENSRNA049454747,117703.830977024,154452.881963838,281610.479308417,29481.4611300988,191500.379856576,152798.616086476,53565.0743236435,14156.0268105017,293348.557078959,155674.99209152,63140.5124259309,243377.975169043 +ENSRNA049454887,2615.6406883783,164417.584026021,28161.0479308417,82548.0911642767,50154.861391008,136714.551235268,97859.270398964,64586.872322914,328271.004350264,159566.866893808,151537.229822234,86920.7054175153 +ENSRNA049454931,177863.566809724,81378.4001744952,235848.776420799,88444.3833902964,18238.131414912,120630.48638406,82407.8066517592,50430.8455124123,118736.320722436,68107.8090400402,232357.085727426,163410.926184929 diff --git a/galaxy/tool/test-data/microarray.normalised.design.csv b/galaxy/tool/test-data/microarray.normalised.design.csv new file mode 100644 index 00000000..d31e5cef --- /dev/null +++ b/galaxy/tool/test-data/microarray.normalised.design.csv @@ -0,0 +1,13 @@ +sample,condition +GSM1528575,g1 +GSM1528576,g1 +GSM1528579,g1 +GSM1528583,g2 +GSM1528584,g2 +GSM1528585,g2 +GSM1528580,g3 +GSM1528586,g3 +GSM1528582,g3 +GSM1528578,g4 +GSM1528581,g4 +GSM1528577,g4 diff --git a/galaxy/tool/test-data/rnaseq.raw.csv b/galaxy/tool/test-data/rnaseq.raw.csv new file mode 100644 index 00000000..a9a6bdb4 --- /dev/null +++ b/galaxy/tool/test-data/rnaseq.raw.csv @@ -0,0 +1,10 @@ +,ESM1528575,ESM1528576,ESM1528579,ESM1528583,ESM1528584,ESM1528585,ESM1528580,ESM1528586,ESM1528582,ESM1528578,ESM1528581,ESM1528577 +ENSRNA049453121,1,82,8,82,4,68,88,73,46,57,25,22 +ENSRNA049453138,68,93,41,84,36,18,28,92,84,85,92,32 +ENSRNA049454388,38,10,0,23,11,17,95,57,25,82,10,70 +ENSRNA049454416,75,55,7,30,79,60,15,97,12,35,60,56 +ENSRNA049454647,35,64,55,91,48,95,68,100,24,26,100,47 +ENSRNA049454661,8,99,80,48,86,29,80,17,19,9,44,2 +ENSRNA049454747,67,7,98,53,3,10,52,87,4,80,22,15 +ENSRNA049454887,8,40,24,90,42,52,79,81,94,23,35,81 +ENSRNA049454931,45,49,67,73,26,76,41,16,34,47,36,25 diff --git a/galaxy/tool/test-data/rnaseq.raw.design.csv b/galaxy/tool/test-data/rnaseq.raw.design.csv new file mode 100644 index 00000000..469751d2 --- /dev/null +++ b/galaxy/tool/test-data/rnaseq.raw.design.csv @@ -0,0 +1,13 @@ +sample,condition +ESM1528575,g1 +ESM1528576,g1 +ESM1528579,g1 +ESM1528583,g2 +ESM1528584,g2 +ESM1528585,g2 +ESM1528580,g3 +ESM1528586,g3 +ESM1528582,g3 +ESM1528578,g4 +ESM1528581,g4 +ESM1528577,g4 diff --git a/galaxy/tools/tool.xml b/galaxy/tool/tool.xml similarity index 84% rename from galaxy/tools/tool.xml rename to galaxy/tool/tool.xml index b85bdcb1..3993bf13 100644 --- a/galaxy/tools/tool.xml +++ b/galaxy/tool/tool.xml @@ -4,11 +4,17 @@ VERSION="1.0dev"; echo "$VERSION" ]]> - nextflow - singularity + nextflow + apptainer + openjdk @@ -126,12 +141,21 @@ VERSION="1.0dev"; echo "$VERSION" - + - - + + + + + + + + + + + - -
    - - -
    -
    diff --git a/galaxy/tools/tool_dependencies.xml b/galaxy/tools/tool_dependencies.xml deleted file mode 100644 index 92ca1db4..00000000 --- a/galaxy/tools/tool_dependencies.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - NXF_OFFLINE=false - curl -s https://get.nextflow.io | bash - chmod +x nextflow - - - - From f606f3f2aade90e90e504a2dab605b609315600d Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Mon, 7 Jul 2025 16:56:12 +0200 Subject: [PATCH 025/258] replace environment.yml files by spec-file.txt files to lock conda environments --- .../local/dataset_statistics/environment.yml | 8 - modules/local/dataset_statistics/main.nf | 2 +- .../local/dataset_statistics/spec-file.txt | 107 ++++++++++ .../getaccessions/environment.yml | 10 - .../expressionatlas/getaccessions/main.nf | 2 +- .../getaccessions/spec-file.txt | 65 ++++++ .../expressionatlas/getdata/environment.yml | 8 - modules/local/expressionatlas/getdata/main.nf | 2 +- .../expressionatlas/getdata/spec-file.txt | 174 ++++++++++++++++ modules/local/gene_statistics/environment.yml | 6 - modules/local/gene_statistics/main.nf | 2 +- modules/local/gene_statistics/spec-file.txt | 40 ++++ .../local/idmapping/gprofiler/environment.yml | 7 - modules/local/idmapping/gprofiler/main.nf | 2 +- .../local/idmapping/gprofiler/spec-file.txt | 56 ++++++ modules/local/merge_data/environment.yml | 6 - modules/local/merge_data/main.nf | 2 +- modules/local/merge_data/spec-file.txt | 40 ++++ .../normalisation/deseq2/environment.yml | 8 - modules/local/normalisation/deseq2/main.nf | 2 +- .../local/normalisation/deseq2/spec-file.txt | 190 ++++++++++++++++++ .../local/normalisation/edger/environment.yml | 8 - modules/local/normalisation/edger/main.nf | 2 +- .../local/normalisation/edger/spec-file.txt | 101 ++++++++++ .../quantile_normalisation/environment.yml | 8 - modules/local/quantile_normalisation/main.nf | 2 +- .../quantile_normalisation/spec-file.txt | 110 ++++++++++ 27 files changed, 892 insertions(+), 78 deletions(-) delete mode 100644 modules/local/dataset_statistics/environment.yml create mode 100644 modules/local/dataset_statistics/spec-file.txt delete mode 100644 modules/local/expressionatlas/getaccessions/environment.yml create mode 100644 modules/local/expressionatlas/getaccessions/spec-file.txt delete mode 100644 modules/local/expressionatlas/getdata/environment.yml create mode 100644 modules/local/expressionatlas/getdata/spec-file.txt delete mode 100644 modules/local/gene_statistics/environment.yml create mode 100644 modules/local/gene_statistics/spec-file.txt delete mode 100644 modules/local/idmapping/gprofiler/environment.yml create mode 100644 modules/local/idmapping/gprofiler/spec-file.txt delete mode 100644 modules/local/merge_data/environment.yml create mode 100644 modules/local/merge_data/spec-file.txt delete mode 100644 modules/local/normalisation/deseq2/environment.yml create mode 100644 modules/local/normalisation/deseq2/spec-file.txt delete mode 100644 modules/local/normalisation/edger/environment.yml create mode 100644 modules/local/normalisation/edger/spec-file.txt delete mode 100644 modules/local/quantile_normalisation/environment.yml create mode 100644 modules/local/quantile_normalisation/spec-file.txt diff --git a/modules/local/dataset_statistics/environment.yml b/modules/local/dataset_statistics/environment.yml deleted file mode 100644 index 2e3af0bd..00000000 --- a/modules/local/dataset_statistics/environment.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: quant_norm -channels: - - conda-forge -dependencies: - - conda-forge::python==3.12.8 - - conda-forge::pandas==2.2.3 - - conda-forge::scipy==1.15.0 - - conda-forge::pyarrow==19.0.0 diff --git a/modules/local/dataset_statistics/main.nf b/modules/local/dataset_statistics/main.nf index e8df03f5..27bcef4a 100644 --- a/modules/local/dataset_statistics/main.nf +++ b/modules/local/dataset_statistics/main.nf @@ -4,7 +4,7 @@ process DATASET_STATISTICS { tag "${meta.dataset}" - conda "${moduleDir}/environment.yml" + conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/5f/5fe497e7a739fa611fedd6f72ab9a3cf925873a5ded3188161fc85fd376b2c1c/data': 'community.wave.seqera.io/library/pandas_pyarrow_python_scipy:7cad0d297a717147' }" diff --git a/modules/local/dataset_statistics/spec-file.txt b/modules/local/dataset_statistics/spec-file.txt new file mode 100644 index 00000000..ebfe01c3 --- /dev/null +++ b/modules/local/dataset_statistics/spec-file.txt @@ -0,0 +1,107 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda#3cd1a7238a0dd3d0860fdefc496cc854 +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda#9e60c55e725c20d23125a5f0dd69af5d +https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.10.6-hb9d3cd8_0.conda#d7d4680337a14001b0e043e96529409b +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.6.15-hbd8a1cb_0.conda#72525f07d72806e3b639ad4504c30ce5 +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda#c87df2ab1448ba69169652ab9547082d +https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.8.1-h1a47875_3.conda#55a8561fdbbbd34f50f57d9be12ed084 +https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.3.0-h4e1184b_5.conda#3f4c1197462a6df2be6dc8241828fe93 +https://conda.anaconda.org/conda-forge/linux-64/s2n-1.5.11-h072c03f_0.conda#5e8060d52f676a40edef0006a75c718f +https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.15.3-h173a860_6.conda#9a063178f1af0a898526cc24ba7be486 +https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.9.2-hefd7a92_4.conda#5ce4df662d32d3123ea8da15571b6f51 +https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.2.2-h4e1184b_0.conda#dcd498d493818b776a77fbc242fbf8e4 +https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.8.1-h205f482_0.conda#9c500858e88df50af3cc883d194de78a +https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.2.2-h4e1184b_4.conda#74e8c3e4df4ceae34aa2959df4b28101 +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda#6d11a5edae89fe413c0569f16d308f5a +https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.5.0-h7959bf6_11.conda#9b3fb60fe57925a92f399bc3fc42eccf +https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.11.0-h11f4f37_12.conda#96c3e0221fa2da97619ee82faa341a73 +https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.7.9-he1b24dc_1.conda#caafc32928a5f7f3f7ef67d287689144 +https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.29.9-he0e7f3f_2.conda#8a4e6fc8a3b285536202b5456a74a940 +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda#e66f2b8ad787e7beb0f846e4bd7e8493 +https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.1-h166bdaf_0.tar.bz2#30186d27e2c9fa62b45fb1476b7200e3 +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 +https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda#c277e0a4d549b03ac1e9d6cbbe3d017b +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_3.conda#57541755b5a51691955012b8e197c06c +https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda#3f43953b7d3fb3aaa1d0d0723d91e368 +https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.5-hb9d3cd8_0.conda#f7f0d6cc2dc986d42ac2689ec88192be +https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda#172bf1cd1ff8629f2b1179945ed45055 +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 +https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.64.0-h161d5f1_0.conda#19e57602824042dfd0446292ef90488b +https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda#eecce068c7e4eddeb169591baac20ac4 +https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda#6432cb5d4ac0046c3ac0a8a0f95842f9 +https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.14.1-h332b0f4_0.conda#45f6713cb00f124af300342512219182 +https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.489-h4d475cb_0.conda#b775e9f46dfa94b228a81d8e8c6d8b1d +https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.14.0-h5cfcd09_0.conda#0a8838771cc2e985cd295e01ae83baf1 +https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.10.0-h113e628_0.conda#73f73f60854f325a55f1d31459f2ab73 +https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 +https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h4ce23a2_1.conda#e796ff8ddc598affdf7c173d6145f087 +https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc +https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.8-h4bc477f_0.conda#14dbe05b929e329dbaa6f2d0aa19466d +https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.8.0-h736e048_1.conda#13de36be8de3ae3f05ba127631599213 +https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.13.0-h3cf044e_1.conda#7eb66060455c7a47d9dcdbfa9f46579b +https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.12.0-ha633028_1.conda#7c1980f89dd41b097549782121a73490 +https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62ee74e96c5ebb0af99386de58cf9553 +https://conda.anaconda.org/conda-forge/linux-64/gflags-2.2.2-h5888daf_1005.conda#d411fc29e338efb48c5fd4576d71d881 +https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hbabe93e_0.conda#ff862eebdfeb2fd048ae9dc92510baca +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_0.conda#e31316a586cac398b1fcdb10ace786b9 +https://conda.anaconda.org/conda-forge/linux-64/libabseil-20240722.0-cxx17_hbbce691_4.conda#488f260ccda0afaf08acb286db439c2f +https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.1.0-hb9d3cd8_3.conda#cb98af5db26e3f482bebb80ce9d947d3 +https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.1.0-hb9d3cd8_3.conda#1c6eecffad553bde44c5238770cfb7da +https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.1.0-hb9d3cd8_3.conda#3facafe58f3858eb95527c7d3a3fc578 +https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-5.28.3-h6128344_1.conda#d8703f1ffe5a06356f06467f1d0b9464 +https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2024.07.02-hbbce691_2.conda#b2fede24428726dd867611664fb372e8 +https://conda.anaconda.org/conda-forge/linux-64/re2-2024.07.02-h9925aae_2.conda#e84ddf12bde691e8ec894b00ea829ddf +https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.67.1-h25350d4_2.conda#bfcedaf5f9b003029cc6abe9431f66bf +https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.35.0-h2b5623c_0.conda#1040ab07d7af9f23cf2466ffe4e58db1 +https://conda.anaconda.org/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2#c965a5aa0d5c1c37ffc62dff36e28400 +https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-2.35.0-h0121fbd_0.conda#34e2243e0428aac6b3e903ef99b6d57d +https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-headers-1.18.0-ha770c72_1.conda#4fb055f57404920a43b147031471e03b +https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h3f2d84a_0.conda#d76872d096d063e226482c99337209dc +https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.1-hb9d3cd8_2.conda#c9f075ab2f33b3bbee9e62d4ad0a6cd8 +https://conda.anaconda.org/conda-forge/linux-64/prometheus-cpp-1.3.0-ha5d0236_0.conda#a83f6a2fdc079e643237887a37460668 +https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-1.18.0-hfcad708_1.conda#1f5a5d66e77a39dc5bd639ec953705cf +https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.10.0-h202a827_0.conda#0f98f3e95272d118f7931b6bef69bfe5 +https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda#9de5350a85c4a20c685259b889aa6393 +https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.1-h8bd8927_1.conda#3b3e64af585eadfb52bb90b553db5edf +https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a +https://conda.anaconda.org/conda-forge/linux-64/orc-2.0.3-h12ee42a_2.conda#4f6f9f3f80354ad185e276c120eac3f0 +https://conda.anaconda.org/conda-forge/linux-64/libarrow-19.0.0-hfa2a6e7_9_cpu.conda#9e09f9cd5c0eb584be78ebea4e0db151 +https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-19.0.0-hcb10f89_9_cpu.conda#cd6e5cd25096e02ddc591f0bc7d0354b +https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda#a1cfcc585f0c42bf8d5546bb1dfb668d +https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.21.0-h0e7cc3e_0.conda#dcb95c0a98ba9ff737f7ae482aef7833 +https://conda.anaconda.org/conda-forge/linux-64/libparquet-19.0.0-h081d1f1_9_cpu.conda#de0b82dc1e9f6f9fb66306f0a15e16fa +https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-19.0.0-hcb10f89_9_cpu.conda#da890e33d20acb4713e73ed841d5088b +https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-19.0.0-h08228c5_9_cpu.conda#9d6c1688d87aeb9fc4513bde60d2f2f3 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda#530566b68c3b8ce7eec4cd047eae19fe +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda#bfbca721fd33188ef923dfe9ba172f29 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda#323dc8f259224d13078aaf7ce96c3efe +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-32_h59b9bed_openblas.conda#2af9f3d5c2e39f417ce040f5a35c40c6 +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-32_he106b2a_openblas.conda#3d3f9355e52f269cd8bc2c440d8a5263 +https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda#db0bfbe7dd197b68ad5f30333bae6ce0 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-32_h7ac8fdf_openblas.conda#6c3f04ccb6c578138e9f9899da0bd714 +https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda#d864d34357c3b65a4b731f78c0801dc4 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.2-h6cd9bfd_0.conda#b04c7eda6d7dab1e6503135e7fad4d25 +https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b +https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc +https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 +https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 +https://conda.anaconda.org/conda-forge/linux-64/python-3.12.8-h9e4cc4f_1_cpython.conda#7fd2fd79436d9b473812f14e86746844 +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda#0dfcdc155cf23812a0c9deada86fb723 +https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.1-py312h6cf2f7f_0.conda#7e086a30150af2536a1059885368dcf0 +https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda#a451d576819089b0d672f18768be0f65 +https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 +https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 +https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 +https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.3-py312hf9745cd_3.conda#2979458c23c7755683a0598fb33e7666 +https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e +https://conda.anaconda.org/conda-forge/noarch/wheel-0.45.1-pyhd8ed1ab_1.conda#75cb7132eb58d97896e173ef12ac9986 +https://conda.anaconda.org/conda-forge/noarch/pip-25.1.1-pyh8b19718_0.conda#32d0781ace05105cc99af55d36cbec7c +https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-19.0.0-py312h01725c0_0_cpu.conda#7ab1143b9ac1af5cc4a630706f643627 +https://conda.anaconda.org/conda-forge/linux-64/pyarrow-19.0.0-py312h7900ff3_0.conda#14f86e63b5c214dd9fb34e5472d4bafc +https://conda.anaconda.org/conda-forge/linux-64/scipy-1.15.0-py312h180e4f1_1.conda#401e9d25f6ed7d9d9a06da0dca473c3e diff --git a/modules/local/expressionatlas/getaccessions/environment.yml b/modules/local/expressionatlas/getaccessions/environment.yml deleted file mode 100644 index 7a9ed092..00000000 --- a/modules/local/expressionatlas/getaccessions/environment.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: eatlas_get_accessions -channels: - - conda-forge -dependencies: - - conda-forge::python==3.12.8 - - conda-forge::requests==2.32.3 - - conda-forge::nltk==3.9.1 - - conda-forge::tenacity==9.0.0 - - conda-forge::pandas==2.2.3 - - conda-forge::pyyaml==6.0.2 diff --git a/modules/local/expressionatlas/getaccessions/main.nf b/modules/local/expressionatlas/getaccessions/main.nf index a44f769e..2083fcf9 100644 --- a/modules/local/expressionatlas/getaccessions/main.nf +++ b/modules/local/expressionatlas/getaccessions/main.nf @@ -2,7 +2,7 @@ process EXPRESSIONATLAS_GETACCESSIONS { label 'process_low' - conda "${moduleDir}/environment.yml" + conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/5e/5e3d9b407277b8bb8f8850eba40724b1cae9bd6e11ae0019011af82e6ac17cd4/data': 'community.wave.seqera.io/library/nltk_pandas_python_pyyaml_pruned:2218f9c10723fbf3' }" diff --git a/modules/local/expressionatlas/getaccessions/spec-file.txt b/modules/local/expressionatlas/getaccessions/spec-file.txt new file mode 100644 index 00000000..5fbccb12 --- /dev/null +++ b/modules/local/expressionatlas/getaccessions/spec-file.txt @@ -0,0 +1,65 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda#3cd1a7238a0dd3d0860fdefc496cc854 +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda#9e60c55e725c20d23125a5f0dd69af5d +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda#6d11a5edae89fe413c0569f16d308f5a +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda#e66f2b8ad787e7beb0f846e4bd7e8493 +https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62ee74e96c5ebb0af99386de58cf9553 +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_0.conda#e31316a586cac398b1fcdb10ace786b9 +https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda#db0bfbe7dd197b68ad5f30333bae6ce0 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 +https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc +https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda#d864d34357c3b65a4b731f78c0801dc4 +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.2-h6cd9bfd_0.conda#b04c7eda6d7dab1e6503135e7fad4d25 +https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b +https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.6.15-hbd8a1cb_0.conda#72525f07d72806e3b639ad4504c30ce5 +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda#c87df2ab1448ba69169652ab9547082d +https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 +https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 +https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a +https://conda.anaconda.org/conda-forge/linux-64/python-3.12.8-h9e4cc4f_1_cpython.conda#7fd2fd79436d9b473812f14e86746844 +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda#0dfcdc155cf23812a0c9deada86fb723 +https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h2ec8cdc_3.conda#a32e0c069f6c3dcac635f7b0b0dac67e +https://conda.anaconda.org/conda-forge/noarch/certifi-2025.6.15-pyhd8ed1ab_0.conda#781d068df0cc2407d4db0ecfbb29225b +https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda#12c566707c80111f9799308d9e265aef +https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py312h06ac9bb_0.conda#a861504bbea4161a9170b85d4d2be840 +https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.2-pyhd8ed1ab_0.conda#40fe4284b8b5835a9073a645139f35af +https://conda.anaconda.org/conda-forge/noarch/click-8.2.1-pyh707e725_0.conda#94b550b8d3a614dbd326af798c7dfb40 +https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda#962b9857ee8e7018c22f2776ffa0b2d7 +https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda#0a802cb9888dd14eeefc611f05c40b6e +https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda#8e6923fc12f1fe8f8c4e5c9f343256ac +https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda#b4754fb1bdcb70c8fd54f918301582c6 +https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda#39a4f67be3286c86d696df570b1201b7 +https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e +https://conda.anaconda.org/conda-forge/noarch/joblib-1.5.1-pyhd8ed1ab_0.conda#fb1c14694de51a476ce8636d92b6f42c +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda#530566b68c3b8ce7eec4cd047eae19fe +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda#bfbca721fd33188ef923dfe9ba172f29 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda#323dc8f259224d13078aaf7ce96c3efe +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-32_h59b9bed_openblas.conda#2af9f3d5c2e39f417ce040f5a35c40c6 +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-32_he106b2a_openblas.conda#3d3f9355e52f269cd8bc2c440d8a5263 +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-32_h7ac8fdf_openblas.conda#6c3f04ccb6c578138e9f9899da0bd714 +https://conda.anaconda.org/conda-forge/linux-64/regex-2024.11.6-py312h66e93f0_0.conda#647770db979b43f9c9ca25dcfa7dc4e4 +https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda#9efbfdc37242619130ea42b1cc4ed861 +https://conda.anaconda.org/conda-forge/noarch/nltk-3.9.1-pyhd8ed1ab_1.conda#85fd21c82d46f871d3820c17270e575d +https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.1-py312h6cf2f7f_0.conda#7e086a30150af2536a1059885368dcf0 +https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda#a451d576819089b0d672f18768be0f65 +https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 +https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 +https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 +https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.3-py312hf9745cd_3.conda#2979458c23c7755683a0598fb33e7666 +https://conda.anaconda.org/conda-forge/noarch/wheel-0.45.1-pyhd8ed1ab_1.conda#75cb7132eb58d97896e173ef12ac9986 +https://conda.anaconda.org/conda-forge/noarch/pip-25.1.1-pyh8b19718_0.conda#32d0781ace05105cc99af55d36cbec7c +https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda#461219d1a5bd61342293efa2c0c90eac +https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2#4cb3ad778ec2d5a7acbdf254eb1c42ae +https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py312h178313f_2.conda#cf2485f39740de96e2a7f2bb18ed2fee +https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py312h66e93f0_2.conda#630db208bc7bbb96725ce9832c7423bb +https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda#436c165519e140cb08d246a4472a9d6a +https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda#a9b9368f3701a417eac9edbcae7cb737 +https://conda.anaconda.org/conda-forge/noarch/tenacity-9.0.0-pyhd8ed1ab_1.conda#a09f66fe95a54a92172e56a4a97ba271 diff --git a/modules/local/expressionatlas/getdata/environment.yml b/modules/local/expressionatlas/getdata/environment.yml deleted file mode 100644 index 156f457b..00000000 --- a/modules/local/expressionatlas/getdata/environment.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: eatlas_get_data -channels: - - conda-forge - - bioconda -dependencies: - - conda-forge::r-base==4.3.3 - - bioconda::bioconductor-expressionatlas==1.30.0 - - conda-forge::r-optparse==1.7.5 diff --git a/modules/local/expressionatlas/getdata/main.nf b/modules/local/expressionatlas/getdata/main.nf index b0903563..85b8a8f3 100644 --- a/modules/local/expressionatlas/getdata/main.nf +++ b/modules/local/expressionatlas/getdata/main.nf @@ -35,7 +35,7 @@ process EXPRESSIONATLAS_GETDATA { } maxRetries = 5 - conda "${moduleDir}/environment.yml" + conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/7f/7fd21450c3a3f7df37fa0480170780019e9686be319da1c9e10712f7f17cca26/data': 'community.wave.seqera.io/library/bioconductor-expressionatlas_r-base_r-optparse:ca0f8cd9d3f44af9' }" diff --git a/modules/local/expressionatlas/getdata/spec-file.txt b/modules/local/expressionatlas/getdata/spec-file.txt new file mode 100644 index 00000000..7bbe4329 --- /dev/null +++ b/modules/local/expressionatlas/getdata/spec-file.txt @@ -0,0 +1,174 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda#3cd1a7238a0dd3d0860fdefc496cc854 +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d +https://conda.anaconda.org/conda-forge/noarch/_r-mutex-1.0.1-anacondar_1.tar.bz2#19f9db5f4f1b7f5ef5f6d67207f25f38 +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda#9e60c55e725c20d23125a5f0dd69af5d +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda#e66f2b8ad787e7beb0f846e4bd7e8493 +https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62ee74e96c5ebb0af99386de58cf9553 +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_0.conda#e31316a586cac398b1fcdb10ace786b9 +https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda#db0bfbe7dd197b68ad5f30333bae6ce0 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 +https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc +https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.2-h6cd9bfd_0.conda#b04c7eda6d7dab1e6503135e7fad4d25 +https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.6.15-hbd8a1cb_0.conda#72525f07d72806e3b639ad4504c30ce5 +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda#c87df2ab1448ba69169652ab9547082d +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-7_cp313.conda#e84b44e6300f1703cb25d29120c5b1d8 +https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 +https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 +https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a +https://conda.anaconda.org/conda-forge/linux-64/python-3.13.5-hec9711d_102_cp313.conda#89e07d92cf50743886f41638d58c4328 +https://conda.anaconda.org/conda-forge/noarch/argcomplete-3.6.2-pyhd8ed1ab_0.conda#eb9d4263271ca287d2e0cf5a86da2d3a +https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-3.10.0-he073ed8_18.conda#ad8527bf134a90e1c9ed35fa0b64318c +https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.17-h0157908_18.conda#460eba7851277ec1fd80a1a24080787a +https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.44-h4bf12b8_0.conda#7a1b5c3fbc0419961eaed361eedc90d4 +https://conda.anaconda.org/conda-forge/linux-64/bwidget-1.10.1-ha770c72_1.conda#983b92277d78c0d0ec498e460caa0e6d +https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.50-h943b412_0.conda#51de14db340a848869e69c632b43cca7 +https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.13.3-h48d6fc4_1.conda#3c255be50a506c50765a93a6644f32fe +https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.13.3-ha770c72_1.conda#51f5be229d83ecd401fb369ab96ae669 +https://conda.anaconda.org/conda-forge/linux-64/freetype-2.13.3-ha770c72_1.conda#9ccd736d31e0c6e41f54e704e5312811 +https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.15.0-h7e30c49_1.conda#8f5b0b297b59e1ac160ad4beec99dbee +https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2#0c96522c6bdaed4b1566d11387caaf45 +https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2#34893075a5c9e55cdafac56607368fc6 +https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2#4d59c254e01d9cde7957100457e2d5fb +https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda#49023d73832ef61042f6a237cb2687e7 +https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29 +https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda#6d11a5edae89fe413c0569f16d308f5a +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_3.conda#57541755b5a51691955012b8e197c06c +https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 +https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h4ce23a2_1.conda#e796ff8ddc598affdf7c173d6145f087 +https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.45-hc749103_0.conda#b90bece58b4c2bf25969b70f3be42d25 +https://conda.anaconda.org/conda-forge/linux-64/libglib-2.84.2-h3618099_0.conda#072ab14a02164b7c0c089055368ff776 +https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda#b3c17d95b5a10c6e64a21fa17573e70e +https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb9d3cd8_0.conda#f6ebe2cb3f82ba6c057dde5d9debe4f7 +https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb9d3cd8_0.conda#8035c64cb77ed555e3f150b7b3972480 +https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda#92ed62436b625154323d40d5f2f11dd7 +https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.2-h29eaf8c_0.conda#39b4228a867772d610c02e06f939a5b8 +https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda#fb901ff28063514abb6046c9ec2c4a45 +https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda#1c74ff8c35dcadf952a16f752ca5aa49 +https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.12-h4f16b4b_0.conda#db038ce880f100acc74dba10302b5630 +https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.6-hb9d3cd8_0.conda#febbab7d15033c913d53c7a2c102309d +https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda#96d57aba173e878a2089d5638016dc5e +https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda#09262e66b19567aff4f592fb53b28760 +https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.1-h166bdaf_0.tar.bz2#30186d27e2c9fa62b45fb1476b7200e3 +https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda#c277e0a4d549b03ac1e9d6cbbe3d017b +https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda#3f43953b7d3fb3aaa1d0d0723d91e368 +https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.5-hb9d3cd8_0.conda#f7f0d6cc2dc986d42ac2689ec88192be +https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda#172bf1cd1ff8629f2b1179945ed45055 +https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.64.0-h161d5f1_0.conda#19e57602824042dfd0446292ef90488b +https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda#eecce068c7e4eddeb169591baac20ac4 +https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda#6432cb5d4ac0046c3ac0a8a0f95842f9 +https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.14.1-h332b0f4_0.conda#45f6713cb00f124af300342512219182 +https://conda.anaconda.org/conda-forge/linux-64/curl-8.14.1-h332b0f4_0.conda#60279087a10b4ab59a70daa838894e4b +https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-15.1.0-h4c094af_103.conda#ea67e87d658d31dc33818f9574563269 +https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-15.1.0-h97b714f_3.conda#bbcff9bf972a0437bea8e431e4b327bb +https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-15.1.0-h4393ad2_3.conda#f39f96280dd8b1ec8cbd395a3d3fdd1e +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda#530566b68c3b8ce7eec4cd047eae19fe +https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-15.1.0-h3b9cdf2_3.conda#649c5fe0593a880702e434bc375f3e8a +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda#bfbca721fd33188ef923dfe9ba172f29 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda#323dc8f259224d13078aaf7ce96c3efe +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-32_h59b9bed_openblas.conda#2af9f3d5c2e39f417ce040f5a35c40c6 +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-32_he106b2a_openblas.conda#3d3f9355e52f269cd8bc2c440d8a5263 +https://conda.anaconda.org/conda-forge/linux-64/gsl-2.7-he838d99_0.tar.bz2#fec079ba39c9cca093bf4c00001825de +https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-15.1.0-h4c094af_103.conda#83bbc814f0aeccccb5ea10267bea0d2e +https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-15.1.0-h6a1bac1_3.conda#d71cc504fcfdbee8dd7925ebb9c2bf85 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-15.1.0-h69a702a_3.conda#6e5d0574e57a38c36e674e9a18eee2b4 +https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.0-hb9d3cd8_0.conda#9fa334557db9f63da6c9285fd2a48638 +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-32_h7ac8fdf_openblas.conda#6c3f04ccb6c578138e9f9899da0bd714 +https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda#9344155d33912347b37f0ae6c410a835 +https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.24-h86f0d12_0.conda#64f0c503da58ec25ebd359e4d990afa8 +https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.5.0-h851e524_0.conda#63f790534398730f59e1b899c3644d4a +https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.0-hf01ce69_5.conda#e79a094918988bb1807462cd42c83962 +https://conda.anaconda.org/conda-forge/linux-64/make-4.4.1-hb9d3cd8_2.conda#33405d2a66b1411db9f7242c8b97c9e7 +https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.10-h36c2ea0_0.tar.bz2#ac7bc6a654f8f41b352b38f4051135f8 +https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-h5888daf_0.conda#951ff8d9e5536896408e89d63230b8d5 +https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-11.2.1-h3beb420_0.conda#0e6e192d4b3d95708ad192d957cf3163 +https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.4-hadf4263_0.conda#79f71230c069a287efe3a8614069ddf1 +https://conda.anaconda.org/conda-forge/linux-64/sed-4.9-h6688a6e_0.conda#171afc5f7ca0408bbccbcb69ade85f92 +https://conda.anaconda.org/conda-forge/linux-64/tktable-2.10-h8d826fa_7.conda#3ac51142c19ba95ae0fadefa333c9afb +https://conda.anaconda.org/conda-forge/linux-64/xorg-libxt-1.3.1-hb9d3cd8_0.conda#279b0de5f6ba95457190a1c459a64e31 +https://conda.anaconda.org/conda-forge/linux-64/r-base-4.3.3-h65010dc_18.conda#721ea26859f44b206b0146eae8444657 +https://conda.anaconda.org/bioconda/noarch/bioconductor-biocgenerics-0.48.1-r43hdfd78af_2.tar.bz2#a313dd8a932cfd178fad2f3e7e6a6184 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-biobase-2.62.0-r43ha9d7317_3.tar.bz2#536352cf94bc990f2d723564fe0d6ff9 +https://conda.anaconda.org/conda-forge/noarch/r-biocmanager-1.30.26-r43hc72bb7e_0.conda#f761a528299b72c890846fc1d1ddf099 +https://conda.anaconda.org/conda-forge/linux-64/r-base64enc-0.1_3-r43hb1dbf0f_1007.conda#3509080778587bf9d42eae11e0246633 +https://conda.anaconda.org/conda-forge/linux-64/r-digest-0.6.37-r43h0d4f4ea_0.conda#edcd201672d47f522666954cc7b25e0d +https://conda.anaconda.org/conda-forge/linux-64/r-rlang-1.1.6-r43h93ab643_0.conda#057b78b5adfffc99092504a1da563abe +https://conda.anaconda.org/conda-forge/linux-64/r-ellipsis-0.3.2-r43hb1dbf0f_3.conda#b8349582a31b17184a7674f4c847a5ad +https://conda.anaconda.org/conda-forge/linux-64/r-fastmap-1.2.0-r43ha18555a_1.conda#ac100509c0d93c8dc19e53fb299a48b5 +https://conda.anaconda.org/conda-forge/linux-64/r-htmltools-0.5.8.1-r43ha18555a_1.conda#7b26688542e1b7a39fc62affeef9d32e +https://conda.anaconda.org/conda-forge/noarch/r-jquerylib-0.1.4-r43hc72bb7e_3.conda#39eb4928bdd8752b548f7cbe8fa7cabd +https://conda.anaconda.org/conda-forge/noarch/r-evaluate-1.0.4-r43hc72bb7e_0.conda#483ee1d772c6a47e28e2bf7b3e161ebe +https://conda.anaconda.org/conda-forge/linux-64/r-xfun-0.52-r43h93ab643_0.conda#5f6b312d62bad138ba4d54869a3b1c81 +https://conda.anaconda.org/conda-forge/noarch/r-highr-0.11-r43hc72bb7e_1.conda#23e4d2048f51cbe7c0fb8b9230edc701 +https://conda.anaconda.org/conda-forge/linux-64/r-yaml-2.3.10-r43hdb488b9_0.conda#ad2267cd6e74c87f2720494ea13f0fa4 +https://conda.anaconda.org/conda-forge/noarch/r-knitr-1.50-r43hc72bb7e_0.conda#8223b7a4d358fa5b5f8403c33885be13 +https://conda.anaconda.org/conda-forge/linux-64/pandoc-3.7.0.2-ha770c72_0.conda#db0c1632047d38997559ce2c4741dd91 +https://conda.anaconda.org/conda-forge/linux-64/r-cachem-1.1.0-r43hb1dbf0f_1.conda#02b195910b59c2cfd1fb7159edbb047a +https://conda.anaconda.org/conda-forge/linux-64/r-jsonlite-2.0.0-r43h2b5f3a1_0.conda#576ceaa3103ec089c0314e717d74e4de +https://conda.anaconda.org/conda-forge/linux-64/r-cli-3.6.5-r43h93ab643_0.conda#5d49a07fdd4ca869c4a79082692c4d2a +https://conda.anaconda.org/conda-forge/linux-64/r-glue-1.8.0-r43h2b5f3a1_0.conda#381d612db7519f2a54f1b187e738ac7b +https://conda.anaconda.org/conda-forge/noarch/r-lifecycle-1.0.4-r43hc72bb7e_1.conda#7a0a8ba1fe2cf12b39062d8291e2fca8 +https://conda.anaconda.org/conda-forge/noarch/r-memoise-2.0.1-r43hc72bb7e_3.conda#98e3d2eb6635a5f7b8af487f47184a98 +https://conda.anaconda.org/conda-forge/linux-64/r-mime-0.13-r43h2b5f3a1_0.conda#908a1d0ff246fcf4040de60d49b08049 +https://conda.anaconda.org/conda-forge/linux-64/r-fs-1.6.6-r43h93ab643_0.conda#6a5d79631aeaeef651fbfd7cbf5a954d +https://conda.anaconda.org/conda-forge/noarch/r-r6-2.6.1-r43hc72bb7e_0.conda#be02712c703445dc5cabbe0f22d0d063 +https://conda.anaconda.org/conda-forge/linux-64/r-rappdirs-0.3.3-r43hb1dbf0f_3.conda#9fb3dd1c37f2b0d351850edf594fb1a9 +https://conda.anaconda.org/conda-forge/linux-64/r-sass-0.4.10-r43h93ab643_0.conda#7e74b09775521a8ac1464c413e3c2646 +https://conda.anaconda.org/conda-forge/noarch/r-bslib-0.9.0-r43hc72bb7e_0.conda#7d028d04efd751bc558a6aaab0940f15 +https://conda.anaconda.org/conda-forge/noarch/r-fontawesome-0.5.3-r43hc72bb7e_0.conda#e2061ac4dd2fcbcd59611167f03eec19 +https://conda.anaconda.org/conda-forge/noarch/r-tinytex-0.57-r43hc72bb7e_0.conda#1986e92b398b17e8c88353dc2e444939 +https://conda.anaconda.org/conda-forge/noarch/r-rmarkdown-2.29-r43hc72bb7e_0.conda#5ecdb9c42acf2f0730c9793dfac09b8d +https://conda.anaconda.org/conda-forge/noarch/r-bookdown-0.43-r43hc72bb7e_0.conda#4afcb86c5eeabd3be77a46f6703e3971 +https://conda.anaconda.org/bioconda/noarch/bioconductor-biocstyle-2.30.0-r43hdfd78af_0.tar.bz2#2fdfd5cd16c84e0d71d7ffb3be4e54d3 +https://conda.anaconda.org/conda-forge/linux-64/oniguruma-6.9.10-hb9d3cd8_0.conda#6ce853cb231f18576d2db5c2d4cb473e +https://conda.anaconda.org/conda-forge/linux-64/jq-1.8.1-h73b1eb8_0.conda#2714e43bfc035f7ef26796632aa1b523 +https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2#4cb3ad778ec2d5a7acbdf254eb1c42ae +https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py313h8060acc_2.conda#50992ba61a8a1f8c2d346168ae1c86df +https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e +https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda#b0dd904de08b7db706167240bf37b164 +https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.13.3-pyha770c72_0.conda#146402bf0f11cbeb8f781fa4309a95d3 +https://conda.anaconda.org/conda-forge/noarch/xmltodict-0.14.2-pyhd8ed1ab_1.conda#96ef17b8734b174d35346da0762f0137 +https://conda.anaconda.org/conda-forge/noarch/yq-3.4.3-pyhe01879c_2.conda#18cefe7c50c1228da474ea0e95a8e646 +https://conda.anaconda.org/bioconda/noarch/bioconductor-data-packages-20250625-hdfd78af_0.tar.bz2#34d7066b99d7e6769305dcebf0a9de87 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-s4vectors-0.40.2-r43ha9d7317_2.tar.bz2#6aa465e83dabb7ed5b853519d8a334e4 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-iranges-2.36.0-r43ha9d7317_2.tar.bz2#cca51afd40439bea147c1adf9857bec0 +https://conda.anaconda.org/conda-forge/linux-64/r-matrixstats-1.5.0-r43h2b5f3a1_0.conda#bbf709a87ed6a14852cb0a4171539a06 +https://conda.anaconda.org/bioconda/noarch/bioconductor-matrixgenerics-1.14.0-r43hdfd78af_3.tar.bz2#c79f36cc0cd464874aefd50a700d0079 +https://conda.anaconda.org/conda-forge/noarch/r-abind-1.4_5-r43hc72bb7e_1006.conda#75d26096ffa98e1cde7b27b9530899a1 +https://conda.anaconda.org/conda-forge/noarch/r-crayon-1.5.3-r43hc72bb7e_1.conda#bafc77be1942ea00228cf18d2cb30e35 +https://conda.anaconda.org/conda-forge/linux-64/r-lattice-0.22_7-r43h2b5f3a1_0.conda#1902233545ef5232dacdd973153d77c4 +https://conda.anaconda.org/conda-forge/linux-64/r-matrix-1.6_5-r43he966344_1.conda#df8a1175a62460e02dbf340966cbfeab +https://conda.anaconda.org/bioconda/linux-64/bioconductor-s4arrays-1.2.0-r43ha9d7317_2.tar.bz2#28fd3fe7fd8d087c1cfa7805bbd16661 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-zlibbioc-1.48.0-r43ha9d7317_2.tar.bz2#b460a5493c1d67ff386a0e63eb078a64 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-xvector-0.42.0-r43ha9d7317_2.tar.bz2#16f45b1c97517cc3d063a442a43689a4 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-sparsearray-1.2.2-r43ha9d7317_2.tar.bz2#41f1e8c1cfb7ff594e923c05e02d9ecb +https://conda.anaconda.org/bioconda/linux-64/bioconductor-delayedarray-0.28.0-r43ha9d7317_2.tar.bz2#cec6a218547ee2af2b823957a373a655 +https://conda.anaconda.org/conda-forge/linux-64/r-statmod-1.5.0-r43ha36c22a_2.conda#d1b3431cbf858fec53e7eb00f8b8cde0 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-limma-3.58.1-r43ha9d7317_1.tar.bz2#c8af3f878cedd1c3c4b6a61a722cddc0 +https://conda.anaconda.org/bioconda/noarch/bioconductor-genomeinfodbdata-1.2.11-r43hdfd78af_1.tar.bz2#14721a7fde8cfe4703796dfd5a119d76 +https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.8-h4bc477f_0.conda#14dbe05b929e329dbaa6f2d0aa19466d +https://conda.anaconda.org/conda-forge/linux-64/r-bitops-1.0_9-r43h2b5f3a1_0.conda#8643d84c1d28ea73e48db9deb9a2eff3 +https://conda.anaconda.org/conda-forge/linux-64/r-rcurl-1.98_1.16-r43he8228da_1.conda#e03c3ff98b32efffb620d7dec4df34b1 +https://conda.anaconda.org/bioconda/noarch/bioconductor-genomeinfodb-1.38.1-r43hdfd78af_1.tar.bz2#03e20a01b672b693c9470dec80d83993 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-genomicranges-1.54.1-r43ha9d7317_2.tar.bz2#01031256b035b2d4a15c14b690be39aa +https://conda.anaconda.org/bioconda/noarch/bioconductor-summarizedexperiment-1.32.0-r43hdfd78af_0.tar.bz2#6bed161da6d64cef9f9ebd5fbc2452e7 +https://conda.anaconda.org/conda-forge/linux-64/r-curl-6.2.2-r43h2700575_0.conda#c41790376a6567e34b7e8c04c04d9ea2 +https://conda.anaconda.org/conda-forge/linux-64/r-sys-3.4.3-r43h2b5f3a1_0.conda#b7ce9f99da446a47b97950ff9a9cbb60 +https://conda.anaconda.org/conda-forge/linux-64/r-askpass-1.2.1-r43h2b5f3a1_0.conda#8ccad521ab24a75928dd54af1e42632d +https://conda.anaconda.org/conda-forge/linux-64/r-openssl-2.3.3-r43he8289e2_0.conda#00a4cd47c633cd3c83f3e5e960b3ddf5 +https://conda.anaconda.org/conda-forge/noarch/r-httr-1.4.7-r43hc72bb7e_1.conda#746050b53c705dbe5ac9c5fbce51737a +https://conda.anaconda.org/conda-forge/linux-64/r-xml-3.99_0.17-r43h5bae778_2.conda#0b6f80438b17c41128f8e00a15aae22d +https://conda.anaconda.org/conda-forge/linux-64/r-xml2-1.3.8-r43h1bb2df6_0.conda#0dd4c0d50e2770cb8fab723a08c50f0c +https://conda.anaconda.org/bioconda/noarch/bioconductor-expressionatlas-1.30.0-r43hdfd78af_0.tar.bz2#50c241f6b09864ffe8e25ed80ffb3b64 +https://conda.anaconda.org/conda-forge/noarch/pip-25.1.1-pyh145f28c_0.conda#01384ff1639c6330a0924791413b8714 +https://conda.anaconda.org/conda-forge/noarch/r-getopt-1.20.4-r43ha770c72_1.conda#cf6793c369dbc7ef63d9c1bc9b186615 +https://conda.anaconda.org/conda-forge/noarch/r-optparse-1.7.5-r43hc72bb7e_1.conda#ae32080aac0f74e73e7cd6e774db1c73 diff --git a/modules/local/gene_statistics/environment.yml b/modules/local/gene_statistics/environment.yml deleted file mode 100644 index a170b24e..00000000 --- a/modules/local/gene_statistics/environment.yml +++ /dev/null @@ -1,6 +0,0 @@ -name: gene_statistics -channels: - - conda-forge -dependencies: - - conda-forge::python==3.12.8 - - conda-forge::polars==1.17.1 diff --git a/modules/local/gene_statistics/main.nf b/modules/local/gene_statistics/main.nf index 456c821d..34257607 100644 --- a/modules/local/gene_statistics/main.nf +++ b/modules/local/gene_statistics/main.nf @@ -12,7 +12,7 @@ process GENE_STATISTICS { } } - conda "${moduleDir}/environment.yml" + conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" diff --git a/modules/local/gene_statistics/spec-file.txt b/modules/local/gene_statistics/spec-file.txt new file mode 100644 index 00000000..1bf5d691 --- /dev/null +++ b/modules/local/gene_statistics/spec-file.txt @@ -0,0 +1,40 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda#3cd1a7238a0dd3d0860fdefc496cc854 +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda#9e60c55e725c20d23125a5f0dd69af5d +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda#e66f2b8ad787e7beb0f846e4bd7e8493 +https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62ee74e96c5ebb0af99386de58cf9553 +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.6.15-hbd8a1cb_0.conda#72525f07d72806e3b639ad4504c30ce5 +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_0.conda#e31316a586cac398b1fcdb10ace786b9 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda#530566b68c3b8ce7eec4cd047eae19fe +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda#bfbca721fd33188ef923dfe9ba172f29 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda#323dc8f259224d13078aaf7ce96c3efe +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-32_h59b9bed_openblas.conda#2af9f3d5c2e39f417ce040f5a35c40c6 +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-32_he106b2a_openblas.conda#3d3f9355e52f269cd8bc2c440d8a5263 +https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda#db0bfbe7dd197b68ad5f30333bae6ce0 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-32_h7ac8fdf_openblas.conda#6c3f04ccb6c578138e9f9899da0bd714 +https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc +https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda#d864d34357c3b65a4b731f78c0801dc4 +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.2-h6cd9bfd_0.conda#b04c7eda6d7dab1e6503135e7fad4d25 +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda#6d11a5edae89fe413c0569f16d308f5a +https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b +https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda#c87df2ab1448ba69169652ab9547082d +https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 +https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 +https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a +https://conda.anaconda.org/conda-forge/linux-64/python-3.12.8-h9e4cc4f_1_cpython.conda#7fd2fd79436d9b473812f14e86746844 +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda#0dfcdc155cf23812a0c9deada86fb723 +https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.1-py312h6cf2f7f_0.conda#7e086a30150af2536a1059885368dcf0 +https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda#58335b26c38bf4a20f399384c33cbcf9 +https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e +https://conda.anaconda.org/conda-forge/noarch/wheel-0.45.1-pyhd8ed1ab_1.conda#75cb7132eb58d97896e173ef12ac9986 +https://conda.anaconda.org/conda-forge/noarch/pip-25.1.1-pyh8b19718_0.conda#32d0781ace05105cc99af55d36cbec7c +https://conda.anaconda.org/conda-forge/linux-64/polars-1.17.1-py312hda0fa55_1.conda#d9d77bfc286b6044dc045d1696c6acdc diff --git a/modules/local/idmapping/gprofiler/environment.yml b/modules/local/idmapping/gprofiler/environment.yml deleted file mode 100644 index aa931b5e..00000000 --- a/modules/local/idmapping/gprofiler/environment.yml +++ /dev/null @@ -1,7 +0,0 @@ -name: idmapping_gprofiler -channels: - - conda-forge -dependencies: - - conda-forge::python==3.12.8 - - conda-forge::pandas==2.2.3 - - conda-forge::requests==2.32.3 diff --git a/modules/local/idmapping/gprofiler/main.nf b/modules/local/idmapping/gprofiler/main.nf index 4b628300..bb5c8bfa 100644 --- a/modules/local/idmapping/gprofiler/main.nf +++ b/modules/local/idmapping/gprofiler/main.nf @@ -25,7 +25,7 @@ process IDMAPPING_GPROFILER { } } - conda "${moduleDir}/environment.yml" + conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/aa/aad4e61f15d97b7c0a24a4e3ee87a11552464fb7110f530e43bdc9acc374cf13/data': 'community.wave.seqera.io/library/pandas_python_requests:8c6da05a2935a952' }" diff --git a/modules/local/idmapping/gprofiler/spec-file.txt b/modules/local/idmapping/gprofiler/spec-file.txt new file mode 100644 index 00000000..1eda65c4 --- /dev/null +++ b/modules/local/idmapping/gprofiler/spec-file.txt @@ -0,0 +1,56 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda#3cd1a7238a0dd3d0860fdefc496cc854 +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda#9e60c55e725c20d23125a5f0dd69af5d +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda#6d11a5edae89fe413c0569f16d308f5a +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda#e66f2b8ad787e7beb0f846e4bd7e8493 +https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62ee74e96c5ebb0af99386de58cf9553 +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_0.conda#e31316a586cac398b1fcdb10ace786b9 +https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda#db0bfbe7dd197b68ad5f30333bae6ce0 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 +https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc +https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda#d864d34357c3b65a4b731f78c0801dc4 +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.2-h6cd9bfd_0.conda#b04c7eda6d7dab1e6503135e7fad4d25 +https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b +https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.6.15-hbd8a1cb_0.conda#72525f07d72806e3b639ad4504c30ce5 +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda#c87df2ab1448ba69169652ab9547082d +https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 +https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 +https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a +https://conda.anaconda.org/conda-forge/linux-64/python-3.12.8-h9e4cc4f_1_cpython.conda#7fd2fd79436d9b473812f14e86746844 +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda#0dfcdc155cf23812a0c9deada86fb723 +https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h2ec8cdc_3.conda#a32e0c069f6c3dcac635f7b0b0dac67e +https://conda.anaconda.org/conda-forge/noarch/certifi-2025.6.15-pyhd8ed1ab_0.conda#781d068df0cc2407d4db0ecfbb29225b +https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda#12c566707c80111f9799308d9e265aef +https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py312h06ac9bb_0.conda#a861504bbea4161a9170b85d4d2be840 +https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.2-pyhd8ed1ab_0.conda#40fe4284b8b5835a9073a645139f35af +https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda#0a802cb9888dd14eeefc611f05c40b6e +https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda#8e6923fc12f1fe8f8c4e5c9f343256ac +https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda#b4754fb1bdcb70c8fd54f918301582c6 +https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda#39a4f67be3286c86d696df570b1201b7 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda#530566b68c3b8ce7eec4cd047eae19fe +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda#bfbca721fd33188ef923dfe9ba172f29 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda#323dc8f259224d13078aaf7ce96c3efe +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-32_h59b9bed_openblas.conda#2af9f3d5c2e39f417ce040f5a35c40c6 +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-32_he106b2a_openblas.conda#3d3f9355e52f269cd8bc2c440d8a5263 +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-32_h7ac8fdf_openblas.conda#6c3f04ccb6c578138e9f9899da0bd714 +https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.1-py312h6cf2f7f_0.conda#7e086a30150af2536a1059885368dcf0 +https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda#a451d576819089b0d672f18768be0f65 +https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 +https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 +https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 +https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.3-py312hf9745cd_3.conda#2979458c23c7755683a0598fb33e7666 +https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e +https://conda.anaconda.org/conda-forge/noarch/wheel-0.45.1-pyhd8ed1ab_1.conda#75cb7132eb58d97896e173ef12ac9986 +https://conda.anaconda.org/conda-forge/noarch/pip-25.1.1-pyh8b19718_0.conda#32d0781ace05105cc99af55d36cbec7c +https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda#461219d1a5bd61342293efa2c0c90eac +https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py312h66e93f0_2.conda#630db208bc7bbb96725ce9832c7423bb +https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda#436c165519e140cb08d246a4472a9d6a +https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda#a9b9368f3701a417eac9edbcae7cb737 diff --git a/modules/local/merge_data/environment.yml b/modules/local/merge_data/environment.yml deleted file mode 100644 index 8ad3830b..00000000 --- a/modules/local/merge_data/environment.yml +++ /dev/null @@ -1,6 +0,0 @@ -name: merge_data -channels: - - conda-forge -dependencies: - - conda-forge::python==3.12.8 - - conda-forge::polars==1.17.1 diff --git a/modules/local/merge_data/main.nf b/modules/local/merge_data/main.nf index e11affc5..ea96e22b 100644 --- a/modules/local/merge_data/main.nf +++ b/modules/local/merge_data/main.nf @@ -2,7 +2,7 @@ process MERGE_DATA { label 'process_low' - conda "${moduleDir}/environment.yml" + conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" diff --git a/modules/local/merge_data/spec-file.txt b/modules/local/merge_data/spec-file.txt new file mode 100644 index 00000000..1bf5d691 --- /dev/null +++ b/modules/local/merge_data/spec-file.txt @@ -0,0 +1,40 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda#3cd1a7238a0dd3d0860fdefc496cc854 +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda#9e60c55e725c20d23125a5f0dd69af5d +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda#e66f2b8ad787e7beb0f846e4bd7e8493 +https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62ee74e96c5ebb0af99386de58cf9553 +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.6.15-hbd8a1cb_0.conda#72525f07d72806e3b639ad4504c30ce5 +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_0.conda#e31316a586cac398b1fcdb10ace786b9 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda#530566b68c3b8ce7eec4cd047eae19fe +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda#bfbca721fd33188ef923dfe9ba172f29 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda#323dc8f259224d13078aaf7ce96c3efe +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-32_h59b9bed_openblas.conda#2af9f3d5c2e39f417ce040f5a35c40c6 +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-32_he106b2a_openblas.conda#3d3f9355e52f269cd8bc2c440d8a5263 +https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda#db0bfbe7dd197b68ad5f30333bae6ce0 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-32_h7ac8fdf_openblas.conda#6c3f04ccb6c578138e9f9899da0bd714 +https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc +https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda#d864d34357c3b65a4b731f78c0801dc4 +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.2-h6cd9bfd_0.conda#b04c7eda6d7dab1e6503135e7fad4d25 +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda#6d11a5edae89fe413c0569f16d308f5a +https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b +https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda#c87df2ab1448ba69169652ab9547082d +https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 +https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 +https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a +https://conda.anaconda.org/conda-forge/linux-64/python-3.12.8-h9e4cc4f_1_cpython.conda#7fd2fd79436d9b473812f14e86746844 +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda#0dfcdc155cf23812a0c9deada86fb723 +https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.1-py312h6cf2f7f_0.conda#7e086a30150af2536a1059885368dcf0 +https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda#58335b26c38bf4a20f399384c33cbcf9 +https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e +https://conda.anaconda.org/conda-forge/noarch/wheel-0.45.1-pyhd8ed1ab_1.conda#75cb7132eb58d97896e173ef12ac9986 +https://conda.anaconda.org/conda-forge/noarch/pip-25.1.1-pyh8b19718_0.conda#32d0781ace05105cc99af55d36cbec7c +https://conda.anaconda.org/conda-forge/linux-64/polars-1.17.1-py312hda0fa55_1.conda#d9d77bfc286b6044dc045d1696c6acdc diff --git a/modules/local/normalisation/deseq2/environment.yml b/modules/local/normalisation/deseq2/environment.yml deleted file mode 100644 index 512715e1..00000000 --- a/modules/local/normalisation/deseq2/environment.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: normalisation_deseq2 -channels: - - conda-forge - - bioconda -dependencies: - - conda-forge::r-base==4.3.3 - - bioconda::bioconductor-deseq2==1.42.0 - - conda-forge::r-optparse==1.7.5 diff --git a/modules/local/normalisation/deseq2/main.nf b/modules/local/normalisation/deseq2/main.nf index 58a525ff..e5e0932c 100644 --- a/modules/local/normalisation/deseq2/main.nf +++ b/modules/local/normalisation/deseq2/main.nf @@ -8,7 +8,7 @@ process NORMALISATION_DESEQ2 { // the subsequent steps will not be run for this dataset errorStrategy { task.exitStatus == 100 ? 'ignore' : 'terminate' } - conda "${moduleDir}/environment.yml" + conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/ce/cef7164b168e74e5db11dcd9acf6172d47ed6753e4814c68f39835d0c6c22f6d/data': 'community.wave.seqera.io/library/bioconductor-deseq2_r-base_r-optparse:c84cd7ffdb298fa7' }" diff --git a/modules/local/normalisation/deseq2/spec-file.txt b/modules/local/normalisation/deseq2/spec-file.txt new file mode 100644 index 00000000..ab36fbb1 --- /dev/null +++ b/modules/local/normalisation/deseq2/spec-file.txt @@ -0,0 +1,190 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda#3cd1a7238a0dd3d0860fdefc496cc854 +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d +https://conda.anaconda.org/conda-forge/noarch/_r-mutex-1.0.1-anacondar_1.tar.bz2#19f9db5f4f1b7f5ef5f6d67207f25f38 +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda#9e60c55e725c20d23125a5f0dd69af5d +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda#e66f2b8ad787e7beb0f846e4bd7e8493 +https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62ee74e96c5ebb0af99386de58cf9553 +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_0.conda#e31316a586cac398b1fcdb10ace786b9 +https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda#db0bfbe7dd197b68ad5f30333bae6ce0 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 +https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc +https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.2-h6cd9bfd_0.conda#b04c7eda6d7dab1e6503135e7fad4d25 +https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.6.15-hbd8a1cb_0.conda#72525f07d72806e3b639ad4504c30ce5 +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda#c87df2ab1448ba69169652ab9547082d +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-7_cp313.conda#e84b44e6300f1703cb25d29120c5b1d8 +https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 +https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 +https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a +https://conda.anaconda.org/conda-forge/linux-64/python-3.13.5-hec9711d_102_cp313.conda#89e07d92cf50743886f41638d58c4328 +https://conda.anaconda.org/conda-forge/noarch/argcomplete-3.6.2-pyhd8ed1ab_0.conda#eb9d4263271ca287d2e0cf5a86da2d3a +https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-3.10.0-he073ed8_18.conda#ad8527bf134a90e1c9ed35fa0b64318c +https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.17-h0157908_18.conda#460eba7851277ec1fd80a1a24080787a +https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.44-h4bf12b8_0.conda#7a1b5c3fbc0419961eaed361eedc90d4 +https://conda.anaconda.org/conda-forge/linux-64/bwidget-1.10.1-ha770c72_1.conda#983b92277d78c0d0ec498e460caa0e6d +https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.50-h943b412_0.conda#51de14db340a848869e69c632b43cca7 +https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.13.3-h48d6fc4_1.conda#3c255be50a506c50765a93a6644f32fe +https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.13.3-ha770c72_1.conda#51f5be229d83ecd401fb369ab96ae669 +https://conda.anaconda.org/conda-forge/linux-64/freetype-2.13.3-ha770c72_1.conda#9ccd736d31e0c6e41f54e704e5312811 +https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.15.0-h7e30c49_1.conda#8f5b0b297b59e1ac160ad4beec99dbee +https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2#0c96522c6bdaed4b1566d11387caaf45 +https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2#34893075a5c9e55cdafac56607368fc6 +https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2#4d59c254e01d9cde7957100457e2d5fb +https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda#49023d73832ef61042f6a237cb2687e7 +https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29 +https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda#6d11a5edae89fe413c0569f16d308f5a +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_3.conda#57541755b5a51691955012b8e197c06c +https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 +https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h4ce23a2_1.conda#e796ff8ddc598affdf7c173d6145f087 +https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.45-hc749103_0.conda#b90bece58b4c2bf25969b70f3be42d25 +https://conda.anaconda.org/conda-forge/linux-64/libglib-2.84.2-h3618099_0.conda#072ab14a02164b7c0c089055368ff776 +https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda#b3c17d95b5a10c6e64a21fa17573e70e +https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb9d3cd8_0.conda#f6ebe2cb3f82ba6c057dde5d9debe4f7 +https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb9d3cd8_0.conda#8035c64cb77ed555e3f150b7b3972480 +https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda#92ed62436b625154323d40d5f2f11dd7 +https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.2-h29eaf8c_0.conda#39b4228a867772d610c02e06f939a5b8 +https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda#fb901ff28063514abb6046c9ec2c4a45 +https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda#1c74ff8c35dcadf952a16f752ca5aa49 +https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.12-h4f16b4b_0.conda#db038ce880f100acc74dba10302b5630 +https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.6-hb9d3cd8_0.conda#febbab7d15033c913d53c7a2c102309d +https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda#96d57aba173e878a2089d5638016dc5e +https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda#09262e66b19567aff4f592fb53b28760 +https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.1-h166bdaf_0.tar.bz2#30186d27e2c9fa62b45fb1476b7200e3 +https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda#c277e0a4d549b03ac1e9d6cbbe3d017b +https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda#3f43953b7d3fb3aaa1d0d0723d91e368 +https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.5-hb9d3cd8_0.conda#f7f0d6cc2dc986d42ac2689ec88192be +https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda#172bf1cd1ff8629f2b1179945ed45055 +https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.64.0-h161d5f1_0.conda#19e57602824042dfd0446292ef90488b +https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda#eecce068c7e4eddeb169591baac20ac4 +https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda#6432cb5d4ac0046c3ac0a8a0f95842f9 +https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.14.1-h332b0f4_0.conda#45f6713cb00f124af300342512219182 +https://conda.anaconda.org/conda-forge/linux-64/curl-8.14.1-h332b0f4_0.conda#60279087a10b4ab59a70daa838894e4b +https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-15.1.0-h4c094af_103.conda#ea67e87d658d31dc33818f9574563269 +https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-15.1.0-h97b714f_3.conda#bbcff9bf972a0437bea8e431e4b327bb +https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-15.1.0-h4393ad2_3.conda#f39f96280dd8b1ec8cbd395a3d3fdd1e +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda#530566b68c3b8ce7eec4cd047eae19fe +https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-15.1.0-h3b9cdf2_3.conda#649c5fe0593a880702e434bc375f3e8a +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda#bfbca721fd33188ef923dfe9ba172f29 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda#323dc8f259224d13078aaf7ce96c3efe +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-32_h59b9bed_openblas.conda#2af9f3d5c2e39f417ce040f5a35c40c6 +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-32_he106b2a_openblas.conda#3d3f9355e52f269cd8bc2c440d8a5263 +https://conda.anaconda.org/conda-forge/linux-64/gsl-2.7-he838d99_0.tar.bz2#fec079ba39c9cca093bf4c00001825de +https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-15.1.0-h4c094af_103.conda#83bbc814f0aeccccb5ea10267bea0d2e +https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-15.1.0-h6a1bac1_3.conda#d71cc504fcfdbee8dd7925ebb9c2bf85 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-15.1.0-h69a702a_3.conda#6e5d0574e57a38c36e674e9a18eee2b4 +https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.0-hb9d3cd8_0.conda#9fa334557db9f63da6c9285fd2a48638 +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-32_h7ac8fdf_openblas.conda#6c3f04ccb6c578138e9f9899da0bd714 +https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda#9344155d33912347b37f0ae6c410a835 +https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.24-h86f0d12_0.conda#64f0c503da58ec25ebd359e4d990afa8 +https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.5.0-h851e524_0.conda#63f790534398730f59e1b899c3644d4a +https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.0-hf01ce69_5.conda#e79a094918988bb1807462cd42c83962 +https://conda.anaconda.org/conda-forge/linux-64/make-4.4.1-hb9d3cd8_2.conda#33405d2a66b1411db9f7242c8b97c9e7 +https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.10-h36c2ea0_0.tar.bz2#ac7bc6a654f8f41b352b38f4051135f8 +https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-h5888daf_0.conda#951ff8d9e5536896408e89d63230b8d5 +https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-11.2.1-h3beb420_0.conda#0e6e192d4b3d95708ad192d957cf3163 +https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.4-hadf4263_0.conda#79f71230c069a287efe3a8614069ddf1 +https://conda.anaconda.org/conda-forge/linux-64/sed-4.9-h6688a6e_0.conda#171afc5f7ca0408bbccbcb69ade85f92 +https://conda.anaconda.org/conda-forge/linux-64/tktable-2.10-h8d826fa_7.conda#3ac51142c19ba95ae0fadefa333c9afb +https://conda.anaconda.org/conda-forge/linux-64/xorg-libxt-1.3.1-hb9d3cd8_0.conda#279b0de5f6ba95457190a1c459a64e31 +https://conda.anaconda.org/conda-forge/linux-64/r-base-4.3.3-h65010dc_18.conda#721ea26859f44b206b0146eae8444657 +https://conda.anaconda.org/bioconda/noarch/bioconductor-biocgenerics-0.48.1-r43hdfd78af_2.tar.bz2#a313dd8a932cfd178fad2f3e7e6a6184 +https://conda.anaconda.org/conda-forge/linux-64/oniguruma-6.9.10-hb9d3cd8_0.conda#6ce853cb231f18576d2db5c2d4cb473e +https://conda.anaconda.org/conda-forge/linux-64/jq-1.8.1-h73b1eb8_0.conda#2714e43bfc035f7ef26796632aa1b523 +https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2#4cb3ad778ec2d5a7acbdf254eb1c42ae +https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py313h8060acc_2.conda#50992ba61a8a1f8c2d346168ae1c86df +https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e +https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda#b0dd904de08b7db706167240bf37b164 +https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.13.3-pyha770c72_0.conda#146402bf0f11cbeb8f781fa4309a95d3 +https://conda.anaconda.org/conda-forge/noarch/xmltodict-0.14.2-pyhd8ed1ab_1.conda#96ef17b8734b174d35346da0762f0137 +https://conda.anaconda.org/conda-forge/noarch/yq-3.4.3-pyhe01879c_2.conda#18cefe7c50c1228da474ea0e95a8e646 +https://conda.anaconda.org/bioconda/noarch/bioconductor-data-packages-20250625-hdfd78af_0.tar.bz2#34d7066b99d7e6769305dcebf0a9de87 +https://conda.anaconda.org/bioconda/noarch/bioconductor-genomeinfodbdata-1.2.11-r43hdfd78af_1.tar.bz2#14721a7fde8cfe4703796dfd5a119d76 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-s4vectors-0.40.2-r43ha9d7317_2.tar.bz2#6aa465e83dabb7ed5b853519d8a334e4 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-iranges-2.36.0-r43ha9d7317_2.tar.bz2#cca51afd40439bea147c1adf9857bec0 +https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.8-h4bc477f_0.conda#14dbe05b929e329dbaa6f2d0aa19466d +https://conda.anaconda.org/conda-forge/linux-64/r-bitops-1.0_9-r43h2b5f3a1_0.conda#8643d84c1d28ea73e48db9deb9a2eff3 +https://conda.anaconda.org/conda-forge/linux-64/r-rcurl-1.98_1.16-r43he8228da_1.conda#e03c3ff98b32efffb620d7dec4df34b1 +https://conda.anaconda.org/bioconda/noarch/bioconductor-genomeinfodb-1.38.1-r43hdfd78af_1.tar.bz2#03e20a01b672b693c9470dec80d83993 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-zlibbioc-1.48.0-r43ha9d7317_2.tar.bz2#b460a5493c1d67ff386a0e63eb078a64 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-xvector-0.42.0-r43ha9d7317_2.tar.bz2#16f45b1c97517cc3d063a442a43689a4 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-genomicranges-1.54.1-r43ha9d7317_2.tar.bz2#01031256b035b2d4a15c14b690be39aa +https://conda.anaconda.org/bioconda/linux-64/bioconductor-biobase-2.62.0-r43ha9d7317_3.tar.bz2#536352cf94bc990f2d723564fe0d6ff9 +https://conda.anaconda.org/conda-forge/linux-64/r-matrixstats-1.5.0-r43h2b5f3a1_0.conda#bbf709a87ed6a14852cb0a4171539a06 +https://conda.anaconda.org/bioconda/noarch/bioconductor-matrixgenerics-1.14.0-r43hdfd78af_3.tar.bz2#c79f36cc0cd464874aefd50a700d0079 +https://conda.anaconda.org/conda-forge/noarch/r-abind-1.4_5-r43hc72bb7e_1006.conda#75d26096ffa98e1cde7b27b9530899a1 +https://conda.anaconda.org/conda-forge/noarch/r-crayon-1.5.3-r43hc72bb7e_1.conda#bafc77be1942ea00228cf18d2cb30e35 +https://conda.anaconda.org/conda-forge/linux-64/r-lattice-0.22_7-r43h2b5f3a1_0.conda#1902233545ef5232dacdd973153d77c4 +https://conda.anaconda.org/conda-forge/linux-64/r-matrix-1.6_5-r43he966344_1.conda#df8a1175a62460e02dbf340966cbfeab +https://conda.anaconda.org/bioconda/linux-64/bioconductor-s4arrays-1.2.0-r43ha9d7317_2.tar.bz2#28fd3fe7fd8d087c1cfa7805bbd16661 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-sparsearray-1.2.2-r43ha9d7317_2.tar.bz2#41f1e8c1cfb7ff594e923c05e02d9ecb +https://conda.anaconda.org/bioconda/linux-64/bioconductor-delayedarray-0.28.0-r43ha9d7317_2.tar.bz2#cec6a218547ee2af2b823957a373a655 +https://conda.anaconda.org/bioconda/noarch/bioconductor-summarizedexperiment-1.32.0-r43hdfd78af_0.tar.bz2#6bed161da6d64cef9f9ebd5fbc2452e7 +https://conda.anaconda.org/conda-forge/linux-64/r-bdsmatrix-1.3_7-r43h2b5f3a1_2.conda#ac4eb8896121a376698bccf410c51cf6 +https://conda.anaconda.org/conda-forge/linux-64/r-mass-7.3_60.0.1-r43hb1dbf0f_1.conda#c3c9184486ccabe19b86aba11351652e +https://conda.anaconda.org/conda-forge/linux-64/r-mvtnorm-1.3_3-r43h9ad1c49_0.conda#53e04c32e1d4cba4181832befe4601f8 +https://conda.anaconda.org/conda-forge/noarch/r-numderiv-2016.8_1.1-r43hc72bb7e_6.conda#f9bd335fa3579f2e0ed2cdd315fc05ed +https://conda.anaconda.org/conda-forge/noarch/r-bbmle-1.0.25.1-r43hc72bb7e_1.conda#f4dba61e861b8c2459ebf5caa575c495 +https://conda.anaconda.org/conda-forge/noarch/r-coda-0.19_4.1-r43hc72bb7e_1.conda#675d29e567d6eced1089f695d19cfff3 +https://conda.anaconda.org/conda-forge/linux-64/r-rcpp-1.1.0-r43h93ab643_0.conda#b10c60cf4d65df16b0fe2a17e2324375 +https://conda.anaconda.org/conda-forge/linux-64/r-plyr-1.8.9-r43ha18555a_1.conda#d93aedee4cc78f78413969b1e891842c +https://conda.anaconda.org/conda-forge/noarch/r-emdbook-1.3.13-r43hc72bb7e_1.conda#c6d8d2535e70b1bbf3d7ceecf6f60bcc +https://conda.anaconda.org/conda-forge/linux-64/r-rcppeigen-0.3.4.0.2-r43hb79369c_0.conda#02aedbcf8e80e09bd14a6512344993bf +https://conda.anaconda.org/conda-forge/linux-64/r-rcppnumerical-0.6_0-r43h0d4f4ea_1.conda#be66552558a5d23eff73aeb0784205d6 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-apeglm-1.24.0-r43hf17093f_1.tar.bz2#e3f97df1d5f32a3eb3472ec85344c9f3 +https://conda.anaconda.org/conda-forge/noarch/r-bh-1.87.0_1-r43hc72bb7e_0.conda#9e6364aa396f48b73fb81d56b44ceedc +https://conda.anaconda.org/conda-forge/noarch/r-codetools-0.2_20-r43hc72bb7e_1.conda#f54a935134de901af63096b29a56697e +https://conda.anaconda.org/conda-forge/noarch/r-cpp11-0.5.2-r43h785f33e_1.conda#7bc23dbad7c6015d9b2b9c59bb3e5d85 +https://conda.anaconda.org/conda-forge/noarch/r-futile.options-1.0.1-r43hc72bb7e_1005.conda#57962626cdffa616861bb383076195a2 +https://conda.anaconda.org/conda-forge/noarch/r-formatr-1.14-r43hc72bb7e_2.conda#20d39b48868b55b5335a0c578fdda15b +https://conda.anaconda.org/conda-forge/noarch/r-lambda.r-1.2.4-r43hc72bb7e_4.conda#bf0eed6164eb10fefeda18059a78193c +https://conda.anaconda.org/conda-forge/noarch/r-futile.logger-1.4.3-r43hc72bb7e_1006.conda#dbfd04b54b6ac781070393f2184b3c6d +https://conda.anaconda.org/conda-forge/noarch/r-snow-0.4_4-r43hc72bb7e_3.conda#60eeeef67921f38a80c1778eae3bbbb9 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-biocparallel-1.36.0-r43hf17093f_2.tar.bz2#6e03fcbba328db0b9f2a722ce663e916 +https://conda.anaconda.org/conda-forge/noarch/r-etrunct-0.1-r43hc72bb7e_1006.conda#0db2b2af6060135475a77e6a4a366c0c +https://conda.anaconda.org/conda-forge/noarch/r-invgamma-1.2-r43hc72bb7e_0.conda#5c23063ced21dcb74c6c6d65a996848a +https://conda.anaconda.org/conda-forge/linux-64/r-irlba-2.3.5.1-r43h0d28552_3.conda#9d0d3d499b5670b3dc626ba1d51ebe0c +https://conda.anaconda.org/conda-forge/linux-64/r-rcpparmadillo-14.4.2_1-r43hc2d650c_0.conda#a9ded0b699d83238bcb9ba6949924fce +https://conda.anaconda.org/conda-forge/linux-64/r-mixsqp-0.3_54-r43hb79369c_3.conda#e61f97f4dac929092d11a0a98eacd1b6 +https://conda.anaconda.org/conda-forge/noarch/r-squarem-2021.1-r43hc72bb7e_3.conda#7540130cd26e12a02742dac9ddc184d6 +https://conda.anaconda.org/conda-forge/linux-64/r-truncnorm-1.0_9-r43h2b5f3a1_4.conda#c7a9a8c285e9c8e49efa16081b7b54d6 +https://conda.anaconda.org/conda-forge/linux-64/r-ashr-2.2_63-r43h93ab643_2.conda#ed9e685349325869eb0d716bf4db2f68 +https://conda.anaconda.org/conda-forge/linux-64/r-cli-3.6.5-r43h93ab643_0.conda#5d49a07fdd4ca869c4a79082692c4d2a +https://conda.anaconda.org/conda-forge/linux-64/r-glue-1.8.0-r43h2b5f3a1_0.conda#381d612db7519f2a54f1b187e738ac7b +https://conda.anaconda.org/conda-forge/linux-64/r-rlang-1.1.6-r43h93ab643_0.conda#057b78b5adfffc99092504a1da563abe +https://conda.anaconda.org/conda-forge/noarch/r-lifecycle-1.0.4-r43hc72bb7e_1.conda#7a0a8ba1fe2cf12b39062d8291e2fca8 +https://conda.anaconda.org/conda-forge/noarch/r-gtable-0.3.6-r43hc72bb7e_0.conda#08f643f31ac131aa067e42ad5f832313 +https://conda.anaconda.org/conda-forge/linux-64/r-isoband-0.2.7-r43ha18555a_3.conda#39459e8609d9461d90ee0683a7fd2f3a +https://conda.anaconda.org/conda-forge/linux-64/r-nlme-3.1_168-r43hb67ce94_0.conda#4add921d1d71c646c037a91202d0f75f +https://conda.anaconda.org/conda-forge/linux-64/r-mgcv-1.9_3-r43h2ae2be5_0.conda#66398dfe29e3bc9c415393ddb4ea864c +https://conda.anaconda.org/conda-forge/linux-64/r-farver-2.1.2-r43ha18555a_1.conda#85a82a5b78397daf57f002120aed9e3e +https://conda.anaconda.org/conda-forge/noarch/r-labeling-0.4.3-r43hc72bb7e_1.conda#0464c37b6ff6701cbb8606e8f4bfebe4 +https://conda.anaconda.org/conda-forge/linux-64/r-colorspace-2.1_1-r43hdb488b9_0.conda#0c6d4c26ca41246a4053d79e1b4d78ff +https://conda.anaconda.org/conda-forge/noarch/r-munsell-0.5.1-r43hc72bb7e_1.conda#8b2f9bb8064ae0896ffedd984661a2d5 +https://conda.anaconda.org/conda-forge/noarch/r-r6-2.6.1-r43hc72bb7e_0.conda#be02712c703445dc5cabbe0f22d0d063 +https://conda.anaconda.org/conda-forge/noarch/r-rcolorbrewer-1.1_3-r43h785f33e_3.conda#ceb1c167b7d9e5eefed0ecbe759540de +https://conda.anaconda.org/conda-forge/noarch/r-viridislite-0.4.2-r43hc72bb7e_2.conda#2a5b8c2803b5714f3319a238c66cc9e7 +https://conda.anaconda.org/conda-forge/noarch/r-scales-1.4.0-r43hc72bb7e_0.conda#ba5fa427e6421aded56f95bd925e3572 +https://conda.anaconda.org/conda-forge/linux-64/r-fansi-1.0.6-r43hb1dbf0f_1.conda#4c17a0f74a974316fdfafa5a9fe91b52 +https://conda.anaconda.org/conda-forge/linux-64/r-magrittr-2.0.3-r43hb1dbf0f_3.conda#fc61bcf37e59037b486c8841a704e9da +https://conda.anaconda.org/conda-forge/linux-64/r-ellipsis-0.3.2-r43hb1dbf0f_3.conda#b8349582a31b17184a7674f4c847a5ad +https://conda.anaconda.org/conda-forge/linux-64/r-utf8-1.2.6-r43h2b5f3a1_0.conda#a2b3283964103f1ff47d6acea6f69e24 +https://conda.anaconda.org/conda-forge/linux-64/r-vctrs-0.6.5-r43h0d4f4ea_1.conda#7f4c30bb576acec2a682c40790c2d406 +https://conda.anaconda.org/conda-forge/noarch/r-pillar-1.11.0-r43hc72bb7e_0.conda#657672af86f156a821b7a8d5ac88f916 +https://conda.anaconda.org/conda-forge/noarch/r-pkgconfig-2.0.3-r43hc72bb7e_4.conda#509adf7f5bc34d77064f28f487d7fa6e +https://conda.anaconda.org/conda-forge/linux-64/r-tibble-3.3.0-r43h2b5f3a1_0.conda#e12f4dc87c2ab21d2e4384cbf8f42111 +https://conda.anaconda.org/conda-forge/noarch/r-withr-3.0.2-r43hc72bb7e_0.conda#e503cae9a96ad7771fa6ccd3af90477b +https://conda.anaconda.org/conda-forge/noarch/r-ggplot2-3.5.2-r43hc72bb7e_0.conda#0245640d7215b4e4f1f07ce7cb08378f +https://conda.anaconda.org/conda-forge/linux-64/r-locfit-1.5_9.12-r43h2b5f3a1_0.conda#94bb7f425967b333ba97ce778b5a2efc +https://conda.anaconda.org/bioconda/linux-64/bioconductor-deseq2-1.42.0-r43hf17093f_2.tar.bz2#f600800873b9b0d08c42215182fc88b1 +https://conda.anaconda.org/conda-forge/noarch/pip-25.1.1-pyh145f28c_0.conda#01384ff1639c6330a0924791413b8714 +https://conda.anaconda.org/conda-forge/noarch/r-getopt-1.20.4-r43ha770c72_1.conda#cf6793c369dbc7ef63d9c1bc9b186615 +https://conda.anaconda.org/conda-forge/noarch/r-optparse-1.7.5-r43hc72bb7e_1.conda#ae32080aac0f74e73e7cd6e774db1c73 diff --git a/modules/local/normalisation/edger/environment.yml b/modules/local/normalisation/edger/environment.yml deleted file mode 100644 index cd4944ab..00000000 --- a/modules/local/normalisation/edger/environment.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: normalisation_edger -channels: - - conda-forge - - bioconda -dependencies: - - conda-forge::r-base==4.3.3 - - bioconda::bioconductor-edger==4.0.16 - - conda-forge::r-optparse==1.7.5 diff --git a/modules/local/normalisation/edger/main.nf b/modules/local/normalisation/edger/main.nf index 43735c7c..77bec625 100644 --- a/modules/local/normalisation/edger/main.nf +++ b/modules/local/normalisation/edger/main.nf @@ -8,7 +8,7 @@ process NORMALISATION_EDGER { // the subsequent steps will not be run for this dataset errorStrategy { task.exitStatus == 100 ? 'ignore' : 'terminate' } - conda "${moduleDir}/environment.yml" + conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/89/89bbc9544e18b624ed6d0a30e701cf8cec63e063cc9b5243e1efde362fe92228/data': 'community.wave.seqera.io/library/bioconductor-edger_r-base_r-optparse:400aaabddeea1574' }" diff --git a/modules/local/normalisation/edger/spec-file.txt b/modules/local/normalisation/edger/spec-file.txt new file mode 100644 index 00000000..54a7d740 --- /dev/null +++ b/modules/local/normalisation/edger/spec-file.txt @@ -0,0 +1,101 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda#3cd1a7238a0dd3d0860fdefc496cc854 +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d +https://conda.anaconda.org/conda-forge/noarch/_r-mutex-1.0.1-anacondar_1.tar.bz2#19f9db5f4f1b7f5ef5f6d67207f25f38 +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_0.conda#e31316a586cac398b1fcdb10ace786b9 +https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-3.10.0-he073ed8_18.conda#ad8527bf134a90e1c9ed35fa0b64318c +https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a +https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.17-h0157908_18.conda#460eba7851277ec1fd80a1a24080787a +https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.44-h4bf12b8_0.conda#7a1b5c3fbc0419961eaed361eedc90d4 +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda#9e60c55e725c20d23125a5f0dd69af5d +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda#530566b68c3b8ce7eec4cd047eae19fe +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda#bfbca721fd33188ef923dfe9ba172f29 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda#323dc8f259224d13078aaf7ce96c3efe +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-32_h59b9bed_openblas.conda#2af9f3d5c2e39f417ce040f5a35c40c6 +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda#e66f2b8ad787e7beb0f846e4bd7e8493 +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-32_h7ac8fdf_openblas.conda#6c3f04ccb6c578138e9f9899da0bd714 +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 +https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 +https://conda.anaconda.org/conda-forge/linux-64/bwidget-1.10.1-ha770c72_1.conda#983b92277d78c0d0ec498e460caa0e6d +https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62ee74e96c5ebb0af99386de58cf9553 +https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.50-h943b412_0.conda#51de14db340a848869e69c632b43cca7 +https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.13.3-h48d6fc4_1.conda#3c255be50a506c50765a93a6644f32fe +https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.13.3-ha770c72_1.conda#51f5be229d83ecd401fb369ab96ae669 +https://conda.anaconda.org/conda-forge/linux-64/freetype-2.13.3-ha770c72_1.conda#9ccd736d31e0c6e41f54e704e5312811 +https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda#db0bfbe7dd197b68ad5f30333bae6ce0 +https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b +https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.15.0-h7e30c49_1.conda#8f5b0b297b59e1ac160ad4beec99dbee +https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2#0c96522c6bdaed4b1566d11387caaf45 +https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2#34893075a5c9e55cdafac56607368fc6 +https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2#4d59c254e01d9cde7957100457e2d5fb +https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda#49023d73832ef61042f6a237cb2687e7 +https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29 +https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda#6d11a5edae89fe413c0569f16d308f5a +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_3.conda#57541755b5a51691955012b8e197c06c +https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 +https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h4ce23a2_1.conda#e796ff8ddc598affdf7c173d6145f087 +https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.45-hc749103_0.conda#b90bece58b4c2bf25969b70f3be42d25 +https://conda.anaconda.org/conda-forge/linux-64/libglib-2.84.2-h3618099_0.conda#072ab14a02164b7c0c089055368ff776 +https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda#b3c17d95b5a10c6e64a21fa17573e70e +https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb9d3cd8_0.conda#f6ebe2cb3f82ba6c057dde5d9debe4f7 +https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb9d3cd8_0.conda#8035c64cb77ed555e3f150b7b3972480 +https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda#92ed62436b625154323d40d5f2f11dd7 +https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.2-h29eaf8c_0.conda#39b4228a867772d610c02e06f939a5b8 +https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda#fb901ff28063514abb6046c9ec2c4a45 +https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda#1c74ff8c35dcadf952a16f752ca5aa49 +https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.12-h4f16b4b_0.conda#db038ce880f100acc74dba10302b5630 +https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.6-hb9d3cd8_0.conda#febbab7d15033c913d53c7a2c102309d +https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda#96d57aba173e878a2089d5638016dc5e +https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda#09262e66b19567aff4f592fb53b28760 +https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.1-h166bdaf_0.tar.bz2#30186d27e2c9fa62b45fb1476b7200e3 +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 +https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda#c277e0a4d549b03ac1e9d6cbbe3d017b +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.6.15-hbd8a1cb_0.conda#72525f07d72806e3b639ad4504c30ce5 +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda#c87df2ab1448ba69169652ab9547082d +https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda#3f43953b7d3fb3aaa1d0d0723d91e368 +https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.5-hb9d3cd8_0.conda#f7f0d6cc2dc986d42ac2689ec88192be +https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda#172bf1cd1ff8629f2b1179945ed45055 +https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.64.0-h161d5f1_0.conda#19e57602824042dfd0446292ef90488b +https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda#eecce068c7e4eddeb169591baac20ac4 +https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda#6432cb5d4ac0046c3ac0a8a0f95842f9 +https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.14.1-h332b0f4_0.conda#45f6713cb00f124af300342512219182 +https://conda.anaconda.org/conda-forge/linux-64/curl-8.14.1-h332b0f4_0.conda#60279087a10b4ab59a70daa838894e4b +https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-15.1.0-h4c094af_103.conda#ea67e87d658d31dc33818f9574563269 +https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-15.1.0-h97b714f_3.conda#bbcff9bf972a0437bea8e431e4b327bb +https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-15.1.0-h4393ad2_3.conda#f39f96280dd8b1ec8cbd395a3d3fdd1e +https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-15.1.0-h3b9cdf2_3.conda#649c5fe0593a880702e434bc375f3e8a +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-32_he106b2a_openblas.conda#3d3f9355e52f269cd8bc2c440d8a5263 +https://conda.anaconda.org/conda-forge/linux-64/gsl-2.7-he838d99_0.tar.bz2#fec079ba39c9cca093bf4c00001825de +https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-15.1.0-h4c094af_103.conda#83bbc814f0aeccccb5ea10267bea0d2e +https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-15.1.0-h6a1bac1_3.conda#d71cc504fcfdbee8dd7925ebb9c2bf85 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-15.1.0-h69a702a_3.conda#6e5d0574e57a38c36e674e9a18eee2b4 +https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.0-hb9d3cd8_0.conda#9fa334557db9f63da6c9285fd2a48638 +https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc +https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda#9344155d33912347b37f0ae6c410a835 +https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.24-h86f0d12_0.conda#64f0c503da58ec25ebd359e4d990afa8 +https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.5.0-h851e524_0.conda#63f790534398730f59e1b899c3644d4a +https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.0-hf01ce69_5.conda#e79a094918988bb1807462cd42c83962 +https://conda.anaconda.org/conda-forge/linux-64/make-4.4.1-hb9d3cd8_2.conda#33405d2a66b1411db9f7242c8b97c9e7 +https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.10-h36c2ea0_0.tar.bz2#ac7bc6a654f8f41b352b38f4051135f8 +https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-h5888daf_0.conda#951ff8d9e5536896408e89d63230b8d5 +https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-11.2.1-h3beb420_0.conda#0e6e192d4b3d95708ad192d957cf3163 +https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.4-hadf4263_0.conda#79f71230c069a287efe3a8614069ddf1 +https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 +https://conda.anaconda.org/conda-forge/linux-64/sed-4.9-h6688a6e_0.conda#171afc5f7ca0408bbccbcb69ade85f92 +https://conda.anaconda.org/conda-forge/linux-64/tktable-2.10-h8d826fa_7.conda#3ac51142c19ba95ae0fadefa333c9afb +https://conda.anaconda.org/conda-forge/linux-64/xorg-libxt-1.3.1-hb9d3cd8_0.conda#279b0de5f6ba95457190a1c459a64e31 +https://conda.anaconda.org/conda-forge/linux-64/r-base-4.3.3-h65010dc_18.conda#721ea26859f44b206b0146eae8444657 +https://conda.anaconda.org/conda-forge/linux-64/r-statmod-1.5.0-r43ha36c22a_2.conda#d1b3431cbf858fec53e7eb00f8b8cde0 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-limma-3.58.1-r43ha9d7317_1.tar.bz2#c8af3f878cedd1c3c4b6a61a722cddc0 +https://conda.anaconda.org/conda-forge/linux-64/r-lattice-0.22_7-r43h2b5f3a1_0.conda#1902233545ef5232dacdd973153d77c4 +https://conda.anaconda.org/conda-forge/linux-64/r-locfit-1.5_9.12-r43h2b5f3a1_0.conda#94bb7f425967b333ba97ce778b5a2efc +https://conda.anaconda.org/conda-forge/linux-64/r-rcpp-1.1.0-r43h93ab643_0.conda#b10c60cf4d65df16b0fe2a17e2324375 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-edger-4.0.16-r43hf17093f_1.tar.bz2#7b499c193120f59dc5f034a069ab277b +https://conda.anaconda.org/conda-forge/noarch/r-getopt-1.20.4-r43ha770c72_1.conda#cf6793c369dbc7ef63d9c1bc9b186615 +https://conda.anaconda.org/conda-forge/noarch/r-optparse-1.7.5-r43hc72bb7e_1.conda#ae32080aac0f74e73e7cd6e774db1c73 diff --git a/modules/local/quantile_normalisation/environment.yml b/modules/local/quantile_normalisation/environment.yml deleted file mode 100644 index 72ffff31..00000000 --- a/modules/local/quantile_normalisation/environment.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: quant_norm -channels: - - conda-forge -dependencies: - - conda-forge::python==3.12.8 - - conda-forge::pandas==2.2.3 - - conda-forge::scikit-learn==1.6.1 - - conda-forge::pyarrow==19.0.0 diff --git a/modules/local/quantile_normalisation/main.nf b/modules/local/quantile_normalisation/main.nf index 1f8471a5..80609383 100644 --- a/modules/local/quantile_normalisation/main.nf +++ b/modules/local/quantile_normalisation/main.nf @@ -4,7 +4,7 @@ process QUANTILE_NORMALISATION { tag "${meta.dataset}" - conda "${moduleDir}/environment.yml" + conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/2d/2df931a4ea181fe1ea9527abe0fd4aff9453d6ea56d56aee7c4ac5dceed611e3/data': 'community.wave.seqera.io/library/pandas_pyarrow_python_scikit-learn:6f85e3c4d1706e81' }" diff --git a/modules/local/quantile_normalisation/spec-file.txt b/modules/local/quantile_normalisation/spec-file.txt new file mode 100644 index 00000000..702e03ac --- /dev/null +++ b/modules/local/quantile_normalisation/spec-file.txt @@ -0,0 +1,110 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda#3cd1a7238a0dd3d0860fdefc496cc854 +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda#9e60c55e725c20d23125a5f0dd69af5d +https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.10.6-hb9d3cd8_0.conda#d7d4680337a14001b0e043e96529409b +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.6.15-hbd8a1cb_0.conda#72525f07d72806e3b639ad4504c30ce5 +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda#c87df2ab1448ba69169652ab9547082d +https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.8.1-h1a47875_3.conda#55a8561fdbbbd34f50f57d9be12ed084 +https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.3.0-h4e1184b_5.conda#3f4c1197462a6df2be6dc8241828fe93 +https://conda.anaconda.org/conda-forge/linux-64/s2n-1.5.11-h072c03f_0.conda#5e8060d52f676a40edef0006a75c718f +https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.15.3-h173a860_6.conda#9a063178f1af0a898526cc24ba7be486 +https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.9.2-hefd7a92_4.conda#5ce4df662d32d3123ea8da15571b6f51 +https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.2.2-h4e1184b_0.conda#dcd498d493818b776a77fbc242fbf8e4 +https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.8.1-h205f482_0.conda#9c500858e88df50af3cc883d194de78a +https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.2.2-h4e1184b_4.conda#74e8c3e4df4ceae34aa2959df4b28101 +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda#6d11a5edae89fe413c0569f16d308f5a +https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.5.0-h7959bf6_11.conda#9b3fb60fe57925a92f399bc3fc42eccf +https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.11.0-h11f4f37_12.conda#96c3e0221fa2da97619ee82faa341a73 +https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.7.9-he1b24dc_1.conda#caafc32928a5f7f3f7ef67d287689144 +https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.29.9-he0e7f3f_2.conda#8a4e6fc8a3b285536202b5456a74a940 +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda#e66f2b8ad787e7beb0f846e4bd7e8493 +https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.1-h166bdaf_0.tar.bz2#30186d27e2c9fa62b45fb1476b7200e3 +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 +https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda#c277e0a4d549b03ac1e9d6cbbe3d017b +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_3.conda#57541755b5a51691955012b8e197c06c +https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda#3f43953b7d3fb3aaa1d0d0723d91e368 +https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.5-hb9d3cd8_0.conda#f7f0d6cc2dc986d42ac2689ec88192be +https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda#172bf1cd1ff8629f2b1179945ed45055 +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 +https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.64.0-h161d5f1_0.conda#19e57602824042dfd0446292ef90488b +https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda#eecce068c7e4eddeb169591baac20ac4 +https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda#6432cb5d4ac0046c3ac0a8a0f95842f9 +https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.14.1-h332b0f4_0.conda#45f6713cb00f124af300342512219182 +https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.489-h4d475cb_0.conda#b775e9f46dfa94b228a81d8e8c6d8b1d +https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.14.0-h5cfcd09_0.conda#0a8838771cc2e985cd295e01ae83baf1 +https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.10.0-h113e628_0.conda#73f73f60854f325a55f1d31459f2ab73 +https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 +https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h4ce23a2_1.conda#e796ff8ddc598affdf7c173d6145f087 +https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc +https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.8-h4bc477f_0.conda#14dbe05b929e329dbaa6f2d0aa19466d +https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.8.0-h736e048_1.conda#13de36be8de3ae3f05ba127631599213 +https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.13.0-h3cf044e_1.conda#7eb66060455c7a47d9dcdbfa9f46579b +https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.12.0-ha633028_1.conda#7c1980f89dd41b097549782121a73490 +https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62ee74e96c5ebb0af99386de58cf9553 +https://conda.anaconda.org/conda-forge/linux-64/gflags-2.2.2-h5888daf_1005.conda#d411fc29e338efb48c5fd4576d71d881 +https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hbabe93e_0.conda#ff862eebdfeb2fd048ae9dc92510baca +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_0.conda#e31316a586cac398b1fcdb10ace786b9 +https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda#db0bfbe7dd197b68ad5f30333bae6ce0 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 +https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda#d864d34357c3b65a4b731f78c0801dc4 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.2-h6cd9bfd_0.conda#b04c7eda6d7dab1e6503135e7fad4d25 +https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b +https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc +https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 +https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 +https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a +https://conda.anaconda.org/conda-forge/linux-64/python-3.12.8-h9e4cc4f_1_cpython.conda#7fd2fd79436d9b473812f14e86746844 +https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e +https://conda.anaconda.org/conda-forge/noarch/joblib-1.5.1-pyhd8ed1ab_0.conda#fb1c14694de51a476ce8636d92b6f42c +https://conda.anaconda.org/conda-forge/linux-64/libabseil-20240722.0-cxx17_hbbce691_4.conda#488f260ccda0afaf08acb286db439c2f +https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.1.0-hb9d3cd8_3.conda#cb98af5db26e3f482bebb80ce9d947d3 +https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.1.0-hb9d3cd8_3.conda#1c6eecffad553bde44c5238770cfb7da +https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.1.0-hb9d3cd8_3.conda#3facafe58f3858eb95527c7d3a3fc578 +https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-5.28.3-h6128344_1.conda#d8703f1ffe5a06356f06467f1d0b9464 +https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2024.07.02-hbbce691_2.conda#b2fede24428726dd867611664fb372e8 +https://conda.anaconda.org/conda-forge/linux-64/re2-2024.07.02-h9925aae_2.conda#e84ddf12bde691e8ec894b00ea829ddf +https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.67.1-h25350d4_2.conda#bfcedaf5f9b003029cc6abe9431f66bf +https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.35.0-h2b5623c_0.conda#1040ab07d7af9f23cf2466ffe4e58db1 +https://conda.anaconda.org/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2#c965a5aa0d5c1c37ffc62dff36e28400 +https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-2.35.0-h0121fbd_0.conda#34e2243e0428aac6b3e903ef99b6d57d +https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-headers-1.18.0-ha770c72_1.conda#4fb055f57404920a43b147031471e03b +https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h3f2d84a_0.conda#d76872d096d063e226482c99337209dc +https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.1-hb9d3cd8_2.conda#c9f075ab2f33b3bbee9e62d4ad0a6cd8 +https://conda.anaconda.org/conda-forge/linux-64/prometheus-cpp-1.3.0-ha5d0236_0.conda#a83f6a2fdc079e643237887a37460668 +https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-1.18.0-hfcad708_1.conda#1f5a5d66e77a39dc5bd639ec953705cf +https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.10.0-h202a827_0.conda#0f98f3e95272d118f7931b6bef69bfe5 +https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda#9de5350a85c4a20c685259b889aa6393 +https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.1-h8bd8927_1.conda#3b3e64af585eadfb52bb90b553db5edf +https://conda.anaconda.org/conda-forge/linux-64/orc-2.0.3-h12ee42a_2.conda#4f6f9f3f80354ad185e276c120eac3f0 +https://conda.anaconda.org/conda-forge/linux-64/libarrow-19.0.0-hfa2a6e7_9_cpu.conda#9e09f9cd5c0eb584be78ebea4e0db151 +https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-19.0.0-hcb10f89_9_cpu.conda#cd6e5cd25096e02ddc591f0bc7d0354b +https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda#a1cfcc585f0c42bf8d5546bb1dfb668d +https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.21.0-h0e7cc3e_0.conda#dcb95c0a98ba9ff737f7ae482aef7833 +https://conda.anaconda.org/conda-forge/linux-64/libparquet-19.0.0-h081d1f1_9_cpu.conda#de0b82dc1e9f6f9fb66306f0a15e16fa +https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-19.0.0-hcb10f89_9_cpu.conda#da890e33d20acb4713e73ed841d5088b +https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-19.0.0-h08228c5_9_cpu.conda#9d6c1688d87aeb9fc4513bde60d2f2f3 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda#530566b68c3b8ce7eec4cd047eae19fe +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda#bfbca721fd33188ef923dfe9ba172f29 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda#323dc8f259224d13078aaf7ce96c3efe +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-32_h59b9bed_openblas.conda#2af9f3d5c2e39f417ce040f5a35c40c6 +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-32_he106b2a_openblas.conda#3d3f9355e52f269cd8bc2c440d8a5263 +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-32_h7ac8fdf_openblas.conda#6c3f04ccb6c578138e9f9899da0bd714 +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda#0dfcdc155cf23812a0c9deada86fb723 +https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.1-py312h6cf2f7f_0.conda#7e086a30150af2536a1059885368dcf0 +https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda#a451d576819089b0d672f18768be0f65 +https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 +https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 +https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 +https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.3-py312hf9745cd_3.conda#2979458c23c7755683a0598fb33e7666 +https://conda.anaconda.org/conda-forge/noarch/wheel-0.45.1-pyhd8ed1ab_1.conda#75cb7132eb58d97896e173ef12ac9986 +https://conda.anaconda.org/conda-forge/noarch/pip-25.1.1-pyh8b19718_0.conda#32d0781ace05105cc99af55d36cbec7c +https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-19.0.0-py312h01725c0_0_cpu.conda#7ab1143b9ac1af5cc4a630706f643627 +https://conda.anaconda.org/conda-forge/linux-64/pyarrow-19.0.0-py312h7900ff3_0.conda#14f86e63b5c214dd9fb34e5472d4bafc +https://conda.anaconda.org/conda-forge/linux-64/scipy-1.16.0-py312hf734454_0.conda#7513ac56209d27a85ffa1582033f10a8 +https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda#9d64911b31d57ca443e9f1e36b04385f +https://conda.anaconda.org/conda-forge/linux-64/scikit-learn-1.6.1-py312h7a48858_0.conda#102727f71df02a51e9e173f2e6f87d57 From 754f5d4cd22b2264dac739a46f34724d8062c4d6 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Tue, 8 Jul 2025 09:00:07 +0200 Subject: [PATCH 026/258] update --- .../formatters/schema/parameter/datasets.py | 6 +- .../nf_core_stableexpression.boilerplate.xml | 6 +- galaxy/{ => tool}/.shed.yml | 2 +- galaxy/tool/nf_core_stableexpression.xml | 14 +- galaxy/tool/tool.xml | 178 ------------------ 5 files changed, 14 insertions(+), 192 deletions(-) rename galaxy/{ => tool}/.shed.yml (96%) delete mode 100644 galaxy/tool/tool.xml diff --git a/galaxy/build/formatters/schema/parameter/datasets.py b/galaxy/build/formatters/schema/parameter/datasets.py index 067ac516..19e8b42b 100644 --- a/galaxy/build/formatters/schema/parameter/datasets.py +++ b/galaxy/build/formatters/schema/parameter/datasets.py @@ -17,7 +17,7 @@ def get_input(self) -> str: ).replace(self.param, "samplesheet") # changing label input_param_str = re.sub( - r'label="[\s\w]*"', 'label="Samplesheet"', input_param_str + r'label="[\s\w]*"', 'format="csv" label="Samplesheet"', input_param_str ) # adding conditional statement @@ -25,8 +25,8 @@ def get_input(self) -> str: {input_param_str} - - + + diff --git a/galaxy/build/static/nf_core_stableexpression.boilerplate.xml b/galaxy/build/static/nf_core_stableexpression.boilerplate.xml index d24fc740..15fa0b6d 100644 --- a/galaxy/build/static/nf_core_stableexpression.boilerplate.xml +++ b/galaxy/build/static/nf_core_stableexpression.boilerplate.xml @@ -1,13 +1,13 @@ DESCRIPTION - nextflow apptainer openjdk + Pipeline dedicated to finding the most stable genes across count datasets - nextflow apptainer openjdk + - - - + + + @@ -108,7 +108,7 @@ VERSION="1.0dev"; echo "$VERSION"
    - ([a-zA-Z,]+) + ([a-zA-Z,]+) ([A-Z0-9-]+,?)+ diff --git a/galaxy/tool/tool.xml b/galaxy/tool/tool.xml deleted file mode 100644 index 3993bf13..00000000 --- a/galaxy/tool/tool.xml +++ /dev/null @@ -1,178 +0,0 @@ - - Pipeline dedicated to finding the most stable genes across count datasets - - - nextflow - apptainer - openjdk - - - - -
    - - ([a-zA-Z]+)[_ ]([a-zA-Z]+) - - - - - - - - - - - - -
    -
    - - ([a-zA-Z,]+) - - - ([A-Z0-9-]+,?)+ - - - - - ([A-Z0-9-]+,?)+ - - -
    -
    - - - -
    -
    - - - - - - -
    -
    - - - - - - - - - - - - - - - - - - - - - - @misc{nf-core/stableexpression, - author = {Coen, Olivier}, - year = {2025}, - title = {nf-core/stableexpression}, - publisher = {GitHub}, - journal = {GitHub repository}, - url = {https://github.com/OlivierCoen/stableexpression}, - } - - -
    From 337af8be00ce276a949aa2773b125f602815d358 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 9 Jul 2025 15:29:40 +0200 Subject: [PATCH 027/258] added new exports in eatlas get_accessions --- bin/get_eatlas_accessions.py | 139 ++++++++++++++---- .../expressionatlas/getaccessions/main.nf | 24 +-- .../getaccessions/spec-file.txt | 12 +- nextflow_schema.json | 12 +- 4 files changed, 138 insertions(+), 49 deletions(-) diff --git a/bin/get_eatlas_accessions.py b/bin/get_eatlas_accessions.py index b2ac2a95..31bce80c 100755 --- a/bin/get_eatlas_accessions.py +++ b/bin/get_eatlas_accessions.py @@ -3,7 +3,9 @@ # Written by Olivier Coen. Released under the MIT license. import argparse + import requests +import pandas as pd from tenacity import ( retry, retry_if_exception_type, @@ -23,7 +25,10 @@ ALL_EXP_URL = "https://www.ebi.ac.uk/gxa/json/experiments/" ACCESSION_OUTFILE_NAME = "accessions.txt" -FILTERED_EXPERIMENTS_OUTFILE_NAME = "filtered_experiments.yaml" +ALL_EXPERIMENTS_METADATA_OUTFILE_NAME = "all_experiments.metadata.tsv" +SPECIES_EXPERIMENTS_METADATA_OUTFILE_NAME = "species_experiments.metadata.tsv" +FILTERED_EXPERIMENTS_METADATA_OUTFILE_NAME = "filtered_experiments.metadata.tsv" +FILTERED_EXPERIMENTS_WITH_KEYWORDS_OUTFILE_NAME = "filtered_experiments.keywords.yaml" ################################################################## ################################################################## @@ -58,7 +63,9 @@ class ExpressionAtlasNothingFoundError(Exception): def parse_args(): parser = argparse.ArgumentParser("Get expression atlas accessions") - parser.add_argument("--species", type=str, help="Species to convert IDs for") + parser.add_argument( + "--species", type=str, help="Search Expression Atlas for this specific species" + ) parser.add_argument( "--keywords", type=str, @@ -163,7 +170,7 @@ def get_all_candidate_target_words(sentence: str): def word_in_sentence(word: str, sentence: str): """ - Checks if a word (or a stemmed version of it) is in a sentence, or if it is a + Check if a word (or a stemmed version of it) is in a sentence, or if it is a subword of a stemmed version of any word in the sentence. Parameters @@ -314,11 +321,25 @@ def get_properties_values(exp_dict: dict): return list(set(values)) -def get_species_experiments( - species: str, -): +def get_eatlas_experiments(): """ - Gets all experiments for a given species + Gets all experiments from Expression Atlas + + Parameters + ---------- + + Returns + ------- + experiments : list + A list of experiment dictionaries + """ + data = get_data(ALL_EXP_URL) + return data["experiments"] + + +def get_species_experiments(experiments: list[dict], species: str): + """ + Gets all experiments for a given species from Expression Atlas Parameters ---------- @@ -330,12 +351,11 @@ def get_species_experiments( experiments : list A list of experiment dictionaries """ - data = get_data(ALL_EXP_URL) - experiments = [] - for exp_dict in data["experiments"]: + species_experiments = [] + for exp_dict in experiments: if exp_dict["species"] == species: - experiments.append(exp_dict) - return experiments + species_experiments.append(exp_dict) + return species_experiments def get_experiment_data(exp_dict: dict): @@ -380,7 +400,7 @@ def keywords_in_experiment(fields: list[str], keywords: list[str]): ] -def filter_experiment(exp_dict: dict, keywords: list[str]): +def filter_experiment_with_keywords(exp_dict: dict, keywords: list[str]): all_searchable_fields = [exp_dict["description"]] + exp_dict["properties"] found_keywords = keywords_in_experiment(all_searchable_fields, keywords) # only returning experiments if found keywords @@ -391,6 +411,17 @@ def filter_experiment(exp_dict: dict, keywords: list[str]): return None +def get_metadata_for_selected_experiments( + experiments: list[dict], results: list[dict] +) -> list[dict]: + filtered_accessions = [result_dict["accession"] for result_dict in results] + return [ + exp_dict + for exp_dict in experiments + if get_experiment_accesssion(exp_dict) in filtered_accessions + ] + + def format_species_name(species: str): return species.replace("_", " ").capitalize().strip() @@ -405,39 +436,93 @@ def format_species_name(species: str): def main(): args = parse_args() + results = None + selected_accessions = [] + selected_experiments = [] + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # PARSING EXPRESSION ATLAS + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Getting arguments species_name = format_species_name(args.species) keywords = args.keywords logger.info(f"Getting experiments corresponding to species {species_name}") - experiments = get_species_experiments(species_name) - logger.info(f"Found {len(experiments)} experiments") + all_experiments = get_eatlas_experiments() + species_experiments = get_species_experiments(all_experiments, species_name) + logger.info( + f"Found {len(species_experiments)} experiments for species {species_name}" + ) logger.info("Parsing experiments") with Pool() as pool: - results = pool.map(parse_experiment, experiments) + results = pool.map(parse_experiment, species_experiments) if keywords: logger.info(f"Filtering experiments with keywords {keywords}") - func = partial(filter_experiment, keywords=keywords) + func = partial(filter_experiment_with_keywords, keywords=keywords) with Pool() as pool: results = [res for res in pool.map(func, results) if res is not None] - if results: - logger.info(f"Kept {len(results)} experiments") - else: - raise RuntimeError( - f"Could not find experiments for species {args.species} and keywords {args.keywords}" - ) + if results: + logger.info(f"Kept {len(results)} experiments") + # getting accessions of selected experiments + selected_accessions = [exp_dict["accession"] for exp_dict in results] + # keeping metadata only for selected experiments + selected_experiments = get_metadata_for_selected_experiments( + species_experiments, results + ) - selected_accessions = [exp_dict["accession"] for exp_dict in results] + else: + logger.warning( + f"Could not find experiments for species {species_name} and keywords {keywords}" + ) + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # EXPORTING DATA + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + # exporting list of accessions logger.info(f"Writing accessions to {ACCESSION_OUTFILE_NAME}") with open(ACCESSION_OUTFILE_NAME, "w") as fout: fout.writelines([f"{acc}\n" for acc in selected_accessions]) - logger.info(f"Writing filtered experiments to {FILTERED_EXPERIMENTS_OUTFILE_NAME}") - with open(FILTERED_EXPERIMENTS_OUTFILE_NAME, "w") as fout: - yaml.dump(results, fout) + # exporting metadata + logger.info( + f"Writing metadata of all experiments to {ALL_EXPERIMENTS_METADATA_OUTFILE_NAME}" + ) + df = pd.DataFrame.from_dict(all_experiments) + df.to_csv(ALL_EXPERIMENTS_METADATA_OUTFILE_NAME, sep="\t", index=False, header=True) + + # exporting metadata + logger.info( + f"Writing metadata of all experiments for species {species_name} to {SPECIES_EXPERIMENTS_METADATA_OUTFILE_NAME}" + ) + df = pd.DataFrame.from_dict(species_experiments) + df.to_csv( + SPECIES_EXPERIMENTS_METADATA_OUTFILE_NAME, sep="\t", index=False, header=True + ) + + if selected_experiments: + logger.info( + f"Writing metadata of filtered experiments to {FILTERED_EXPERIMENTS_METADATA_OUTFILE_NAME}" + ) + df = pd.DataFrame.from_dict(selected_experiments) + df.to_csv( + FILTERED_EXPERIMENTS_METADATA_OUTFILE_NAME, + sep="\t", + index=False, + header=True, + ) + + if results is not None: + # exporting list of selected experiments with their keywords + logger.info( + f"Writing filtered experiments with keywords to {FILTERED_EXPERIMENTS_WITH_KEYWORDS_OUTFILE_NAME}" + ) + with open(FILTERED_EXPERIMENTS_WITH_KEYWORDS_OUTFILE_NAME, "w") as fout: + yaml.dump(results, fout) if __name__ == "__main__": diff --git a/modules/local/expressionatlas/getaccessions/main.nf b/modules/local/expressionatlas/getaccessions/main.nf index 2083fcf9..22ca8ee5 100644 --- a/modules/local/expressionatlas/getaccessions/main.nf +++ b/modules/local/expressionatlas/getaccessions/main.nf @@ -4,19 +4,24 @@ process EXPRESSIONATLAS_GETACCESSIONS { conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/5e/5e3d9b407277b8bb8f8850eba40724b1cae9bd6e11ae0019011af82e6ac17cd4/data': - 'community.wave.seqera.io/library/nltk_pandas_python_pyyaml_pruned:2218f9c10723fbf3' }" + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/f2/f2219a174683388670dc0817da45717014aca444323027480f84aaaf12bfb460/data': + 'community.wave.seqera.io/library/nltk_data_pandas_pyyaml_requests_tenacity:5f5f82f858433879' }" input: val species val keywords output: - path "accessions.txt", emit: txt - path "filtered_experiments.yaml", emit: filtered_experiments + path "accessions.txt", emit: accessions + path "all_experiments.metadata.tsv", emit: all_eatlas_experiment_metadata + path "species_experiments.metadata.tsv", topic: species_eatlas_experiment_metadata + path "filtered_experiments.metadata.tsv", optional: true, topic: filtered_eatlas_experiment_metadata + path "filtered_experiments.keywords.yaml", optional: true, topic: filtered_eatlas_experiment_keywords tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('requests'), eval('python3 -c "import requests; print(requests.__version__)"'), topic: versions tuple val("${task.process}"), val('nltk'), eval('python3 -c "import nltk; print(nltk.__version__)"'), topic: versions + tuple val("${task.process}"), val('pyyaml'), eval('python3 -c "import yaml; print(yaml.__version__)"'), topic: versions + tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions when: task.ext.when == null || task.ext.when @@ -27,21 +32,20 @@ process EXPRESSIONATLAS_GETACCESSIONS { // the folder where nltk will download data needs to be writable (necessary for singularity) if (keywords_string == "") { """ - NLTK_DATA=$PWD get_eatlas_accessions.py \ - --species $species \ + NLTK_DATA=$PWD get_eatlas_accessions.py \\ + --species $species """ } else { """ - NLTK_DATA=$PWD get_eatlas_accessions.py \ - --species $species \ + NLTK_DATA=$PWD get_eatlas_accessions.py \\ + --species $species \\ --keywords $keywords_string """ } - stub: """ - touch accessions.txt filtered_experiments.yaml + touch accessions.txt all_experiments.metadata.tsv filtered_experiments.metadata.tsv filtered_experiments.keywords.yaml """ } diff --git a/modules/local/expressionatlas/getaccessions/spec-file.txt b/modules/local/expressionatlas/getaccessions/spec-file.txt index 5fbccb12..11788e79 100644 --- a/modules/local/expressionatlas/getaccessions/spec-file.txt +++ b/modules/local/expressionatlas/getaccessions/spec-file.txt @@ -9,7 +9,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda#9 https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda#6d11a5edae89fe413c0569f16d308f5a https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda#e66f2b8ad787e7beb0f846e4bd7e8493 https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62ee74e96c5ebb0af99386de58cf9553 -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_0.conda#e31316a586cac398b1fcdb10ace786b9 +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda#0be7c6e070c19105f966d3758448d018 https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda#db0bfbe7dd197b68ad5f30333bae6ce0 https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc @@ -19,12 +19,12 @@ https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.2-h6cd9bfd_0.cond https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.6.15-hbd8a1cb_0.conda#72525f07d72806e3b639ad4504c30ce5 +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.7.9-hbd8a1cb_0.conda#54521bf3b59c86e2f55b7294b40a04dc https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda#c87df2ab1448ba69169652ab9547082d https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.12.8-h9e4cc4f_1_cpython.conda#7fd2fd79436d9b473812f14e86746844 +https://conda.anaconda.org/conda-forge/linux-64/python-3.12.11-h9e4cc4f_0_cpython.conda#94206474a5608243a10c92cefbe0908f https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda#0dfcdc155cf23812a0c9deada86fb723 https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h2ec8cdc_3.conda#a32e0c069f6c3dcac635f7b0b0dac67e https://conda.anaconda.org/conda-forge/noarch/certifi-2025.6.15-pyhd8ed1ab_0.conda#781d068df0cc2407d4db0ecfbb29225b @@ -53,7 +53,7 @@ https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda#a451 https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 -https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.3-py312hf9745cd_3.conda#2979458c23c7755683a0598fb33e7666 +https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.0-py312hf9745cd_0.conda#ac82ac336dbe61106e21fb2e11704459 https://conda.anaconda.org/conda-forge/noarch/wheel-0.45.1-pyhd8ed1ab_1.conda#75cb7132eb58d97896e173ef12ac9986 https://conda.anaconda.org/conda-forge/noarch/pip-25.1.1-pyh8b19718_0.conda#32d0781ace05105cc99af55d36cbec7c https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda#461219d1a5bd61342293efa2c0c90eac @@ -61,5 +61,5 @@ https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2#4c https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py312h178313f_2.conda#cf2485f39740de96e2a7f2bb18ed2fee https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py312h66e93f0_2.conda#630db208bc7bbb96725ce9832c7423bb https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda#436c165519e140cb08d246a4472a9d6a -https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda#a9b9368f3701a417eac9edbcae7cb737 -https://conda.anaconda.org/conda-forge/noarch/tenacity-9.0.0-pyhd8ed1ab_1.conda#a09f66fe95a54a92172e56a4a97ba271 +https://conda.anaconda.org/conda-forge/noarch/requests-2.32.4-pyhd8ed1ab_0.conda#f6082eae112814f1447b56a5e1f6ed05 +https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.2-pyhd8ed1ab_0.conda#5d99943f2ae3cc69e1ada12ce9d4d701 diff --git a/nextflow_schema.json b/nextflow_schema.json index bbee13e4..6a42e839 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -85,12 +85,6 @@ "fa_icon": "fas fa-id-card", "help_text": "File containing Expression Atlas accession(s) that you want to download. One accession per line.Example: `--eatlas_accessions_file included_accessions.txt`. Combine with --skip_fetch_eatlas_accessions if you want only these accessions to be used." }, - "accessions_only": { - "type": "boolean", - "description": "Only get accessions from Expression Atlas and exit.", - "fa_icon": "fas fa-id-card", - "help_text": "Use this option if you want to only get Expression Atlas accessions and skip the rest of the pipeline." - }, "exclude_eatlas_accessions": { "type": "string", "pattern": "([A-Z0-9-]+,?)+", @@ -105,6 +99,12 @@ "description": "File with Expression Atlas accessions to exclude", "fa_icon": "fas fa-id-card", "help_text": "File containing Expression Atlas accession(s) that you want to exclude. One accession per line. Example: `--exclude_eatlas_accessions_file excluded_accessions.txt`." + }, + "accessions_only": { + "type": "boolean", + "description": "Only get accessions from Expression Atlas and exit.", + "fa_icon": "fas fa-id-card", + "help_text": "Use this option if you only want to get Expression Atlas accessions and skip the rest of the pipeline." } } }, From efa0d2a7f5dc64dfd42dce734f5c5a39c34e922e Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 9 Jul 2025 15:30:03 +0200 Subject: [PATCH 028/258] added eatlas tables in multiqc --- assets/multiqc_config.yml | 22 +++++++++ .../local/expressionatlas_fetchdata/main.nf | 9 +++- workflows/stableexpression.nf | 49 +++++++++++++------ 3 files changed, 62 insertions(+), 18 deletions(-) diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index 90cea002..81db9c90 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -228,6 +228,24 @@ custom_data: Pearson correlation color: "#bf812d" + eatlas_filtered_experiments_metadata: + section_name: "Expression Atlas metadata - filtered" + file_format: "tsv" + no_violin: true + sort_rows: false + description: | + Metadata of Expression Atlas accessions filtered relatively to the provided species (and optionally the provided keywords) + plot_type: "table" + + eatlas_all_experiments_metadata: + section_name: "Expression Atlas metadata - all" + file_format: "tsv" + no_violin: true + sort_rows: false + description: | + Metadata of Expression Atlas accessions filtered relatively to the provided species (and optionally the provided keywords) + plot_type: "table" + #violin_downsample_after: 10000 log_filesize_limit: 10000000000 # 10GB @@ -250,3 +268,7 @@ sp: fn: "*ks_test_statistics.csv" distribution_correlations: fn: "*distribution_correlations.csv" + eatlas_filtered_experiments_metadata: + fn: "*filtered_experiments.metadata.tsv" + eatlas_all_experiments_metadata: + fn: "*all_experiments.metadata.tsv" diff --git a/subworkflows/local/expressionatlas_fetchdata/main.nf b/subworkflows/local/expressionatlas_fetchdata/main.nf index 87396cfb..6d570b57 100644 --- a/subworkflows/local/expressionatlas_fetchdata/main.nf +++ b/subworkflows/local/expressionatlas_fetchdata/main.nf @@ -35,7 +35,12 @@ workflow EXPRESSIONATLAS_FETCHDATA { ch_species, params.eatlas_keywords ) - EXPRESSIONATLAS_GETACCESSIONS.out.txt.set { ch_fetched_accessions } + EXPRESSIONATLAS_GETACCESSIONS.out.accessions.splitText().set { ch_fetched_accessions } + + // printing message if no accession could be retrieved from Expression Atlas + ch_fetched_accessions + .ifEmpty('No Expression Atlas accession could be retrieved!') + .view() } @@ -55,7 +60,7 @@ workflow EXPRESSIONATLAS_FETCHDATA { // removing E-PROT- accessions // removing excluded accessions ch_input_accessions - .mix( ch_fetched_accessions.splitText() ) + .mix( ch_fetched_accessions ) .unique() .map { it -> it.trim() } .filter { it.startsWith('E-') && !it.startsWith('E-PROT-') } diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index f2e38251..1a12485c 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -26,7 +26,14 @@ workflow STABLEEXPRESSION { main: - multiqc_report = Channel.empty() + + ch_top_stable_genes_summary = Channel.empty() + ch_all_genes_statistics = Channel.empty() + ch_top_stable_genes_transposed_counts = Channel.empty() + ch_gene_count_statistics = Channel.empty() + ch_skewness_statistics = Channel.empty() + ch_ks_stats = Channel.empty() + ch_distribution_correlations = Channel.empty() ch_species = Channel.value( params.species.split(' ').join('_') ) @@ -73,6 +80,9 @@ workflow STABLEEXPRESSION { MERGE_DATA.out.candidate_gene_counts.set { ch_candidate_gene_counts } MERGE_DATA.out.ks_test_statistics.set { ch_ks_stats } + MERGE_DATA.out.gene_count_statistics.set { ch_gene_count_statistics } + MERGE_DATA.out.skewness_statistics.set { ch_skewness_statistics } + MERGE_DATA.out.distribution_correlations.set { ch_distribution_correlations } // ----------------------------------------------------------------- // GENE STATISTICS @@ -87,25 +97,32 @@ workflow STABLEEXPRESSION { params.ks_pvalue_threshold ) - // ----------------------------------------------------------------- - // MULTIQC - // ----------------------------------------------------------------- + GENE_STATISTICS.out.top_stable_genes_summary.set { ch_top_stable_genes_summary } + GENE_STATISTICS.out.all_statistics.set { ch_all_genes_statistics } + GENE_STATISTICS.out.top_stable_genes_transposed_counts.set { ch_top_stable_genes_transposed_counts } - Channel.empty() - .mix( GENE_STATISTICS.out.top_stable_genes_summary.collect() ) - .mix( GENE_STATISTICS.out.all_statistics.collect() ) - .mix( GENE_STATISTICS.out.top_stable_genes_transposed_counts.collect() ) - .mix( MERGE_DATA.out.gene_count_statistics.collect() ) - .mix( MERGE_DATA.out.skewness_statistics.collect() ) - .mix( ch_ks_stats.collect() ) - .mix( MERGE_DATA.out.distribution_correlations.collect() ) - .set { ch_multiqc_files } + } - MULTIQC_WORKFLOW( ch_multiqc_files ) + // ----------------------------------------------------------------- + // MULTIQC + // ----------------------------------------------------------------- - MULTIQC_WORKFLOW.out.report.toList().set { multiqc_report } + Channel.empty() + .mix( ch_top_stable_genes_summary.collect() ) + .mix( ch_all_genes_statistics.collect() ) + .mix( ch_top_stable_genes_transposed_counts.collect() ) + .mix( ch_gene_count_statistics.collect() ) + .mix( ch_skewness_statistics.collect() ) + .mix( ch_ks_stats.collect() ) + .mix( ch_distribution_correlations.collect() ) + .mix( Channel.topic('all_eatlas_experiment_metadata').collect() ) + .mix( Channel.topic('filtered_eatlas_experiment_metadata').collect() ) + .set { ch_multiqc_files } + + MULTIQC_WORKFLOW( ch_multiqc_files ) + + MULTIQC_WORKFLOW.out.report.toList().set { multiqc_report } - } emit: multiqc_report From 405b65d413da0a5f048ef67d32a3b5c8395a6ca5 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 9 Jul 2025 15:41:35 +0200 Subject: [PATCH 029/258] changed species in some tests to go faster --- conf/test.config | 3 +- .../nf_core_stableexpression.boilerplate.xml | 7 ++-- .../getaccessions/main.nf.test.snap | 8 ++--- .../local/idmapping/gprofiler/main.nf.test | 4 +-- .../expressionatlas_fetchdata/main.nf.test | 30 ++++++++++++++-- tests/workflows/stableexpression.nf.test | 35 +++---------------- 6 files changed, 44 insertions(+), 43 deletions(-) diff --git a/conf/test.config b/conf/test.config index 8fe4d0c7..bc7050cd 100644 --- a/conf/test.config +++ b/conf/test.config @@ -16,7 +16,8 @@ params { config_profile_description = 'Minimal test dataset to check pipeline function' // Input data - species = 'solanum tuberosum' + species = 'beta vulgaris' + eatlas_keywords = "leaf" datasets = "tests/test_data/input_datasets/input.csv" outdir = "results/test_dataset" } diff --git a/galaxy/build/static/nf_core_stableexpression.boilerplate.xml b/galaxy/build/static/nf_core_stableexpression.boilerplate.xml index 15fa0b6d..beec4d42 100644 --- a/galaxy/build/static/nf_core_stableexpression.boilerplate.xml +++ b/galaxy/build/static/nf_core_stableexpression.boilerplate.xml @@ -61,7 +61,7 @@ INPUTS - + @@ -80,7 +80,8 @@ INPUTS - + + @@ -94,7 +95,7 @@ INPUTS - + diff --git a/tests/modules/local/expressionatlas/getaccessions/main.nf.test.snap b/tests/modules/local/expressionatlas/getaccessions/main.nf.test.snap index 92fbf4cd..75972b72 100644 --- a/tests/modules/local/expressionatlas/getaccessions/main.nf.test.snap +++ b/tests/modules/local/expressionatlas/getaccessions/main.nf.test.snap @@ -1,5 +1,5 @@ { - "Solanum tuberosum no keyword": { + "Beta vulgaris no keyword": { "content": [ { "0": [ @@ -43,7 +43,7 @@ }, "timestamp": "2025-05-18T07:05:49.797203628" }, - "Solanum tuberosum one keyword": { + "Beta vulgaris one keyword": { "content": [ { "0": [ @@ -87,7 +87,7 @@ }, "timestamp": "2025-05-18T07:05:09.434053957" }, - "Solanum tuberosum two keywords": { + "Beta vulgaris two keywords": { "content": [ { "0": [ @@ -131,4 +131,4 @@ }, "timestamp": "2025-05-18T07:05:31.438421087" } -} \ No newline at end of file +} diff --git a/tests/modules/local/idmapping/gprofiler/main.nf.test b/tests/modules/local/idmapping/gprofiler/main.nf.test index c8dd8561..9f06851d 100644 --- a/tests/modules/local/idmapping/gprofiler/main.nf.test +++ b/tests/modules/local/idmapping/gprofiler/main.nf.test @@ -17,7 +17,7 @@ nextflow_process { file("$projectDir/tests/test_data/idmapping/base/counts.ensembl_ids.csv", checkIfExists: true) ] ) - input[1] = "Solanum tuberosum" + input[1] = "Beta vulgaris" input[2] = Channel.value([]) """ } @@ -164,7 +164,7 @@ nextflow_process { file("$projectDir/tests/test_data/idmapping/base/counts.ensembl_ids.csv", checkIfExists: true) ] ) - input[1] = "Solanum tuberosum" + input[1] = "Beta vulgaris" input[2] = Channel.fromPath( "$projectDir/tests/test_data/idmapping/custom/mapping.csv", checkIfExists: true) """ } diff --git a/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test b/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test index bcc3786f..529a339a 100644 --- a/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test +++ b/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test @@ -6,7 +6,7 @@ nextflow_workflow { tag "expressionatlas_fetchdata" tag "subworkflow" - test("Download Expression Atlas datasets") { + test("Accessions provided + keywords") { when { workflow { @@ -34,7 +34,7 @@ nextflow_workflow { } - test("Download Expression Atlas accessions only") { + test("Accessions only") { when { workflow { @@ -62,4 +62,30 @@ nextflow_workflow { } + test("No accesssion + no keywords + multiple dataset species") { + + when { + workflow { + """ + species = 'beta vulgaris' + skip_fetch_eatlas_accessions = false + accessions_only = true + + input[0] = Channel.value( species.split(' ').join('_') ) + input[1] = eatlas_accessions + input[2] = eatlas_keywords + input[3] = fetch_eatlas_accessions + """ + } + } + + then { + assertAll( + { assert workflow.success }, + { assert snapshot(workflow.out).match() } + ) + } + + } + } diff --git a/tests/workflows/stableexpression.nf.test b/tests/workflows/stableexpression.nf.test index 4298fac2..11ab6ce9 100644 --- a/tests/workflows/stableexpression.nf.test +++ b/tests/workflows/stableexpression.nf.test @@ -12,7 +12,7 @@ nextflow_workflow { when { params { - species = "solanum tuberosum" + species = "beta vulgaris" eatlas_accessions = "E-MTAB-552,E-GEOD-61690" } workflow { @@ -33,40 +33,13 @@ nextflow_workflow { } } - test("Expression Atlas accession - two output datasets") { - - tag "workflow_eatlas_accession_two_datasets" - - when { - params { - species = "homo sapiens" - eatlas_accessions = "E-GEOD-1615" - } - workflow { - """ - input[0] = Channel.empty() - """ - } - } - - then { - assert workflow.success - with(workflow.out.multiqc_report[0]) { - assertAll( - { assert path(get(0)).readLines().any { it.contains('MultiQC: A modular tool') } }, - { assert path(get(0)).readLines().any { it.contains('Data was processed using nf-core/stableexpression') } } - ) - } - } - } - test("Two Expression Atlas no keyword (whole species)") { tag "workflow_eatlas_no_kw" when { params { - species = "solanum tuberosum" + species = "beta vulgaris" fetch_eatlas_accessions = true } workflow { @@ -93,7 +66,7 @@ nextflow_workflow { when { params { - species = "solanum tuberosum" + species = "beta vulgaris" eatlas_keywords = "potato,stress" } workflow { @@ -177,7 +150,7 @@ nextflow_workflow { when { params { - species = "solanum tuberosum" + species = "beta vulgaris" accessions_only = true } workflow { From ac874583459634be667c0fb94e1ac5f5e3bb6136 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Thu, 10 Jul 2025 10:06:00 +0200 Subject: [PATCH 030/258] update --- galaxy/test/test.sh | 2 +- galaxy/tool/nf_core_stableexpression.xml | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/galaxy/test/test.sh b/galaxy/test/test.sh index 635d6946..c0fc9767 100755 --- a/galaxy/test/test.sh +++ b/galaxy/test/test.sh @@ -4,5 +4,5 @@ galaxy_dir="$(dirname $(dirname $(readlink -f "$0")))" tool_file="${galaxy_dir}/tool/nf_core_stableexpression.xml" # add --update_test_data to create output file -planemo test $tool_file +planemo test $tool_file --update_test_data diff --git a/galaxy/tool/nf_core_stableexpression.xml b/galaxy/tool/nf_core_stableexpression.xml index 1c6fff8c..dc6dd081 100644 --- a/galaxy/tool/nf_core_stableexpression.xml +++ b/galaxy/tool/nf_core_stableexpression.xml @@ -65,15 +65,15 @@ VERSION="1.0dev"; echo "$VERSION" #if $expression_atlas_options.eatlas_accessions_file --eatlas_accessions_file "$expression_atlas_options.eatlas_accessions_file" #end if - #if $expression_atlas_options.accessions_only - --accessions_only $expression_atlas_options.accessions_only - #end if #if $expression_atlas_options.exclude_eatlas_accessions --exclude_eatlas_accessions "$expression_atlas_options.exclude_eatlas_accessions" #end if #if $expression_atlas_options.exclude_eatlas_accessions_file --exclude_eatlas_accessions_file "$expression_atlas_options.exclude_eatlas_accessions_file" #end if + #if $expression_atlas_options.accessions_only + --accessions_only $expression_atlas_options.accessions_only + #end if #if $idmapping_options.skip_gprofiler --skip_gprofiler $idmapping_options.skip_gprofiler #end if @@ -108,17 +108,17 @@ VERSION="1.0dev"; echo "$VERSION"
    - ([a-zA-Z,]+) + ([a-zA-Z,]+) ([A-Z0-9-]+,?)+ - ([A-Z0-9-]+,?)+ +
    @@ -140,7 +140,7 @@ VERSION="1.0dev"; echo "$VERSION" - + @@ -159,7 +159,8 @@ VERSION="1.0dev"; echo "$VERSION" - + + @@ -173,7 +174,7 @@ VERSION="1.0dev"; echo "$VERSION" - + From daa676162dd4412fcc9a3d8898c0d4f560837e5e Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Thu, 10 Jul 2025 10:09:32 +0200 Subject: [PATCH 031/258] removed recursively unwanted files --- galaxy/test/tool_test_output.html | 18659 ---------------------------- galaxy/test/tool_test_output.json | 463 - 2 files changed, 19122 deletions(-) delete mode 100644 galaxy/test/tool_test_output.html delete mode 100644 galaxy/test/tool_test_output.json diff --git a/galaxy/test/tool_test_output.html b/galaxy/test/tool_test_output.html deleted file mode 100644 index 114f72be..00000000 --- a/galaxy/test/tool_test_output.html +++ /dev/null @@ -1,18659 +0,0 @@ - - - - - - - Test Results (powered by Planemo) - - - - - - - - -
    -
    -
    - - - - - - - diff --git a/galaxy/test/tool_test_output.json b/galaxy/test/tool_test_output.json deleted file mode 100644 index 5aec7b73..00000000 --- a/galaxy/test/tool_test_output.json +++ /dev/null @@ -1,463 +0,0 @@ -{ - "summary": { - "num_errors": 0, - "num_failures": 0, - "num_skips": 0, - "num_tests": 3 - }, - "tests": [ - { - "data": { - "inputs": { - "input_output_options|datasets|count_datasets": [ - { - "id": "d77c1c1553d8ae81", - "src": "hda" - }, - { - "id": "8f49e6aa49bd8f4e", - "src": "hda" - } - ], - "input_output_options|datasets|experimental_designs": [ - { - "id": "b93aa8e2a35b8857", - "src": "hda" - }, - { - "id": "d10e079854ce5c69", - "src": "hda" - } - ], - "input_output_options|datasets|provide_datasets": true, - "input_output_options|datasets|samplesheet": { - "id": "f14d691d132db03a", - "src": "hda" - }, - "input_output_options|skip_fetch_eatlas_accessions": true, - "input_output_options|species": "solanum tuberosum" - }, - "job": { - "command_line": "python '/home/olivier/repositories/nf-core-stableexpression/galaxy/tool/rebuild_samplesheet.py' --in /tmp/tmppbklvq_q/files/3/5/f/dataset_35f02710-5dc7-48d3-beeb-6670fe961d3c.dat --out renamed_samplesheet.csv --count-files /tmp/tmppbklvq_q/files/a/0/7/dataset_a07a6342-8e00-47d9-83d1-ebd4c14037e0.dat,/tmp/tmppbklvq_q/files/3/4/9/dataset_34937997-5658-430c-b9d7-521063b31b61.dat --count-filenames microarray.normalised.csv rnaseq.raw.csv --design-files /tmp/tmppbklvq_q/files/3/2/5/dataset_32558adb-41a2-4e78-a0fb-dd3fb60e5cfc.dat,/tmp/tmppbklvq_q/files/c/1/e/dataset_c1e00074-715d-4205-af0b-d52abd151ae5.dat --design-filenames microarray.normalised.design.csv rnaseq.raw.design.csv && nextflow run OlivierCoen/stableexpression -r dev -latest -profile apptainer -ansi-log false --outdir ./results --species \"solanum tuberosum\" --datasets renamed_samplesheet.csv --skip_fetch_eatlas_accessions --skip_fetch_eatlas_accessions --normalisation_method \"deseq2\" --nb_top_gene_candidates 1000 --ks_pvalue_threshold 0.0", - "command_version": "1.0dev", - "copied_from_job_id": null, - "create_time": "2025-07-07T13:34:47.305076", - "dependencies": [], - "exit_code": 0, - "external_id": "2189932", - "galaxy_version": "25.0", - "handler": null, - "history_id": "f14d691d132db03a", - "id": "b53dd89ac6a51dd9", - "inputs": { - "input_output_options|datasets|count_datasets": { - "id": "d77c1c1553d8ae81", - "src": "hda", - "uuid": "a07a6342-8e00-47d9-83d1-ebd4c14037e0" - }, - "input_output_options|datasets|count_datasets1": { - "id": "d77c1c1553d8ae81", - "src": "hda", - "uuid": "a07a6342-8e00-47d9-83d1-ebd4c14037e0" - }, - "input_output_options|datasets|count_datasets2": { - "id": "8f49e6aa49bd8f4e", - "src": "hda", - "uuid": "34937997-5658-430c-b9d7-521063b31b61" - }, - "input_output_options|datasets|experimental_designs": { - "id": "b93aa8e2a35b8857", - "src": "hda", - "uuid": "32558adb-41a2-4e78-a0fb-dd3fb60e5cfc" - }, - "input_output_options|datasets|experimental_designs1": { - "id": "b93aa8e2a35b8857", - "src": "hda", - "uuid": "32558adb-41a2-4e78-a0fb-dd3fb60e5cfc" - }, - "input_output_options|datasets|experimental_designs2": { - "id": "d10e079854ce5c69", - "src": "hda", - "uuid": "c1e00074-715d-4205-af0b-d52abd151ae5" - }, - "input_output_options|datasets|samplesheet": { - "id": "f14d691d132db03a", - "src": "hda", - "uuid": "35f02710-5dc7-48d3-beeb-6670fe961d3c" - } - }, - "job_messages": [], - "job_metrics": [], - "job_runner_name": null, - "job_stderr": "", - "job_stdout": "", - "model_class": "Job", - "output_collections": {}, - "outputs": { - "multiqc_report": { - "id": "b53dd89ac6a51dd9", - "src": "hda", - "uuid": "02b60d90-cbc8-4a41-8656-71abf653b621" - }, - "top_stable_genes_summary": { - "id": "4e217783f5b35092", - "src": "hda", - "uuid": "7c59b178-207b-462b-8808-ec7425201f75" - } - }, - "params": { - "__input_ext": "\"txt\"", - "chromInfo": "\"/tmp/tmppbklvq_q/galaxy-dev/tool-data/shared/ucsc/chrom/?.len\"", - "dbkey": "\"?\"", - "expression_atlas_options": "{\"accessions_only\": false, \"eatlas_accessions\": null, \"eatlas_accessions_file\": null, \"eatlas_keywords\": null, \"exclude_eatlas_accessions\": null, \"exclude_eatlas_accessions_file\": null}", - "idmapping_options": "{\"gene_id_mapping_file\": null, \"gene_metadata\": null, \"skip_gprofiler\": false}", - "input_output_options": "{\"datasets\": {\"__current_case__\": 0, \"count_datasets\": {\"values\": [{\"id\": 2, \"src\": \"hda\"}, {\"id\": 3, \"src\": \"hda\"}]}, \"experimental_designs\": {\"values\": [{\"id\": 4, \"src\": \"hda\"}, {\"id\": 5, \"src\": \"hda\"}]}, \"provide_datasets\": true, \"samplesheet\": {\"values\": [{\"id\": 1, \"src\": \"hda\"}]}}, \"skip_fetch_eatlas_accessions\": true, \"species\": \"solanum tuberosum\"}", - "statistical_options": "{\"ks_pvalue_threshold\": \"0.0\", \"nb_top_gene_candidates\": \"1000\", \"normalisation_method\": \"deseq2\"}" - }, - "state": "ok", - "stderr": "", - "stdout": "N E X T F L O W ~ version 25.04.6\nPulling OlivierCoen/stableexpression ...\n Already-up-to-date\nLaunching `https://github.com/OlivierCoen/stableexpression` [infallible_sanger] DSL2 - revision: 139de1d916 [dev]\n\n-\u001b[2m----------------------------------------------------\u001b[0m-\n \u001b[0;32m,--.\u001b[0;30m/\u001b[0;32m,-.\u001b[0m\n\u001b[0;34m ___ __ __ __ ___ \u001b[0;32m/,-._.--~'\u001b[0m\n\u001b[0;34m |\\ | |__ __ / ` / \\ |__) |__ \u001b[0;33m} {\u001b[0m\n\u001b[0;34m | \\| | \\__, \\__/ | \\ |___ \u001b[0;32m\\`-._,-`-,\u001b[0m\n \u001b[0;32m`._,._,'\u001b[0m\n\u001b[0;35m nf-core/stableexpression 1.0dev\u001b[0m\n-\u001b[2m----------------------------------------------------\u001b[0m-\n\u001b[1mInput/output options\u001b[0m\n \u001b[0;34mspecies : \u001b[0;32msolanum tuberosum\u001b[0m\n \u001b[0;34moutdir : \u001b[0;32m./results\u001b[0m\n \u001b[0;34mdatasets : \u001b[0;32mrenamed_samplesheet.csv\u001b[0m\n \u001b[0;34mskip_fetch_eatlas_accessions: \u001b[0;32mtrue\u001b[0m\n\n\u001b[1mStatistics options\u001b[0m\n \u001b[0;34mks_pvalue_threshold : \u001b[0;32m0.0\u001b[0m\n\n\u001b[1mGeneric options\u001b[0m\n \u001b[0;34mmax_multiqc_email_size : \u001b[0;32m25.MB\u001b[0m\n \u001b[0;34mtrace_report_suffix : \u001b[0;32m2025-07-07_15-34-55\u001b[0m\n\n\u001b[1mCore Nextflow options\u001b[0m\n \u001b[0;34mrevision : \u001b[0;32mdev\u001b[0m\n \u001b[0;34mrunName : \u001b[0;32minfallible_sanger\u001b[0m\n \u001b[0;34mcontainerEngine : \u001b[0;32mapptainer\u001b[0m\n \u001b[0;34mlaunchDir : \u001b[0;32m/tmp/tmppbklvq_q/job_working_directory/000/6/working\u001b[0m\n \u001b[0;34mworkDir : \u001b[0;32m/tmp/tmppbklvq_q/job_working_directory/000/6/working/work\u001b[0m\n \u001b[0;34mprojectDir : \u001b[0;32m/home/olivier/.nextflow/assets/OlivierCoen/stableexpression\u001b[0m\n \u001b[0;34muserName : \u001b[0;32molivier\u001b[0m\n \u001b[0;34mprofile : \u001b[0;32mapptainer\u001b[0m\n \u001b[0;34mconfigFiles : \u001b[0;32m\u001b[0m\n\n!! Only displaying parameters that differ from the pipeline defaults !!\n-\u001b[2m----------------------------------------------------\u001b[0m-\n* The nf-core framework\n https://doi.org/10.1038/s41587-020-0439-x\n\n* Software dependencies\n https://github.com/nf-core/stableexpression/blob/main/CITATIONS.md\n\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_python_requests:8c6da05a2935a952 [cache /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity/community.wave.seqera.io-library-pandas_python_requests-8c6da05a2935a952.img]\n[fa/056a72] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (dataset_a07a6342-8e00-47d9-83d1-ebd4c14037e0)\n[07/8e178c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (dataset_34937997-5658-430c-b9d7-521063b31b61)\nPulling Apptainer image docker://community.wave.seqera.io/library/bioconductor-deseq2_r-base_r-optparse:c84cd7ffdb298fa7 [cache /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity/community.wave.seqera.io-library-bioconductor-deseq2_r-base_r-optparse-c84cd7ffdb298fa7.img]\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_pyarrow_python_scikit-learn:6f85e3c4d1706e81 [cache /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity/community.wave.seqera.io-library-pandas_pyarrow_python_scikit-learn-6f85e3c4d1706e81.img]\n[dd/9ac587] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (dataset_a07a6342-8e00-47d9-83d1-ebd4c14037e0)\n[18/8d0d97] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (dataset_34937997-5658-430c-b9d7-521063b31b61)\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_pyarrow_python_scipy:7cad0d297a717147 [cache /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity/community.wave.seqera.io-library-pandas_pyarrow_python_scipy-7cad0d297a717147.img]\n[4e/9c4b12] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (dataset_a07a6342-8e00-47d9-83d1-ebd4c14037e0)\n[58/0c2003] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (dataset_34937997-5658-430c-b9d7-521063b31b61)\n[f0/d63cd8] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (dataset_34937997-5658-430c-b9d7-521063b31b61)\nPulling Apptainer image docker://community.wave.seqera.io/library/polars_python:cab787b788e5eba7 [cache /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity/community.wave.seqera.io-library-polars_python-cab787b788e5eba7.img]\n[19/9b4dd5] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:MERGE_DATA\n[2b/8ceec2] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:GENE_STATISTICS\nPulling Apptainer image docker://quay.io/biocontainers/multiqc:1.27--pyhdfd78af_0 [cache /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity/quay.io-biocontainers-multiqc-1.27--pyhdfd78af_0.img]\n[d3/a84151] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:MULTIQC_WORKFLOW:MULTIQC\n-\u001b[0;35m[nf-core/stableexpression]\u001b[0;32m Pipeline completed successfully\u001b[0m-\n", - "tool_id": "nf_core_stableexpression", - "tool_stderr": "", - "tool_stdout": "N E X T F L O W ~ version 25.04.6\nPulling OlivierCoen/stableexpression ...\n Already-up-to-date\nLaunching `https://github.com/OlivierCoen/stableexpression` [infallible_sanger] DSL2 - revision: 139de1d916 [dev]\n\n-\u001b[2m----------------------------------------------------\u001b[0m-\n \u001b[0;32m,--.\u001b[0;30m/\u001b[0;32m,-.\u001b[0m\n\u001b[0;34m ___ __ __ __ ___ \u001b[0;32m/,-._.--~'\u001b[0m\n\u001b[0;34m |\\ | |__ __ / ` / \\ |__) |__ \u001b[0;33m} {\u001b[0m\n\u001b[0;34m | \\| | \\__, \\__/ | \\ |___ \u001b[0;32m\\`-._,-`-,\u001b[0m\n \u001b[0;32m`._,._,'\u001b[0m\n\u001b[0;35m nf-core/stableexpression 1.0dev\u001b[0m\n-\u001b[2m----------------------------------------------------\u001b[0m-\n\u001b[1mInput/output options\u001b[0m\n \u001b[0;34mspecies : \u001b[0;32msolanum tuberosum\u001b[0m\n \u001b[0;34moutdir : \u001b[0;32m./results\u001b[0m\n \u001b[0;34mdatasets : \u001b[0;32mrenamed_samplesheet.csv\u001b[0m\n \u001b[0;34mskip_fetch_eatlas_accessions: \u001b[0;32mtrue\u001b[0m\n\n\u001b[1mStatistics options\u001b[0m\n \u001b[0;34mks_pvalue_threshold : \u001b[0;32m0.0\u001b[0m\n\n\u001b[1mGeneric options\u001b[0m\n \u001b[0;34mmax_multiqc_email_size : \u001b[0;32m25.MB\u001b[0m\n \u001b[0;34mtrace_report_suffix : \u001b[0;32m2025-07-07_15-34-55\u001b[0m\n\n\u001b[1mCore Nextflow options\u001b[0m\n \u001b[0;34mrevision : \u001b[0;32mdev\u001b[0m\n \u001b[0;34mrunName : \u001b[0;32minfallible_sanger\u001b[0m\n \u001b[0;34mcontainerEngine : \u001b[0;32mapptainer\u001b[0m\n \u001b[0;34mlaunchDir : \u001b[0;32m/tmp/tmppbklvq_q/job_working_directory/000/6/working\u001b[0m\n \u001b[0;34mworkDir : \u001b[0;32m/tmp/tmppbklvq_q/job_working_directory/000/6/working/work\u001b[0m\n \u001b[0;34mprojectDir : \u001b[0;32m/home/olivier/.nextflow/assets/OlivierCoen/stableexpression\u001b[0m\n \u001b[0;34muserName : \u001b[0;32molivier\u001b[0m\n \u001b[0;34mprofile : \u001b[0;32mapptainer\u001b[0m\n \u001b[0;34mconfigFiles : \u001b[0;32m\u001b[0m\n\n!! Only displaying parameters that differ from the pipeline defaults !!\n-\u001b[2m----------------------------------------------------\u001b[0m-\n* The nf-core framework\n https://doi.org/10.1038/s41587-020-0439-x\n\n* Software dependencies\n https://github.com/nf-core/stableexpression/blob/main/CITATIONS.md\n\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_python_requests:8c6da05a2935a952 [cache /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity/community.wave.seqera.io-library-pandas_python_requests-8c6da05a2935a952.img]\n[fa/056a72] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (dataset_a07a6342-8e00-47d9-83d1-ebd4c14037e0)\n[07/8e178c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (dataset_34937997-5658-430c-b9d7-521063b31b61)\nPulling Apptainer image docker://community.wave.seqera.io/library/bioconductor-deseq2_r-base_r-optparse:c84cd7ffdb298fa7 [cache /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity/community.wave.seqera.io-library-bioconductor-deseq2_r-base_r-optparse-c84cd7ffdb298fa7.img]\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_pyarrow_python_scikit-learn:6f85e3c4d1706e81 [cache /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity/community.wave.seqera.io-library-pandas_pyarrow_python_scikit-learn-6f85e3c4d1706e81.img]\n[dd/9ac587] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (dataset_a07a6342-8e00-47d9-83d1-ebd4c14037e0)\n[18/8d0d97] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (dataset_34937997-5658-430c-b9d7-521063b31b61)\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_pyarrow_python_scipy:7cad0d297a717147 [cache /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity/community.wave.seqera.io-library-pandas_pyarrow_python_scipy-7cad0d297a717147.img]\n[4e/9c4b12] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (dataset_a07a6342-8e00-47d9-83d1-ebd4c14037e0)\n[58/0c2003] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (dataset_34937997-5658-430c-b9d7-521063b31b61)\n[f0/d63cd8] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (dataset_34937997-5658-430c-b9d7-521063b31b61)\nPulling Apptainer image docker://community.wave.seqera.io/library/polars_python:cab787b788e5eba7 [cache /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity/community.wave.seqera.io-library-polars_python-cab787b788e5eba7.img]\n[19/9b4dd5] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:MERGE_DATA\n[2b/8ceec2] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:GENE_STATISTICS\nPulling Apptainer image docker://quay.io/biocontainers/multiqc:1.27--pyhdfd78af_0 [cache /tmp/tmppbklvq_q/job_working_directory/000/6/working/work/singularity/quay.io-biocontainers-multiqc-1.27--pyhdfd78af_0.img]\n[d3/a84151] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:MULTIQC_WORKFLOW:MULTIQC\n-\u001b[0;35m[nf-core/stableexpression]\u001b[0;32m Pipeline completed successfully\u001b[0m-\n", - "update_time": "2025-07-07T13:35:48.617986", - "user_email": "planemo@galaxyproject.org", - "user_id": "f14d691d132db03a" - }, - "status": "success", - "test_index": 0, - "time_seconds": 79.66745114326477, - "tool_id": "nf_core_stableexpression", - "tool_version": "1.0dev" - }, - "has_data": true, - "id": "nf_core_stableexpression-0" - }, - { - "data": { - "inputs": { - "input_output_options|species": "solanum tuberosum" - }, - "job": { - "command_line": "echo \"No user dataset provided\" && nextflow run OlivierCoen/stableexpression -r dev -latest -profile apptainer -ansi-log false --outdir ./results --species \"solanum tuberosum\" --normalisation_method \"deseq2\" --nb_top_gene_candidates 1000 --ks_pvalue_threshold 0.0", - "command_version": "1.0dev", - "copied_from_job_id": null, - "create_time": "2025-07-07T13:35:49.408354", - "dependencies": [ - { - "cacheable": false, - "dependency_resolver": { - "auto_init": true, - "auto_install": true, - "can_uninstall_dependencies": true, - "ensure_channels": "conda-forge,bioconda", - "model_class": "CondaDependencyResolver", - "prefix": "/home/olivier/miniconda3", - "read_only": false, - "resolver_type": "conda", - "resolves_simple_dependencies": true, - "use_local": false, - "versionless": false - }, - "dependency_type": "conda", - "environment_path": "/home/olivier/miniconda3/envs/mulled-v1-07f6cfb22b452fc0be855ecba9cd73b57bf68b9e0f328b7d8a5bb8363ec2f8bf", - "exact": true, - "model_class": "MergedCondaDependency", - "name": "nextflow", - "version": "25.04.6" - }, - { - "cacheable": false, - "dependency_resolver": { - "auto_init": true, - "auto_install": true, - "can_uninstall_dependencies": true, - "ensure_channels": "conda-forge,bioconda", - "model_class": "CondaDependencyResolver", - "prefix": "/home/olivier/miniconda3", - "read_only": false, - "resolver_type": "conda", - "resolves_simple_dependencies": true, - "use_local": false, - "versionless": false - }, - "dependency_type": "conda", - "environment_path": "/home/olivier/miniconda3/envs/mulled-v1-07f6cfb22b452fc0be855ecba9cd73b57bf68b9e0f328b7d8a5bb8363ec2f8bf", - "exact": true, - "model_class": "MergedCondaDependency", - "name": "apptainer", - "version": "1.4.1" - }, - { - "cacheable": false, - "dependency_resolver": { - "auto_init": true, - "auto_install": true, - "can_uninstall_dependencies": true, - "ensure_channels": "conda-forge,bioconda", - "model_class": "CondaDependencyResolver", - "prefix": "/home/olivier/miniconda3", - "read_only": false, - "resolver_type": "conda", - "resolves_simple_dependencies": true, - "use_local": false, - "versionless": false - }, - "dependency_type": "conda", - "environment_path": "/home/olivier/miniconda3/envs/mulled-v1-07f6cfb22b452fc0be855ecba9cd73b57bf68b9e0f328b7d8a5bb8363ec2f8bf", - "exact": true, - "model_class": "MergedCondaDependency", - "name": "openjdk", - "version": "23.0.2" - } - ], - "exit_code": 0, - "external_id": "2198635", - "galaxy_version": "25.0", - "handler": null, - "history_id": "d77c1c1553d8ae81", - "id": "4e217783f5b35092", - "inputs": {}, - "job_messages": [], - "job_metrics": [], - "job_runner_name": null, - "job_stderr": "", - "job_stdout": "", - "model_class": "Job", - "output_collections": {}, - "outputs": { - "multiqc_report": { - "id": "2f686cc81a8947ff", - "src": "hda", - "uuid": "f802e5f0-25e1-486a-b305-4426ff321221" - }, - "top_stable_genes_summary": { - "id": "74b2595b34860659", - "src": "hda", - "uuid": "c8db12f8-d089-425b-8d7f-0cebf8c352a5" - } - }, - "params": { - "__input_ext": "\"data\"", - "chromInfo": "\"/tmp/tmppbklvq_q/galaxy-dev/tool-data/shared/ucsc/chrom/?.len\"", - "dbkey": "\"?\"", - "expression_atlas_options": "{\"accessions_only\": false, \"eatlas_accessions\": null, \"eatlas_accessions_file\": null, \"eatlas_keywords\": null, \"exclude_eatlas_accessions\": null, \"exclude_eatlas_accessions_file\": null}", - "idmapping_options": "{\"gene_id_mapping_file\": null, \"gene_metadata\": null, \"skip_gprofiler\": false}", - "input_output_options": "{\"datasets\": {\"__current_case__\": 1, \"provide_datasets\": false}, \"skip_fetch_eatlas_accessions\": false, \"species\": \"solanum tuberosum\"}", - "statistical_options": "{\"ks_pvalue_threshold\": \"0.0\", \"nb_top_gene_candidates\": \"1000\", \"normalisation_method\": \"deseq2\"}" - }, - "state": "ok", - "stderr": "", - "stdout": "No user dataset provided\nN E X T F L O W ~ version 25.04.6\nPulling OlivierCoen/stableexpression ...\n Already-up-to-date\nLaunching `https://github.com/OlivierCoen/stableexpression` [intergalactic_pauling] DSL2 - revision: 139de1d916 [dev]\n\n-\u001b[2m----------------------------------------------------\u001b[0m-\n \u001b[0;32m,--.\u001b[0;30m/\u001b[0;32m,-.\u001b[0m\n\u001b[0;34m ___ __ __ __ ___ \u001b[0;32m/,-._.--~'\u001b[0m\n\u001b[0;34m |\\ | |__ __ / ` / \\ |__) |__ \u001b[0;33m} {\u001b[0m\n\u001b[0;34m | \\| | \\__, \\__/ | \\ |___ \u001b[0;32m\\`-._,-`-,\u001b[0m\n \u001b[0;32m`._,._,'\u001b[0m\n\u001b[0;35m nf-core/stableexpression 1.0dev\u001b[0m\n-\u001b[2m----------------------------------------------------\u001b[0m-\n\u001b[1mInput/output options\u001b[0m\n \u001b[0;34mspecies : \u001b[0;32msolanum tuberosum\u001b[0m\n \u001b[0;34moutdir : \u001b[0;32m./results\u001b[0m\n\n\u001b[1mStatistics options\u001b[0m\n \u001b[0;34mks_pvalue_threshold : \u001b[0;32m0.0\u001b[0m\n\n\u001b[1mGeneric options\u001b[0m\n \u001b[0;34mmax_multiqc_email_size: \u001b[0;32m25.MB\u001b[0m\n \u001b[0;34mtrace_report_suffix : \u001b[0;32m2025-07-07_15-35-57\u001b[0m\n\n\u001b[1mCore Nextflow options\u001b[0m\n \u001b[0;34mrevision : \u001b[0;32mdev\u001b[0m\n \u001b[0;34mrunName : \u001b[0;32mintergalactic_pauling\u001b[0m\n \u001b[0;34mcontainerEngine : \u001b[0;32mapptainer\u001b[0m\n \u001b[0;34mlaunchDir : \u001b[0;32m/tmp/tmppbklvq_q/job_working_directory/000/7/working\u001b[0m\n \u001b[0;34mworkDir : \u001b[0;32m/tmp/tmppbklvq_q/job_working_directory/000/7/working/work\u001b[0m\n \u001b[0;34mprojectDir : \u001b[0;32m/home/olivier/.nextflow/assets/OlivierCoen/stableexpression\u001b[0m\n \u001b[0;34muserName : \u001b[0;32molivier\u001b[0m\n \u001b[0;34mprofile : \u001b[0;32mapptainer\u001b[0m\n \u001b[0;34mconfigFiles : \u001b[0;32m\u001b[0m\n\n!! Only displaying parameters that differ from the pipeline defaults !!\n-\u001b[2m----------------------------------------------------\u001b[0m-\n* The nf-core framework\n https://doi.org/10.1038/s41587-020-0439-x\n\n* Software dependencies\n https://github.com/nf-core/stableexpression/blob/main/CITATIONS.md\n\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/nltk_pandas_python_pyyaml_pruned:2218f9c10723fbf3 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-nltk_pandas_python_pyyaml_pruned-2218f9c10723fbf3.img]\n[1f/c49a85] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETACCESSIONS\nPulling Apptainer image docker://community.wave.seqera.io/library/bioconductor-expressionatlas_r-base_r-optparse:ca0f8cd9d3f44af9 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-bioconductor-expressionatlas_r-base_r-optparse-ca0f8cd9d3f44af9.img]\n[9a/9a4cd2] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-4251)\n[f9/c5afe9] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-7711)\n[ff/ca3fdb] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-4301)\n[cf/054e1d] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-5038)\n[b9/4a7438] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-552)\n[e0/6636bb] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-GEOD-61690)\n[87/3dd640] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-GEOD-77826)\n[36/e011ea] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-5215)\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_python_requests:8c6da05a2935a952 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-pandas_python_requests-8c6da05a2935a952.img]\n[34/4b621f] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_GEOD_77826_rnaseq)\n[59/26bbb6] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_4251_rnaseq)\n[d3/ba509a] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_7711_rnaseq)\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/bioconductor-deseq2_r-base_r-optparse:c84cd7ffdb298fa7 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-bioconductor-deseq2_r-base_r-optparse-c84cd7ffdb298fa7.img]\n[b4/3cb17c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_5038_rnaseq)\n[af/419079] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_GEOD_61690_rnaseq)\n[31/91c25c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_4301_rnaseq)\n[20/eb8d95] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_5215_rnaseq)\n[9f/9131f9] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_552_rnaseq)\n[04/09f0f7] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_GEOD_77826_rnaseq)\n[22/23cbcf] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_4251_rnaseq)\n[a5/75e84d] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_7711_rnaseq)\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_pyarrow_python_scikit-learn:6f85e3c4d1706e81 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-pandas_pyarrow_python_scikit-learn-6f85e3c4d1706e81.img]\n[c8/f80f4e] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_5038_rnaseq)\n[a5/6663fb] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_GEOD_61690_rnaseq)\n[31/a97a9d] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_4301_rnaseq)\n[5e/e3acd4] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_5215_rnaseq)\n[ce/aee630] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_552_rnaseq)\n[e6/616f1a] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_4251_rnaseq)\n[63/3c20fe] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_GEOD_77826_rnaseq)\n[0b/88927c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_7711_rnaseq)\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_pyarrow_python_scipy:7cad0d297a717147 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-pandas_pyarrow_python_scipy-7cad0d297a717147.img]\n[27/d62eca] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_5038_rnaseq)\n[28/3ddfd3] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_GEOD_61690_rnaseq)\n[4c/314be3] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_4301_rnaseq)\n[aa/b72868] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_5215_rnaseq)\n[33/301a93] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_552_rnaseq)\n[48/e9e634] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_4251_rnaseq)\n[de/22b91c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_GEOD_77826_rnaseq)\n[02/28863b] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_7711_rnaseq)\n[70/48c1c4] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_5038_rnaseq)\n[d6/3320ec] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_GEOD_61690_rnaseq)\n[ac/3b0515] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_4301_rnaseq)\n[88/5c00b6] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_5215_rnaseq)\n[c7/15b347] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_552_rnaseq)\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/polars_python:cab787b788e5eba7 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-polars_python-cab787b788e5eba7.img]\n[f8/fa4aca] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:MERGE_DATA\n[2c/a6954a] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:GENE_STATISTICS\nPulling Apptainer image docker://quay.io/biocontainers/multiqc:1.27--pyhdfd78af_0 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/quay.io-biocontainers-multiqc-1.27--pyhdfd78af_0.img]\n[5c/c1a568] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:MULTIQC_WORKFLOW:MULTIQC\n-\u001b[0;35m[nf-core/stableexpression]\u001b[0;32m Pipeline completed successfully\u001b[0m-\n", - "tool_id": "nf_core_stableexpression", - "tool_stderr": "", - "tool_stdout": "No user dataset provided\nN E X T F L O W ~ version 25.04.6\nPulling OlivierCoen/stableexpression ...\n Already-up-to-date\nLaunching `https://github.com/OlivierCoen/stableexpression` [intergalactic_pauling] DSL2 - revision: 139de1d916 [dev]\n\n-\u001b[2m----------------------------------------------------\u001b[0m-\n \u001b[0;32m,--.\u001b[0;30m/\u001b[0;32m,-.\u001b[0m\n\u001b[0;34m ___ __ __ __ ___ \u001b[0;32m/,-._.--~'\u001b[0m\n\u001b[0;34m |\\ | |__ __ / ` / \\ |__) |__ \u001b[0;33m} {\u001b[0m\n\u001b[0;34m | \\| | \\__, \\__/ | \\ |___ \u001b[0;32m\\`-._,-`-,\u001b[0m\n \u001b[0;32m`._,._,'\u001b[0m\n\u001b[0;35m nf-core/stableexpression 1.0dev\u001b[0m\n-\u001b[2m----------------------------------------------------\u001b[0m-\n\u001b[1mInput/output options\u001b[0m\n \u001b[0;34mspecies : \u001b[0;32msolanum tuberosum\u001b[0m\n \u001b[0;34moutdir : \u001b[0;32m./results\u001b[0m\n\n\u001b[1mStatistics options\u001b[0m\n \u001b[0;34mks_pvalue_threshold : \u001b[0;32m0.0\u001b[0m\n\n\u001b[1mGeneric options\u001b[0m\n \u001b[0;34mmax_multiqc_email_size: \u001b[0;32m25.MB\u001b[0m\n \u001b[0;34mtrace_report_suffix : \u001b[0;32m2025-07-07_15-35-57\u001b[0m\n\n\u001b[1mCore Nextflow options\u001b[0m\n \u001b[0;34mrevision : \u001b[0;32mdev\u001b[0m\n \u001b[0;34mrunName : \u001b[0;32mintergalactic_pauling\u001b[0m\n \u001b[0;34mcontainerEngine : \u001b[0;32mapptainer\u001b[0m\n \u001b[0;34mlaunchDir : \u001b[0;32m/tmp/tmppbklvq_q/job_working_directory/000/7/working\u001b[0m\n \u001b[0;34mworkDir : \u001b[0;32m/tmp/tmppbklvq_q/job_working_directory/000/7/working/work\u001b[0m\n \u001b[0;34mprojectDir : \u001b[0;32m/home/olivier/.nextflow/assets/OlivierCoen/stableexpression\u001b[0m\n \u001b[0;34muserName : \u001b[0;32molivier\u001b[0m\n \u001b[0;34mprofile : \u001b[0;32mapptainer\u001b[0m\n \u001b[0;34mconfigFiles : \u001b[0;32m\u001b[0m\n\n!! Only displaying parameters that differ from the pipeline defaults !!\n-\u001b[2m----------------------------------------------------\u001b[0m-\n* The nf-core framework\n https://doi.org/10.1038/s41587-020-0439-x\n\n* Software dependencies\n https://github.com/nf-core/stableexpression/blob/main/CITATIONS.md\n\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/nltk_pandas_python_pyyaml_pruned:2218f9c10723fbf3 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-nltk_pandas_python_pyyaml_pruned-2218f9c10723fbf3.img]\n[1f/c49a85] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETACCESSIONS\nPulling Apptainer image docker://community.wave.seqera.io/library/bioconductor-expressionatlas_r-base_r-optparse:ca0f8cd9d3f44af9 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-bioconductor-expressionatlas_r-base_r-optparse-ca0f8cd9d3f44af9.img]\n[9a/9a4cd2] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-4251)\n[f9/c5afe9] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-7711)\n[ff/ca3fdb] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-4301)\n[cf/054e1d] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-5038)\n[b9/4a7438] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-552)\n[e0/6636bb] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-GEOD-61690)\n[87/3dd640] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-GEOD-77826)\n[36/e011ea] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-5215)\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_python_requests:8c6da05a2935a952 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-pandas_python_requests-8c6da05a2935a952.img]\n[34/4b621f] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_GEOD_77826_rnaseq)\n[59/26bbb6] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_4251_rnaseq)\n[d3/ba509a] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_7711_rnaseq)\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/bioconductor-deseq2_r-base_r-optparse:c84cd7ffdb298fa7 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-bioconductor-deseq2_r-base_r-optparse-c84cd7ffdb298fa7.img]\n[b4/3cb17c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_5038_rnaseq)\n[af/419079] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_GEOD_61690_rnaseq)\n[31/91c25c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_4301_rnaseq)\n[20/eb8d95] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_5215_rnaseq)\n[9f/9131f9] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_552_rnaseq)\n[04/09f0f7] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_GEOD_77826_rnaseq)\n[22/23cbcf] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_4251_rnaseq)\n[a5/75e84d] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_7711_rnaseq)\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_pyarrow_python_scikit-learn:6f85e3c4d1706e81 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-pandas_pyarrow_python_scikit-learn-6f85e3c4d1706e81.img]\n[c8/f80f4e] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_5038_rnaseq)\n[a5/6663fb] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_GEOD_61690_rnaseq)\n[31/a97a9d] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_4301_rnaseq)\n[5e/e3acd4] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_5215_rnaseq)\n[ce/aee630] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_552_rnaseq)\n[e6/616f1a] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_4251_rnaseq)\n[63/3c20fe] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_GEOD_77826_rnaseq)\n[0b/88927c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_7711_rnaseq)\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_pyarrow_python_scipy:7cad0d297a717147 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-pandas_pyarrow_python_scipy-7cad0d297a717147.img]\n[27/d62eca] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_5038_rnaseq)\n[28/3ddfd3] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_GEOD_61690_rnaseq)\n[4c/314be3] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_4301_rnaseq)\n[aa/b72868] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_5215_rnaseq)\n[33/301a93] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_552_rnaseq)\n[48/e9e634] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_4251_rnaseq)\n[de/22b91c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_GEOD_77826_rnaseq)\n[02/28863b] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_7711_rnaseq)\n[70/48c1c4] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_5038_rnaseq)\n[d6/3320ec] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_GEOD_61690_rnaseq)\n[ac/3b0515] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_4301_rnaseq)\n[88/5c00b6] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_5215_rnaseq)\n[c7/15b347] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_552_rnaseq)\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/polars_python:cab787b788e5eba7 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/community.wave.seqera.io-library-polars_python-cab787b788e5eba7.img]\n[f8/fa4aca] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:MERGE_DATA\n[2c/a6954a] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:GENE_STATISTICS\nPulling Apptainer image docker://quay.io/biocontainers/multiqc:1.27--pyhdfd78af_0 [cache /tmp/tmppbklvq_q/job_working_directory/000/7/working/work/singularity/quay.io-biocontainers-multiqc-1.27--pyhdfd78af_0.img]\n[5c/c1a568] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:MULTIQC_WORKFLOW:MULTIQC\n-\u001b[0;35m[nf-core/stableexpression]\u001b[0;32m Pipeline completed successfully\u001b[0m-\n", - "update_time": "2025-07-07T13:40:17.207952", - "user_email": "planemo@galaxyproject.org", - "user_id": "f14d691d132db03a" - }, - "status": "success", - "test_index": 1, - "time_seconds": 268.3443777561188, - "tool_id": "nf_core_stableexpression", - "tool_version": "1.0dev" - }, - "has_data": true, - "id": "nf_core_stableexpression-1" - }, - { - "data": { - "inputs": { - "input_output_options|datasets|count_datasets": [ - { - "id": "1749056fc9e975ac", - "src": "hda" - }, - { - "id": "ace316e9ce92c65a", - "src": "hda" - } - ], - "input_output_options|datasets|experimental_designs": [ - { - "id": "093c5f960045e43a", - "src": "hda" - }, - { - "id": "063a55387997889b", - "src": "hda" - } - ], - "input_output_options|datasets|provide_datasets": true, - "input_output_options|datasets|samplesheet": { - "id": "e7b79b392ebf8da8", - "src": "hda" - }, - "input_output_options|species": "solanum tuberosum" - }, - "job": { - "command_line": "python '/home/olivier/repositories/nf-core-stableexpression/galaxy/tool/rebuild_samplesheet.py' --in /tmp/tmppbklvq_q/files/7/6/2/dataset_762f4e43-a746-4274-9f6a-ce01101735cd.dat --out renamed_samplesheet.csv --count-files /tmp/tmppbklvq_q/files/6/9/d/dataset_69d3f92c-2be0-4deb-a415-fea0c6b418d1.dat,/tmp/tmppbklvq_q/files/3/6/7/dataset_3671e188-4fd4-41ab-ad36-517ad81c4465.dat --count-filenames microarray.normalised.csv rnaseq.raw.csv --design-files /tmp/tmppbklvq_q/files/e/f/9/dataset_ef9df148-00cb-485d-b28e-6caf887a48e6.dat,/tmp/tmppbklvq_q/files/8/6/3/dataset_863e9d57-3c97-4a70-b305-688a48c3642d.dat --design-filenames microarray.normalised.design.csv rnaseq.raw.design.csv && nextflow run OlivierCoen/stableexpression -r dev -latest -profile apptainer -ansi-log false --outdir ./results --species \"solanum tuberosum\" --datasets renamed_samplesheet.csv --normalisation_method \"deseq2\" --nb_top_gene_candidates 1000 --ks_pvalue_threshold 0.0", - "command_version": "1.0dev", - "copied_from_job_id": null, - "create_time": "2025-07-07T13:40:34.899637", - "dependencies": [ - { - "cacheable": false, - "dependency_resolver": { - "auto_init": true, - "auto_install": true, - "can_uninstall_dependencies": true, - "ensure_channels": "conda-forge,bioconda", - "model_class": "CondaDependencyResolver", - "prefix": "/home/olivier/miniconda3", - "read_only": false, - "resolver_type": "conda", - "resolves_simple_dependencies": true, - "use_local": false, - "versionless": false - }, - "dependency_type": "conda", - "environment_path": "/home/olivier/miniconda3/envs/mulled-v1-07f6cfb22b452fc0be855ecba9cd73b57bf68b9e0f328b7d8a5bb8363ec2f8bf", - "exact": true, - "model_class": "MergedCondaDependency", - "name": "nextflow", - "version": "25.04.6" - }, - { - "cacheable": false, - "dependency_resolver": { - "auto_init": true, - "auto_install": true, - "can_uninstall_dependencies": true, - "ensure_channels": "conda-forge,bioconda", - "model_class": "CondaDependencyResolver", - "prefix": "/home/olivier/miniconda3", - "read_only": false, - "resolver_type": "conda", - "resolves_simple_dependencies": true, - "use_local": false, - "versionless": false - }, - "dependency_type": "conda", - "environment_path": "/home/olivier/miniconda3/envs/mulled-v1-07f6cfb22b452fc0be855ecba9cd73b57bf68b9e0f328b7d8a5bb8363ec2f8bf", - "exact": true, - "model_class": "MergedCondaDependency", - "name": "apptainer", - "version": "1.4.1" - }, - { - "cacheable": false, - "dependency_resolver": { - "auto_init": true, - "auto_install": true, - "can_uninstall_dependencies": true, - "ensure_channels": "conda-forge,bioconda", - "model_class": "CondaDependencyResolver", - "prefix": "/home/olivier/miniconda3", - "read_only": false, - "resolver_type": "conda", - "resolves_simple_dependencies": true, - "use_local": false, - "versionless": false - }, - "dependency_type": "conda", - "environment_path": "/home/olivier/miniconda3/envs/mulled-v1-07f6cfb22b452fc0be855ecba9cd73b57bf68b9e0f328b7d8a5bb8363ec2f8bf", - "exact": true, - "model_class": "MergedCondaDependency", - "name": "openjdk", - "version": "23.0.2" - } - ], - "exit_code": 0, - "external_id": "2239136", - "galaxy_version": "25.0", - "handler": null, - "history_id": "8f49e6aa49bd8f4e", - "id": "093c5f960045e43a", - "inputs": { - "input_output_options|datasets|count_datasets": { - "id": "1749056fc9e975ac", - "src": "hda", - "uuid": "69d3f92c-2be0-4deb-a415-fea0c6b418d1" - }, - "input_output_options|datasets|count_datasets1": { - "id": "1749056fc9e975ac", - "src": "hda", - "uuid": "69d3f92c-2be0-4deb-a415-fea0c6b418d1" - }, - "input_output_options|datasets|count_datasets2": { - "id": "ace316e9ce92c65a", - "src": "hda", - "uuid": "3671e188-4fd4-41ab-ad36-517ad81c4465" - }, - "input_output_options|datasets|experimental_designs": { - "id": "093c5f960045e43a", - "src": "hda", - "uuid": "ef9df148-00cb-485d-b28e-6caf887a48e6" - }, - "input_output_options|datasets|experimental_designs1": { - "id": "093c5f960045e43a", - "src": "hda", - "uuid": "ef9df148-00cb-485d-b28e-6caf887a48e6" - }, - "input_output_options|datasets|experimental_designs2": { - "id": "063a55387997889b", - "src": "hda", - "uuid": "863e9d57-3c97-4a70-b305-688a48c3642d" - }, - "input_output_options|datasets|samplesheet": { - "id": "e7b79b392ebf8da8", - "src": "hda", - "uuid": "762f4e43-a746-4274-9f6a-ce01101735cd" - } - }, - "job_messages": [], - "job_metrics": [], - "job_runner_name": null, - "job_stderr": "", - "job_stdout": "", - "model_class": "Job", - "output_collections": {}, - "outputs": { - "multiqc_report": { - "id": "c5287a5f0f3a1faf", - "src": "hda", - "uuid": "86ef096d-8ff5-4e37-a58e-169dd3c38c49" - }, - "top_stable_genes_summary": { - "id": "60efb04c06cc3091", - "src": "hda", - "uuid": "d2ab28b3-8f62-4bba-9eb5-322e16024164" - } - }, - "params": { - "__input_ext": "\"txt\"", - "chromInfo": "\"/tmp/tmppbklvq_q/galaxy-dev/tool-data/shared/ucsc/chrom/?.len\"", - "dbkey": "\"?\"", - "expression_atlas_options": "{\"accessions_only\": false, \"eatlas_accessions\": null, \"eatlas_accessions_file\": null, \"eatlas_keywords\": null, \"exclude_eatlas_accessions\": null, \"exclude_eatlas_accessions_file\": null}", - "idmapping_options": "{\"gene_id_mapping_file\": null, \"gene_metadata\": null, \"skip_gprofiler\": false}", - "input_output_options": "{\"datasets\": {\"__current_case__\": 0, \"count_datasets\": {\"values\": [{\"id\": 11, \"src\": \"hda\"}, {\"id\": 12, \"src\": \"hda\"}]}, \"experimental_designs\": {\"values\": [{\"id\": 13, \"src\": \"hda\"}, {\"id\": 14, \"src\": \"hda\"}]}, \"provide_datasets\": true, \"samplesheet\": {\"values\": [{\"id\": 10, \"src\": \"hda\"}]}}, \"skip_fetch_eatlas_accessions\": false, \"species\": \"solanum tuberosum\"}", - "statistical_options": "{\"ks_pvalue_threshold\": \"0.0\", \"nb_top_gene_candidates\": \"1000\", \"normalisation_method\": \"deseq2\"}" - }, - "state": "ok", - "stderr": "", - "stdout": "N E X T F L O W ~ version 25.04.6\nPulling OlivierCoen/stableexpression ...\n Already-up-to-date\nLaunching `https://github.com/OlivierCoen/stableexpression` [grave_hodgkin] DSL2 - revision: 139de1d916 [dev]\n\n-\u001b[2m----------------------------------------------------\u001b[0m-\n \u001b[0;32m,--.\u001b[0;30m/\u001b[0;32m,-.\u001b[0m\n\u001b[0;34m ___ __ __ __ ___ \u001b[0;32m/,-._.--~'\u001b[0m\n\u001b[0;34m |\\ | |__ __ / ` / \\ |__) |__ \u001b[0;33m} {\u001b[0m\n\u001b[0;34m | \\| | \\__, \\__/ | \\ |___ \u001b[0;32m\\`-._,-`-,\u001b[0m\n \u001b[0;32m`._,._,'\u001b[0m\n\u001b[0;35m nf-core/stableexpression 1.0dev\u001b[0m\n-\u001b[2m----------------------------------------------------\u001b[0m-\n\u001b[1mInput/output options\u001b[0m\n \u001b[0;34mspecies : \u001b[0;32msolanum tuberosum\u001b[0m\n \u001b[0;34moutdir : \u001b[0;32m./results\u001b[0m\n \u001b[0;34mdatasets : \u001b[0;32mrenamed_samplesheet.csv\u001b[0m\n\n\u001b[1mStatistics options\u001b[0m\n \u001b[0;34mks_pvalue_threshold : \u001b[0;32m0.0\u001b[0m\n\n\u001b[1mGeneric options\u001b[0m\n \u001b[0;34mmax_multiqc_email_size: \u001b[0;32m25.MB\u001b[0m\n \u001b[0;34mtrace_report_suffix : \u001b[0;32m2025-07-07_15-40-39\u001b[0m\n\n\u001b[1mCore Nextflow options\u001b[0m\n \u001b[0;34mrevision : \u001b[0;32mdev\u001b[0m\n \u001b[0;34mrunName : \u001b[0;32mgrave_hodgkin\u001b[0m\n \u001b[0;34mcontainerEngine : \u001b[0;32mapptainer\u001b[0m\n \u001b[0;34mlaunchDir : \u001b[0;32m/tmp/tmppbklvq_q/job_working_directory/000/13/working\u001b[0m\n \u001b[0;34mworkDir : \u001b[0;32m/tmp/tmppbklvq_q/job_working_directory/000/13/working/work\u001b[0m\n \u001b[0;34mprojectDir : \u001b[0;32m/home/olivier/.nextflow/assets/OlivierCoen/stableexpression\u001b[0m\n \u001b[0;34muserName : \u001b[0;32molivier\u001b[0m\n \u001b[0;34mprofile : \u001b[0;32mapptainer\u001b[0m\n \u001b[0;34mconfigFiles : \u001b[0;32m\u001b[0m\n\n!! Only displaying parameters that differ from the pipeline defaults !!\n-\u001b[2m----------------------------------------------------\u001b[0m-\n* The nf-core framework\n https://doi.org/10.1038/s41587-020-0439-x\n\n* Software dependencies\n https://github.com/nf-core/stableexpression/blob/main/CITATIONS.md\n\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/nltk_pandas_python_pyyaml_pruned:2218f9c10723fbf3 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-nltk_pandas_python_pyyaml_pruned-2218f9c10723fbf3.img]\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_python_requests:8c6da05a2935a952 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-pandas_python_requests-8c6da05a2935a952.img]\n[86/9fbf39] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (dataset_3671e188-4fd4-41ab-ad36-517ad81c4465)\n[e5/47d907] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (dataset_69d3f92c-2be0-4deb-a415-fea0c6b418d1)\n[87/da389d] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETACCESSIONS\nPulling Apptainer image docker://community.wave.seqera.io/library/bioconductor-deseq2_r-base_r-optparse:c84cd7ffdb298fa7 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-bioconductor-deseq2_r-base_r-optparse-c84cd7ffdb298fa7.img]\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_pyarrow_python_scikit-learn:6f85e3c4d1706e81 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-pandas_pyarrow_python_scikit-learn-6f85e3c4d1706e81.img]\n[51/7cb214] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (dataset_69d3f92c-2be0-4deb-a415-fea0c6b418d1)\n[30/76ffa2] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (dataset_3671e188-4fd4-41ab-ad36-517ad81c4465)\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_pyarrow_python_scipy:7cad0d297a717147 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-pandas_pyarrow_python_scipy-7cad0d297a717147.img]\n[8b/cb4e47] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (dataset_69d3f92c-2be0-4deb-a415-fea0c6b418d1)\nPulling Apptainer image docker://community.wave.seqera.io/library/bioconductor-expressionatlas_r-base_r-optparse:ca0f8cd9d3f44af9 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-bioconductor-expressionatlas_r-base_r-optparse-ca0f8cd9d3f44af9.img]\n[50/62910a] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-552)\n[99/0032bc] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-4251)\n[b0/fea57e] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-4301)\n[5d/78028e] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-GEOD-61690)\n[b3/c3b0b4] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-GEOD-77826)\n[ef/e42508] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-5038)\n[bd/3fa292] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-7711)\n[8f/a81318] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-5215)\n[51/2fe86b] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_4301_rnaseq)\n[fb/1074e5] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_552_rnaseq)\n[43/8d985d] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_GEOD_61690_rnaseq)\n[92/138d42] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_GEOD_77826_rnaseq)\n[db/dd3fbf] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_4251_rnaseq)\n[44/dc08c9] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_5038_rnaseq)\n[0c/f4f46e] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_5215_rnaseq)\n[cd/61f87c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_7711_rnaseq)\n[65/24c205] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_552_rnaseq)\n[f6/f1cf3f] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_4301_rnaseq)\n[9b/fe7347] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_GEOD_61690_rnaseq)\n[ed/bd7ad2] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_GEOD_77826_rnaseq)\n[47/b13d0c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_4251_rnaseq)\n[c7/3de644] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_5038_rnaseq)\n[26/a49898] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_5215_rnaseq)\n[5c/fa0cee] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_7711_rnaseq)\n[2b/6ee60b] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (dataset_3671e188-4fd4-41ab-ad36-517ad81c4465)\n[b2/a196a7] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_552_rnaseq)\n[41/7dc9e1] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_4301_rnaseq)\n[01/673e26] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_GEOD_61690_rnaseq)\n[b8/8fb0eb] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_GEOD_77826_rnaseq)\n[30/ee19b6] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_4251_rnaseq)\n[f9/f2b9b6] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_5038_rnaseq)\n[ca/18206e] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_5215_rnaseq)\n[22/7e80e7] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_7711_rnaseq)\n[bc/d70f60] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (dataset_3671e188-4fd4-41ab-ad36-517ad81c4465)\n[58/2d50b7] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_552_rnaseq)\n[1e/aeb2b1] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_4301_rnaseq)\n[29/8ca339] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_GEOD_61690_rnaseq)\n[af/164cba] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_GEOD_77826_rnaseq)\n[77/7abd5c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_4251_rnaseq)\n[20/cc6ed1] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_5038_rnaseq)\n[77/61b951] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_5215_rnaseq)\n[21/f6cbd4] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_7711_rnaseq)\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/polars_python:cab787b788e5eba7 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-polars_python-cab787b788e5eba7.img]\n[f7/b8e320] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:MERGE_DATA\n[cb/7f5612] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:GENE_STATISTICS\nPulling Apptainer image docker://quay.io/biocontainers/multiqc:1.27--pyhdfd78af_0 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/quay.io-biocontainers-multiqc-1.27--pyhdfd78af_0.img]\n[61/43e626] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:MULTIQC_WORKFLOW:MULTIQC\n-\u001b[0;35m[nf-core/stableexpression]\u001b[0;32m Pipeline completed successfully\u001b[0m-\n", - "tool_id": "nf_core_stableexpression", - "tool_stderr": "", - "tool_stdout": "N E X T F L O W ~ version 25.04.6\nPulling OlivierCoen/stableexpression ...\n Already-up-to-date\nLaunching `https://github.com/OlivierCoen/stableexpression` [grave_hodgkin] DSL2 - revision: 139de1d916 [dev]\n\n-\u001b[2m----------------------------------------------------\u001b[0m-\n \u001b[0;32m,--.\u001b[0;30m/\u001b[0;32m,-.\u001b[0m\n\u001b[0;34m ___ __ __ __ ___ \u001b[0;32m/,-._.--~'\u001b[0m\n\u001b[0;34m |\\ | |__ __ / ` / \\ |__) |__ \u001b[0;33m} {\u001b[0m\n\u001b[0;34m | \\| | \\__, \\__/ | \\ |___ \u001b[0;32m\\`-._,-`-,\u001b[0m\n \u001b[0;32m`._,._,'\u001b[0m\n\u001b[0;35m nf-core/stableexpression 1.0dev\u001b[0m\n-\u001b[2m----------------------------------------------------\u001b[0m-\n\u001b[1mInput/output options\u001b[0m\n \u001b[0;34mspecies : \u001b[0;32msolanum tuberosum\u001b[0m\n \u001b[0;34moutdir : \u001b[0;32m./results\u001b[0m\n \u001b[0;34mdatasets : \u001b[0;32mrenamed_samplesheet.csv\u001b[0m\n\n\u001b[1mStatistics options\u001b[0m\n \u001b[0;34mks_pvalue_threshold : \u001b[0;32m0.0\u001b[0m\n\n\u001b[1mGeneric options\u001b[0m\n \u001b[0;34mmax_multiqc_email_size: \u001b[0;32m25.MB\u001b[0m\n \u001b[0;34mtrace_report_suffix : \u001b[0;32m2025-07-07_15-40-39\u001b[0m\n\n\u001b[1mCore Nextflow options\u001b[0m\n \u001b[0;34mrevision : \u001b[0;32mdev\u001b[0m\n \u001b[0;34mrunName : \u001b[0;32mgrave_hodgkin\u001b[0m\n \u001b[0;34mcontainerEngine : \u001b[0;32mapptainer\u001b[0m\n \u001b[0;34mlaunchDir : \u001b[0;32m/tmp/tmppbklvq_q/job_working_directory/000/13/working\u001b[0m\n \u001b[0;34mworkDir : \u001b[0;32m/tmp/tmppbklvq_q/job_working_directory/000/13/working/work\u001b[0m\n \u001b[0;34mprojectDir : \u001b[0;32m/home/olivier/.nextflow/assets/OlivierCoen/stableexpression\u001b[0m\n \u001b[0;34muserName : \u001b[0;32molivier\u001b[0m\n \u001b[0;34mprofile : \u001b[0;32mapptainer\u001b[0m\n \u001b[0;34mconfigFiles : \u001b[0;32m\u001b[0m\n\n!! Only displaying parameters that differ from the pipeline defaults !!\n-\u001b[2m----------------------------------------------------\u001b[0m-\n* The nf-core framework\n https://doi.org/10.1038/s41587-020-0439-x\n\n* Software dependencies\n https://github.com/nf-core/stableexpression/blob/main/CITATIONS.md\n\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/nltk_pandas_python_pyyaml_pruned:2218f9c10723fbf3 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-nltk_pandas_python_pyyaml_pruned-2218f9c10723fbf3.img]\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_python_requests:8c6da05a2935a952 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-pandas_python_requests-8c6da05a2935a952.img]\n[86/9fbf39] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (dataset_3671e188-4fd4-41ab-ad36-517ad81c4465)\n[e5/47d907] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (dataset_69d3f92c-2be0-4deb-a415-fea0c6b418d1)\n[87/da389d] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETACCESSIONS\nPulling Apptainer image docker://community.wave.seqera.io/library/bioconductor-deseq2_r-base_r-optparse:c84cd7ffdb298fa7 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-bioconductor-deseq2_r-base_r-optparse-c84cd7ffdb298fa7.img]\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_pyarrow_python_scikit-learn:6f85e3c4d1706e81 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-pandas_pyarrow_python_scikit-learn-6f85e3c4d1706e81.img]\n[51/7cb214] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (dataset_69d3f92c-2be0-4deb-a415-fea0c6b418d1)\n[30/76ffa2] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (dataset_3671e188-4fd4-41ab-ad36-517ad81c4465)\nPulling Apptainer image docker://community.wave.seqera.io/library/pandas_pyarrow_python_scipy:7cad0d297a717147 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-pandas_pyarrow_python_scipy-7cad0d297a717147.img]\n[8b/cb4e47] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (dataset_69d3f92c-2be0-4deb-a415-fea0c6b418d1)\nPulling Apptainer image docker://community.wave.seqera.io/library/bioconductor-expressionatlas_r-base_r-optparse:ca0f8cd9d3f44af9 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-bioconductor-expressionatlas_r-base_r-optparse-ca0f8cd9d3f44af9.img]\n[50/62910a] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-552)\n[99/0032bc] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-4251)\n[b0/fea57e] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-4301)\n[5d/78028e] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-GEOD-61690)\n[b3/c3b0b4] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-GEOD-77826)\n[ef/e42508] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-5038)\n[bd/3fa292] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-7711)\n[8f/a81318] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSIONATLAS_FETCHDATA:EXPRESSIONATLAS_GETDATA (E-MTAB-5215)\n[51/2fe86b] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_4301_rnaseq)\n[fb/1074e5] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_552_rnaseq)\n[43/8d985d] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_GEOD_61690_rnaseq)\n[92/138d42] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_GEOD_77826_rnaseq)\n[db/dd3fbf] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_4251_rnaseq)\n[44/dc08c9] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_5038_rnaseq)\n[0c/f4f46e] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_5215_rnaseq)\n[cd/61f87c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:IDMAPPING:IDMAPPING_GPROFILER (E_MTAB_7711_rnaseq)\n[65/24c205] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_552_rnaseq)\n[f6/f1cf3f] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_4301_rnaseq)\n[9b/fe7347] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_GEOD_61690_rnaseq)\n[ed/bd7ad2] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_GEOD_77826_rnaseq)\n[47/b13d0c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_4251_rnaseq)\n[c7/3de644] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_5038_rnaseq)\n[26/a49898] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_5215_rnaseq)\n[5c/fa0cee] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:NORMALISATION_DESEQ2 (E_MTAB_7711_rnaseq)\n[2b/6ee60b] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (dataset_3671e188-4fd4-41ab-ad36-517ad81c4465)\n[b2/a196a7] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_552_rnaseq)\n[41/7dc9e1] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_4301_rnaseq)\n[01/673e26] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_GEOD_61690_rnaseq)\n[b8/8fb0eb] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_GEOD_77826_rnaseq)\n[30/ee19b6] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_4251_rnaseq)\n[f9/f2b9b6] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_5038_rnaseq)\n[ca/18206e] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_5215_rnaseq)\n[22/7e80e7] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:QUANTILE_NORMALISATION (E_MTAB_7711_rnaseq)\n[bc/d70f60] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (dataset_3671e188-4fd4-41ab-ad36-517ad81c4465)\n[58/2d50b7] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_552_rnaseq)\n[1e/aeb2b1] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_4301_rnaseq)\n[29/8ca339] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_GEOD_61690_rnaseq)\n[af/164cba] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_GEOD_77826_rnaseq)\n[77/7abd5c] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_4251_rnaseq)\n[20/cc6ed1] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_5038_rnaseq)\n[77/61b951] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_5215_rnaseq)\n[21/f6cbd4] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:EXPRESSION_NORMALISATION:DATASET_STATISTICS (E_MTAB_7711_rnaseq)\nWARN: Apptainer cache directory has not been defined -- Remote image will be stored in the path: /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity -- Use the environment variable NXF_APPTAINER_CACHEDIR to specify a different location\nPulling Apptainer image docker://community.wave.seqera.io/library/polars_python:cab787b788e5eba7 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/community.wave.seqera.io-library-polars_python-cab787b788e5eba7.img]\n[f7/b8e320] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:MERGE_DATA\n[cb/7f5612] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:GENE_STATISTICS\nPulling Apptainer image docker://quay.io/biocontainers/multiqc:1.27--pyhdfd78af_0 [cache /tmp/tmppbklvq_q/job_working_directory/000/13/working/work/singularity/quay.io-biocontainers-multiqc-1.27--pyhdfd78af_0.img]\n[61/43e626] Submitted process > NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:MULTIQC_WORKFLOW:MULTIQC\n-\u001b[0;35m[nf-core/stableexpression]\u001b[0;32m Pipeline completed successfully\u001b[0m-\n", - "update_time": "2025-07-07T13:45:08.624371", - "user_email": "planemo@galaxyproject.org", - "user_id": "f14d691d132db03a" - }, - "status": "success", - "test_index": 2, - "time_seconds": 291.48228931427, - "tool_id": "nf_core_stableexpression", - "tool_version": "1.0dev" - }, - "has_data": true, - "id": "nf_core_stableexpression-2" - } - ], - "version": "0.1" -} From 7052de28a853aa61c7e31b920c64cc19af719511 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Thu, 10 Jul 2025 15:29:34 +0200 Subject: [PATCH 032/258] forcing drop of local project in Galaxy before pulling the new one --- galaxy/build/static/nf_core_stableexpression.boilerplate.xml | 1 + galaxy/tool/nf_core_stableexpression.xml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/galaxy/build/static/nf_core_stableexpression.boilerplate.xml b/galaxy/build/static/nf_core_stableexpression.boilerplate.xml index beec4d42..19bdef51 100644 --- a/galaxy/build/static/nf_core_stableexpression.boilerplate.xml +++ b/galaxy/build/static/nf_core_stableexpression.boilerplate.xml @@ -43,6 +43,7 @@ VERSION="PIPELINE_VERSION"; echo "$VERSION" ## Running pipeline ## ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + && nextflow drop -f OlivierCoen/stableexpression && nextflow run OlivierCoen/stableexpression -r dev -latest -profile apptainer diff --git a/galaxy/tool/nf_core_stableexpression.xml b/galaxy/tool/nf_core_stableexpression.xml index dc6dd081..cfb935e9 100644 --- a/galaxy/tool/nf_core_stableexpression.xml +++ b/galaxy/tool/nf_core_stableexpression.xml @@ -42,7 +42,7 @@ VERSION="1.0dev"; echo "$VERSION" ## ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## Running pipeline ## ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - + && nextflow drop -f OlivierCoen/stableexpression && nextflow run OlivierCoen/stableexpression -r dev -latest -profile apptainer From 516ce91d0ab3b3bed8b8b4de25cdd77fb3b9ce17 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Thu, 10 Jul 2025 21:35:41 +0200 Subject: [PATCH 033/258] update github link in galaxy .shed --- galaxy/tool/.shed.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/galaxy/tool/.shed.yml b/galaxy/tool/.shed.yml index f9b1598e..bb7a40cd 100644 --- a/galaxy/tool/.shed.yml +++ b/galaxy/tool/.shed.yml @@ -8,4 +8,4 @@ long_description: | A typical usage is to find the most suitable qPCR housekeeping genes for a specific species (and optionally specific conditions). name: nf_core_stableexpression owner: olivier_coen -remote_repository_url: https://github.com/OlivierCoen/stableexpression/dev/galaxy/.shed.yml +remote_repository_url: https://github.com/OlivierCoen/stableexpression/ From 89ca81e3d0ae843c843f2158fd075ca7230cfe3a Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Thu, 10 Jul 2025 23:01:54 +0200 Subject: [PATCH 034/258] update sections in galaxy .shed --- galaxy/tool/.shed.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/galaxy/tool/.shed.yml b/galaxy/tool/.shed.yml index bb7a40cd..8c97037c 100644 --- a/galaxy/tool/.shed.yml +++ b/galaxy/tool/.shed.yml @@ -1,5 +1,7 @@ categories: - Transcriptomics + - RNA + - Micro-array Analysis description: Pipeline dedicated to finding the most stable genes across count datasets homepage_url: https://nf-co.re/stableexpression/ long_description: | From d1ca74433c69efc2a39973f24ecea471c6a8ddaf Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Thu, 10 Jul 2025 23:02:12 +0200 Subject: [PATCH 035/258] fix galaxy tool --- galaxy/build/formatters/schema/parameter/__init__.py | 4 ++-- .../build/static/nf_core_stableexpression.boilerplate.xml | 5 ++++- galaxy/test/lint.sh | 6 ++++++ galaxy/test/serve.sh | 2 +- galaxy/tool/nf_core_stableexpression.xml | 8 ++++++-- galaxy/tool/rebuild_samplesheet.py | 3 ++- 6 files changed, 21 insertions(+), 7 deletions(-) create mode 100755 galaxy/test/lint.sh diff --git a/galaxy/build/formatters/schema/parameter/__init__.py b/galaxy/build/formatters/schema/parameter/__init__.py index b75b8c65..74e9e956 100644 --- a/galaxy/build/formatters/schema/parameter/__init__.py +++ b/galaxy/build/formatters/schema/parameter/__init__.py @@ -1,14 +1,14 @@ from .base import BaseParameterFormatter from .datasets import DatasetsParameterFormatter from .required import RequiredParameterFormatter -from .default_value import DefaultValueParameterFormatter +# from .default_value import DefaultValueParameterFormatter PARAMETER_TO_CUSTOM_CLASS = { "datasets": DatasetsParameterFormatter, "normalisation_method": RequiredParameterFormatter, "nb_top_gene_candidates": RequiredParameterFormatter, "ks_pvalue_threshold": RequiredParameterFormatter, - "species": DefaultValueParameterFormatter, + # "species": DefaultValueParameterFormatter, } __all__ = ["BaseParameterFormatter"] diff --git a/galaxy/build/static/nf_core_stableexpression.boilerplate.xml b/galaxy/build/static/nf_core_stableexpression.boilerplate.xml index 19bdef51..74e239fc 100644 --- a/galaxy/build/static/nf_core_stableexpression.boilerplate.xml +++ b/galaxy/build/static/nf_core_stableexpression.boilerplate.xml @@ -51,6 +51,8 @@ VERSION="PIPELINE_VERSION"; echo "$VERSION" --outdir ./results PARAMETERS + && zip -r results.zip results + ]]> @@ -58,7 +60,8 @@ INPUTS - + + diff --git a/galaxy/test/lint.sh b/galaxy/test/lint.sh new file mode 100755 index 00000000..99fbbea7 --- /dev/null +++ b/galaxy/test/lint.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +galaxy_dir="$(dirname $(dirname $(readlink -f "$0")))" +tool_file="${galaxy_dir}/tool/nf_core_stableexpression.xml" + +planemo lint $tool_file --fail_level error diff --git a/galaxy/test/serve.sh b/galaxy/test/serve.sh index a98fa4bb..dc421b15 100755 --- a/galaxy/test/serve.sh +++ b/galaxy/test/serve.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash galaxy_dir="$(dirname $(dirname $(readlink -f "$0")))" -tool_dir="${galaxy_dir}/tools" +tool_dir="${galaxy_dir}/tool" planemo serve $tool_dir diff --git a/galaxy/tool/nf_core_stableexpression.xml b/galaxy/tool/nf_core_stableexpression.xml index cfb935e9..177fe8bf 100644 --- a/galaxy/tool/nf_core_stableexpression.xml +++ b/galaxy/tool/nf_core_stableexpression.xml @@ -42,6 +42,7 @@ VERSION="1.0dev"; echo "$VERSION" ## ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## Running pipeline ## ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + && nextflow drop -f OlivierCoen/stableexpression && nextflow run OlivierCoen/stableexpression -r dev -latest @@ -87,11 +88,13 @@ VERSION="1.0dev"; echo "$VERSION" --nb_top_gene_candidates $statistical_options.nb_top_gene_candidates --ks_pvalue_threshold $statistical_options.ks_pvalue_threshold + && zip -r results.zip results + ]]>
    - + ([a-zA-Z]+)[_ ]([a-zA-Z]+) @@ -136,7 +139,8 @@ VERSION="1.0dev"; echo "$VERSION" - + + diff --git a/galaxy/tool/rebuild_samplesheet.py b/galaxy/tool/rebuild_samplesheet.py index 82ea47f5..77165b5a 100644 --- a/galaxy/tool/rebuild_samplesheet.py +++ b/galaxy/tool/rebuild_samplesheet.py @@ -58,7 +58,8 @@ def parse_args(): row["counts"] = count_names_to_files[original_count_filename] if "design" in row: original_design_filename = Path(row["design"]).name - row["design"] = design_names_to_files[original_design_filename] + # the design is optional + row["design"] = design_names_to_files.get(original_design_filename, "") renamed_rows.append(row) with open(args.outfile, "w", newline="") as fout: From a29ef2f4ef34b8bba843800bf1ab036241540c41 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Fri, 18 Jul 2025 15:24:29 +0200 Subject: [PATCH 036/258] fix example command in doc --- nextflow.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nextflow.config b/nextflow.config index 115dbb18..287f6f8e 100644 --- a/nextflow.config +++ b/nextflow.config @@ -282,7 +282,7 @@ validation { monochromeLogs = params.monochrome_logs help { enabled = true - command = "nextflow run nf-core/stableexpression -profile --input samplesheet.csv --outdir " + command = "nextflow run nf-core/stableexpression -profile --species genus_species --datasets samplesheet.csv --outdir " fullParameter = "help_full" showHiddenParameter = "show_hidden" beforeText = """ From eb159c4bfa7928dfe522174a42b1c0c087e481c9 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Fri, 18 Jul 2025 15:25:00 +0200 Subject: [PATCH 037/258] fix issue in eatlas get data when data type was not recognised --- bin/get_eatlas_data.R | 14 ++- modules/local/expressionatlas/getdata/main.nf | 4 +- .../expressionatlas/getdata/main.nf.test.snap | 113 ++++++++++++++++++ 3 files changed, 124 insertions(+), 7 deletions(-) create mode 100644 tests/modules/local/expressionatlas/getdata/main.nf.test.snap diff --git a/bin/get_eatlas_data.R b/bin/get_eatlas_data.R index b2a46c35..db4f78b7 100755 --- a/bin/get_eatlas_data.R +++ b/bin/get_eatlas_data.R @@ -2,9 +2,11 @@ # Written by Olivier Coen. Released under the MIT license. +suppressPackageStartupMessages(library("ExpressionAtlas")) library(ExpressionAtlas) library(optparse) + ##################################################### ##################################################### # FUNCTIONS @@ -127,7 +129,7 @@ export_count_data <- function(result, batch_id) { # exporting to CSV file # index represents gene names - print(paste('Exporting count data to file', outfilename)) + cat(paste('Exporting count data to file', outfilename)) write.table(result$count_data, outfilename, sep = ',', row.names = TRUE, col.names = TRUE, quote = FALSE) } @@ -143,7 +145,7 @@ export_metadata <- function(result, batch_id) { ) outfilename <- paste0(batch_id, '.design.csv') - print(paste('Exporting design data to file', outfilename)) + cat(paste('Exporting design data to file', outfilename)) write.table(df, outfilename, sep = ',', row.names = FALSE, col.names = TRUE, quote = FALSE) } @@ -161,9 +163,9 @@ process_data <- function(atlas_data, accession) { # getting count dataframe tryCatch({ - if (data_type == 'rnaseq') { + if ( data_type == 'rnaseq' ) { result <- get_rnaseq_data(data) - } else if (startsWith(data_type, 'A-AFFY-')) { + } else if ( startsWith(data_type, 'A-') ) { # typically: A-AFFY- or A-GEOD- result <- get_one_colour_microarray_data(data) } else { stop(paste('ERROR: Unknown data type:', data_type)) @@ -172,7 +174,7 @@ process_data <- function(atlas_data, accession) { }, error = function(e) { print(paste("Caught an error: ", e$message)) print(paste('ERROR: Could not get assay data for experiment ID', accession, 'and data type', data_type)) - skip_iteration <- TRUE + skip_iteration <<- TRUE }) # If an error occurred, skip to the next iteration @@ -199,6 +201,8 @@ process_data <- function(atlas_data, accession) { args <- get_args() +cat(paste("Getting data for accession", args$accession, "\n")) + accession <- trimws(args$accession) if (startsWith(accession, "E-PROT")) { warning("Ignoring the ", accession, " experiment.") diff --git a/modules/local/expressionatlas/getdata/main.nf b/modules/local/expressionatlas/getdata/main.nf index 85b8a8f3..494a279f 100644 --- a/modules/local/expressionatlas/getdata/main.nf +++ b/modules/local/expressionatlas/getdata/main.nf @@ -44,8 +44,8 @@ process EXPRESSIONATLAS_GETDATA { val(accession) output: - path "*.design.csv", emit: design - path "*.counts.csv", emit: counts + path "*.design.csv", optional: true, emit: design + path "*.counts.csv", optional: true, emit: counts tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //"'), topic: versions tuple val("${task.process}"), val('ExpressionAtlas'), eval('Rscript -e "cat(as.character(packageVersion(\'ExpressionAtlas\')))"'), topic: versions diff --git a/tests/modules/local/expressionatlas/getdata/main.nf.test.snap b/tests/modules/local/expressionatlas/getdata/main.nf.test.snap new file mode 100644 index 00000000..15ebc35b --- /dev/null +++ b/tests/modules/local/expressionatlas/getdata/main.nf.test.snap @@ -0,0 +1,113 @@ +{ + "Transcriptome Analysis of the potato (rnaseq)": { + "content": [ + { + "0": [ + "E_MTAB_552_rnaseq.design.csv:md5,b81490696f638e90c1cf14236bb0c08c" + ], + "1": [ + "E_MTAB_552_rnaseq.rnaseq.raw.counts.csv:md5,830f50b60b17b62f9ca2f6a163a2879f" + ], + "2": [ + [ + "EXPRESSIONATLAS_GETDATA", + "R", + "4.3.3 (2024-02-29)" + ] + ], + "3": [ + [ + "EXPRESSIONATLAS_GETDATA", + "ExpressionAtlas", + "1.30.0" + ] + ], + "counts": [ + "E_MTAB_552_rnaseq.rnaseq.raw.counts.csv:md5,830f50b60b17b62f9ca2f6a163a2879f" + ], + "design": [ + "E_MTAB_552_rnaseq.design.csv:md5,b81490696f638e90c1cf14236bb0c08c" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.4" + }, + "timestamp": "2025-07-18T12:57:46.833178737" + }, + "Arabidopsis Geo dataset": { + "content": [ + { + "0": [ + "E_GEOD_62537_A_AFFY_2.design.csv:md5,7d7dd72be4f5b326dd25a36db01ebf88" + ], + "1": [ + "E_GEOD_62537_A_AFFY_2.microarray.normalised.counts.csv:md5,673c55171d0ccfc1d036bf43c49ae320" + ], + "2": [ + [ + "EXPRESSIONATLAS_GETDATA", + "R", + "4.3.3 (2024-02-29)" + ] + ], + "3": [ + [ + "EXPRESSIONATLAS_GETDATA", + "ExpressionAtlas", + "1.30.0" + ] + ], + "counts": [ + "E_GEOD_62537_A_AFFY_2.microarray.normalised.counts.csv:md5,673c55171d0ccfc1d036bf43c49ae320" + ], + "design": [ + "E_GEOD_62537_A_AFFY_2.design.csv:md5,7d7dd72be4f5b326dd25a36db01ebf88" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.4" + }, + "timestamp": "2025-07-18T12:58:41.56141538" + }, + "Transcription profiling by array of Arabidopsis mutant for fis2 (microarray)": { + "content": [ + { + "0": [ + "E_TABM_1007_A_AFFY_2.design.csv:md5,120f63cae2193b97d7451483bdbbaab1" + ], + "1": [ + "E_TABM_1007_A_AFFY_2.microarray.normalised.counts.csv:md5,a3afe33d7eaed3339da9109bf25bb3ed" + ], + "2": [ + [ + "EXPRESSIONATLAS_GETDATA", + "R", + "4.3.3 (2024-02-29)" + ] + ], + "3": [ + [ + "EXPRESSIONATLAS_GETDATA", + "ExpressionAtlas", + "1.30.0" + ] + ], + "counts": [ + "E_TABM_1007_A_AFFY_2.microarray.normalised.counts.csv:md5,a3afe33d7eaed3339da9109bf25bb3ed" + ], + "design": [ + "E_TABM_1007_A_AFFY_2.design.csv:md5,120f63cae2193b97d7451483bdbbaab1" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.4" + }, + "timestamp": "2025-07-18T12:58:01.254049012" + } +} \ No newline at end of file From 7305f9a15063090e1f146902d935cf439359d2ef Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Fri, 15 Aug 2025 09:07:02 +0200 Subject: [PATCH 038/258] better handle download issues with eatlas get data --- bin/get_eatlas_data.R | 2 +- modules/local/expressionatlas/getdata/main.nf | 12 ++++++---- .../local/expressionatlas_fetchdata/main.nf | 8 +++---- .../expressionatlas/getdata/main.nf.test | 23 +++++++++++++++++++ 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/bin/get_eatlas_data.R b/bin/get_eatlas_data.R index db4f78b7..d3d56de7 100755 --- a/bin/get_eatlas_data.R +++ b/bin/get_eatlas_data.R @@ -55,7 +55,7 @@ download_expression_atlas_data_with_retries <- function(accession, max_retries = if (grepl("550 Requested action not taken; file unavailable", w$message)) { warning(w$message) - quit(save = "no", status = 100) # quit & ignore process + quit(save = "no", status = 101) # quit & ignore process } else if (grepl("Failure when receiving data from the peer", w$message)) { warning(w$message) quit(save = "no", status = 100) # quit & ignore process diff --git a/modules/local/expressionatlas/getdata/main.nf b/modules/local/expressionatlas/getdata/main.nf index 494a279f..582ec1ef 100644 --- a/modules/local/expressionatlas/getdata/main.nf +++ b/modules/local/expressionatlas/getdata/main.nf @@ -26,14 +26,18 @@ process EXPRESSIONATLAS_GETDATA { log.warn("Unhandled error occurred with accession: ${accession}") return 'ignore' } else if (task.exitStatus == 137) { // override default behaviour to sleep some time before retry - // in case of OOM errors, we wait a bit and try again - sleep(Math.pow(2, task.attempt) * 2000 as long) - return 'retry' + // in case of OOM errors, we wait a bit and try again (2 retries) + if ( task.attempt in <= 2) { + sleep(Math.pow(2, task.attempt) * 2000 as long) + return 'retry' + } else { + log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") + return 'ignore' + } } else { return 'terminate' } } - maxRetries = 5 conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? diff --git a/subworkflows/local/expressionatlas_fetchdata/main.nf b/subworkflows/local/expressionatlas_fetchdata/main.nf index 6d570b57..b94925e6 100644 --- a/subworkflows/local/expressionatlas_fetchdata/main.nf +++ b/subworkflows/local/expressionatlas_fetchdata/main.nf @@ -35,12 +35,10 @@ workflow EXPRESSIONATLAS_FETCHDATA { ch_species, params.eatlas_keywords ) - EXPRESSIONATLAS_GETACCESSIONS.out.accessions.splitText().set { ch_fetched_accessions } - // printing message if no accession could be retrieved from Expression Atlas - ch_fetched_accessions - .ifEmpty('No Expression Atlas accession could be retrieved!') - .view() + EXPRESSIONATLAS_GETACCESSIONS.out.accessions + .splitText() + .set { ch_fetched_accessions } } diff --git a/tests/modules/local/expressionatlas/getdata/main.nf.test b/tests/modules/local/expressionatlas/getdata/main.nf.test index c55adf41..f3156920 100644 --- a/tests/modules/local/expressionatlas/getdata/main.nf.test +++ b/tests/modules/local/expressionatlas/getdata/main.nf.test @@ -158,4 +158,27 @@ nextflow_process { } + test("E-MTAB-3578 :: serverside error 550") { + + tag "getdata_error_550" + + when { + + process { + """ + input[0] = "E-MTAB-3578" + """ + } + } + + // check for the absence of expected output (the error is ignored but no output is produced) + then { + assert process.success + assert process.trace.succeeded().size() == 0 + assert process.trace.failed().size() == 1 + assert process.out.design.size() == 0 + } + + } + } From b57276e380e6934cacff5114732f55ad235ebf86 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Fri, 15 Aug 2025 09:09:53 +0200 Subject: [PATCH 039/258] ignore nf tests for editorconfig --- .editorconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.editorconfig b/.editorconfig index 283ba7c2..9fb0f28d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -28,8 +28,8 @@ indent_style = unset [/assets/email*] indent_size = unset -# ignore python and markdown -[*.{py,md}] +# ignore python, nf tests and markdown +[*.{py,md,test}] indent_style = unset # ignore ro-crate metadata files From 4f9756c1b8ed2900fe3f095ce2dc54af58687539 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Fri, 15 Aug 2025 09:10:00 +0200 Subject: [PATCH 040/258] fix issue with deseq2 normalise --- bin/normalise_with_deseq2.R | 14 +++++------ .../local/normalisation/deseq2/main.nf.test | 25 +++++++++++++++++++ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/bin/normalise_with_deseq2.R b/bin/normalise_with_deseq2.R index 168fb5d6..20a7db2c 100755 --- a/bin/normalise_with_deseq2.R +++ b/bin/normalise_with_deseq2.R @@ -2,6 +2,7 @@ # Written by Olivier Coen. Released under the MIT license. +suppressPackageStartupMessages(library("DESeq2")) library(DESeq2) library(optparse) @@ -50,7 +51,7 @@ prefilter_counts <- function(count_matrix, design_data) { # keep genes with at least 10 counts over a certain number of samples keep <- rowSums(count_matrix >= 10) >= smallest_group_size } - filtered_count_matrix <- count_matrix[keep,] + filtered_count_matrix <- count_matrix[keep, , drop = FALSE] # drop = FALSE: keep dataframe structure even if only one column remains return(filtered_count_matrix) } @@ -87,8 +88,6 @@ get_cpm_counts <- function(normalised_counts, filtered_count_matrix) { get_normalised_cpm_counts <- function(count_file, design_file) { - print(paste('Normalizing counts in:', count_file)) - count_data <- read.csv(count_file, row.names = 1) # data should all be integers but sometimes they are integers converted to floats (1234 -> 1234.0) @@ -101,15 +100,12 @@ get_normalised_cpm_counts <- function(count_file, design_file) { count_matrix <- remove_all_zero_columns(count_matrix) if ( is.null(design_file) ) { - # faking a design table design_data <- data.frame( sample = colnames(count_matrix), condition = rep("A", ncol(count_matrix)) ) - } else { - # getting design data design_data <- read.csv(design_file) # removing extra samples in design table @@ -135,7 +131,8 @@ get_normalised_cpm_counts <- function(count_file, design_file) { # if the dataframe is now empty, stop the process if (nrow(filtered_count_matrix) == 0) { message("No genes left after pre-filtering.") - quit(save = "no", status = 100) + #quit(save = "no", status = 100) + quit(save = "no", status = 0) } # add a small pseudocount to avoid zero counts @@ -158,7 +155,7 @@ get_normalised_cpm_counts <- function(count_file, design_file) { export_data <- function(cpm_counts, filename) { filename <- sub("\\.csv$", ".cpm.csv", filename) - print(paste('Exporting normalised counts per million to:', filename)) + cat(paste('Exporting normalised counts per million to:', filename, "\n")) write.table(cpm_counts, filename, sep = ',', row.names = TRUE, col.names = NA, quote = FALSE) } @@ -170,6 +167,7 @@ export_data <- function(cpm_counts, filename) { args <- get_args() +cat(paste("Normalising counts in", args$count_file, "\n")) cpm_counts <- get_normalised_cpm_counts(args$count_file, args$design_file) export_data(cpm_counts, basename(args$count_file)) diff --git a/tests/modules/local/normalisation/deseq2/main.nf.test b/tests/modules/local/normalisation/deseq2/main.nf.test index fa0fbc36..8bd25db7 100644 --- a/tests/modules/local/normalisation/deseq2/main.nf.test +++ b/tests/modules/local/normalisation/deseq2/main.nf.test @@ -103,4 +103,29 @@ nextflow_process { } + test("No gene left") { + + tag "deseq2_no_gene_left" + + when { + + process { + """ + input[0] = [ + [ accession: "accession" ], + file('$projectDir/tests/test_data/normalisation/no_gene_left/E_GEOD_17367_A_AFFY_47.microarray.normalised.counts.csv') + ] + """ + } + } + + then { + assert process.success + assert process.trace.succeeded().size() == 0 + assert process.trace.failed().size() == 1 + assert process.out.cpm.size() == 0 + } + + } + } From 8939a31bd578d418e717ad2de55412687b2b5079 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Fri, 15 Aug 2025 09:10:42 +0200 Subject: [PATCH 041/258] add parameter to change target distrib for quantile normalisation; set normal as default instead of uniform --- assets/multiqc_config.yml | 14 +++---- bin/get_dataset_statistics.py | 41 +++++++++++++++---- bin/get_gene_statistics.py | 6 +++ bin/merge_data.py | 2 +- bin/quantile_normalise.py | 17 ++++++-- ...{test.config => test_ignore_errors.config} | 1 + modules/local/dataset_statistics/main.nf | 6 ++- modules/local/gene_statistics/main.nf | 7 ++++ modules/local/quantile_normalisation/main.nf | 5 ++- nextflow.config | 3 +- nextflow_schema.json | 11 ++++- .../local/expression_normalisation/main.nf | 16 ++++++-- workflows/stableexpression.nf | 3 +- 13 files changed, 104 insertions(+), 28 deletions(-) rename conf/{test.config => test_ignore_errors.config} (95%) diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index 81db9c90..5538d79c 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -192,12 +192,12 @@ custom_data: color: "#bf812d" uniform_distribution_probabilities: - section_name: "Probabilities of uniform count distribution" + section_name: "Probabilities of normal / uniform count distribution" file_format: "csv" description: | - Pvalue of Kolmogorov-Smirnov test to uniform distribution. - The higher the pvalue, the more likely the distribution is uniform. - If the pvalue < 0.05, the null hypothesis is rejected and the distribution is not uniform. + Pvalue of Kolmogorov-Smirnov test to normal / uniform distribution. + The higher the pvalue, the more likely the distribution is normal / uniform. + If the pvalue < 0.05, the null hypothesis is rejected and the distribution is not normal / uniform. Samples showing a pvalue lower than the threshold (set in parameters) when not considered for stability scoring. plot_type: "linegraph" pconfig: @@ -206,10 +206,10 @@ custom_data: logswitch_active: true logswitch_label: "Log10" headers: - kolmogorov_smirnov_to_uniform_dist_pvalue: - title: "KS test to uniform distribution - pvalue" + kolmogorov_smirnov_pvalue: + title: "KS test to normal / uniform distribution - pvalue" description: | - Pvalue of Kolmogorov-Smirnov test to uniform distribution. + Pvalue of Kolmogorov-Smirnov test to normal / uniform distribution. color: "#bf812d" distribution_correlations: diff --git a/bin/get_dataset_statistics.py b/bin/get_dataset_statistics.py index 907b0c26..55f2d054 100755 --- a/bin/get_dataset_statistics.py +++ b/bin/get_dataset_statistics.py @@ -16,7 +16,9 @@ ENSEMBL_GENE_ID_COLNAME = "ensembl_gene_id" SAMPLE_COLNAME = "sample" -KS_TEST_COLNAME = "kolmogorov_smirnov_to_uniform_dist_pvalue" +KS_TEST_COLNAME = "kolmogorov_smirnov_pvalue" + +ALLOWED_TARGET_DISTRIBUTIONS = ["normal", "uniform"] ##################################################### @@ -36,23 +38,46 @@ def parse_args(): parser.add_argument( "--output", type=str, dest="outfile_name", required=True, help="Output file" ) + parser.add_argument( + "--target-distrib", + type=str, + dest="target_distribution", + required=True, + choices=ALLOWED_TARGET_DISTRIBUTIONS, + help="Target distribution to map counts to", + ) return parser.parse_args() -def compute_kolmogorov_smirnov_test_to_uniform_distribution(count_df: pd.DataFrame): - """Compute Kolmogorov-Smirnov test to uniform distribution.""" +def compute_kolmogorov_smirnov_test_to_target_distribution( + count_df: pd.DataFrame, target_distribution: str +) -> pd.Series: + """Compute Kolmogorov-Smirnov test to target distribution.""" + + if target_distribution == "normal": + cum_distrib_function = stats.norm.cdf + elif target_distribution == "uniform": + cum_distrib_function = stats.uniform.cdf + else: + raise ValueError(f"Unknown target distribution: {target_distribution}") + ks_tests = pd.Series(index=count_df.columns) for col in count_df.columns: - ks = stats.ks_1samp(count_df[col], stats.uniform.cdf, nan_policy="omit") + ks = stats.ks_1samp(count_df[col], cum_distrib_function, nan_policy="omit") ks_tests[col] = ks.pvalue + return ks_tests -def compute_dataset_statistics(count_df: pd.DataFrame): +def compute_dataset_statistics( + count_df: pd.DataFrame, target_distribution: str +) -> pd.DataFrame: dataset_stats_df = count_df.describe() dataset_stats_df.loc["skewness"] = count_df.skew() - # for each sample, test distance to uniform distribution - ks_tests = compute_kolmogorov_smirnov_test_to_uniform_distribution(count_df) + # for each sample, test distance to target distribution + ks_tests = compute_kolmogorov_smirnov_test_to_target_distribution( + count_df, target_distribution + ) dataset_stats_df.loc[KS_TEST_COLNAME] = ks_tests return dataset_stats_df.T @@ -79,7 +104,7 @@ def main(): count_df = pd.read_parquet(count_file) count_df.set_index(ENSEMBL_GENE_ID_COLNAME, inplace=True) - dataset_stats_df = compute_dataset_statistics(count_df) + dataset_stats_df = compute_dataset_statistics(count_df, args.target_distribution) export_count_data(dataset_stats_df, args.outfile_name) diff --git a/bin/get_gene_statistics.py b/bin/get_gene_statistics.py index eb24cf20..f307b0ad 100755 --- a/bin/get_gene_statistics.py +++ b/bin/get_gene_statistics.py @@ -3,6 +3,7 @@ # Written by Olivier Coen. Released under the MIT license. import argparse +import sys import polars as pl from pathlib import Path from dataclasses import dataclass, field @@ -211,6 +212,11 @@ def get_counts( valid_samples = ks_stats_df.filter( ks_stats_df[KS_TEST_COLNAME] > ks_pvalue_threshold )[SAMPLE_COLNAME].to_list() + + if not valid_samples: + logger.error("No more valid sample to process...") + sys.exit(101) + # filtering the count dataframe to keep only the valid samples return count_lf.select([ENSEMBL_GENE_ID_COLNAME] + valid_samples) diff --git a/bin/merge_data.py b/bin/merge_data.py index 55757f41..ae269924 100755 --- a/bin/merge_data.py +++ b/bin/merge_data.py @@ -22,7 +22,7 @@ STATISTIC_TYPE_COLNAME = "stat_type" GENE_COUNT_COLNAME = "count" SKEWNESS_COLNAME = "skewness" -KS_TEST_COLNAME = "kolmogorov_smirnov_to_uniform_dist_pvalue" +KS_TEST_COLNAME = "kolmogorov_smirnov_pvalue" SAMPLE_COLNAME = "sample" STAT_COLNAME_TO_PARAMS = { diff --git a/bin/quantile_normalise.py b/bin/quantile_normalise.py index f4d903b8..c42b11f3 100755 --- a/bin/quantile_normalise.py +++ b/bin/quantile_normalise.py @@ -15,7 +15,8 @@ ENSEMBL_GENE_ID_COLNAME = "ensembl_gene_id" N_QUANTILES = 1000 -OUTPUT_DISTRIBUTION = "uniform" + +ALLOWED_TARGET_DISTRIBUTIONS = ["normal", "uniform"] ##################################################### @@ -32,15 +33,23 @@ def parse_args(): parser.add_argument( "--counts", type=Path, dest="count_file", required=True, help="Count file" ) + parser.add_argument( + "--target-distrib", + type=str, + dest="target_distribution", + required=True, + choices=ALLOWED_TARGET_DISTRIBUTIONS, + help="Target distribution to map counts to", + ) return parser.parse_args() -def quantile_normalize(data: pd.DataFrame): +def quantile_normalize(data: pd.DataFrame, target_distribution: str): """ Quantile normalize a data matrix based on a target distribution. """ transformer = QuantileTransformer( - n_quantiles=N_QUANTILES, output_distribution=OUTPUT_DISTRIBUTION + n_quantiles=N_QUANTILES, output_distribution=target_distribution ) normalised_data = pd.DataFrame(index=data.index, columns=data.columns) @@ -72,7 +81,7 @@ def main(): count_df = pd.read_csv(count_file, index_col=0) count_df.index.name = ENSEMBL_GENE_ID_COLNAME - quantile_normalized_counts = quantile_normalize(count_df) + quantile_normalized_counts = quantile_normalize(count_df, args.target_distribution) export_count_data(quantile_normalized_counts, count_file) diff --git a/conf/test.config b/conf/test_ignore_errors.config similarity index 95% rename from conf/test.config rename to conf/test_ignore_errors.config index bc7050cd..81f3d196 100644 --- a/conf/test.config +++ b/conf/test_ignore_errors.config @@ -20,4 +20,5 @@ params { eatlas_keywords = "leaf" datasets = "tests/test_data/input_datasets/input.csv" outdir = "results/test_dataset" + // quant_norm_target_distrib = "uniform" } diff --git a/modules/local/dataset_statistics/main.nf b/modules/local/dataset_statistics/main.nf index 27bcef4a..43b9cec2 100644 --- a/modules/local/dataset_statistics/main.nf +++ b/modules/local/dataset_statistics/main.nf @@ -11,6 +11,7 @@ process DATASET_STATISTICS { input: tuple val(meta), path(count_file) + val target_distribution output: tuple val(meta), path('*.dataset_stats.csv'), emit: stats @@ -25,7 +26,10 @@ process DATASET_STATISTICS { script: def prefix = task.ext.prefix ?: "${meta.dataset}" """ - get_dataset_statistics.py --counts $count_file --output ${prefix}.dataset_stats.csv + get_dataset_statistics.py \ + --counts $count_file \ + --target-distrib $target_distribution \ + --output ${prefix}.dataset_stats.csv """ diff --git a/modules/local/gene_statistics/main.nf b/modules/local/gene_statistics/main.nf index 34257607..145b3aeb 100644 --- a/modules/local/gene_statistics/main.nf +++ b/modules/local/gene_statistics/main.nf @@ -9,6 +9,13 @@ process GENE_STATISTICS { + "Please check the provided accessions and datasets and run again" ) return 'terminate' + } else if (task.exitStatus == 101) { + log.error( + "No more valid sample after checking p-value of Kolmogorow-Smirnoff test against target distribution! " + + "You can try a more flexible approach by setting again the value of the ks_pvalue_threshold parameter. " + + "Provide a negative value to disable this filter." + ) + return 'terminate' } } diff --git a/modules/local/quantile_normalisation/main.nf b/modules/local/quantile_normalisation/main.nf index 80609383..d659ec05 100644 --- a/modules/local/quantile_normalisation/main.nf +++ b/modules/local/quantile_normalisation/main.nf @@ -11,6 +11,7 @@ process QUANTILE_NORMALISATION { input: tuple val(meta), path(count_file) + val target_distribution output: tuple val(meta), path('*.quant_norm.parquet'), emit: counts @@ -24,7 +25,9 @@ process QUANTILE_NORMALISATION { script: """ - quantile_normalise.py --counts $count_file + quantile_normalise.py \ + --counts $count_file \ + --target-distrib $target_distribution """ stub: diff --git a/nextflow.config b/nextflow.config index 287f6f8e..15ef937e 100644 --- a/nextflow.config +++ b/nextflow.config @@ -17,6 +17,7 @@ params { // statistics normalisation_method = 'deseq2' + quant_norm_target_distrib = 'normal' nb_top_gene_candidates = 1000 ks_pvalue_threshold = 0 @@ -183,7 +184,7 @@ profiles { } } - test { includeConfig 'conf/test.config' } + test_ignore_errors { includeConfig 'conf/test_ignore_errors.config' } test_eatlas_only { includeConfig 'conf/test_eatlas_only.config' } test_full { includeConfig 'conf/test_full.config' } test_dataset_custom_mapping { includeConfig 'conf/test_dataset_custom_mapping.config' } diff --git a/nextflow_schema.json b/nextflow_schema.json index 6a42e839..0f0c46a3 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -155,7 +155,16 @@ "description": "Tool to use for normalisation", "fa_icon": "fas fa-chart-simple", "enum": ["deseq2", "edger"], - "default": "deseq2" + "default": "deseq2", + "help_text": "Raw RNAseq data must be normalised before further processing. You can select the package used for normalisation." + }, + "quant_norm_target_distrib": { + "type": "string", + "description": "Target distribution to map to during quantile normalisation", + "fa_icon": "fas fa-chart-simple", + "enum": ["normal", "uniform"], + "default": "normal", + "help_text": "All sample counts get quantile normalised and mapped to a specific distribution so that subsequent can compare them. The pipeline uses scikit-learn's quantile_transform function. You can select the target distribution to map counts to." }, "nb_top_gene_candidates": { "type": "integer", diff --git a/subworkflows/local/expression_normalisation/main.nf b/subworkflows/local/expression_normalisation/main.nf index 8960bd94..ec3de5fb 100644 --- a/subworkflows/local/expression_normalisation/main.nf +++ b/subworkflows/local/expression_normalisation/main.nf @@ -14,7 +14,7 @@ workflow EXPRESSION_NORMALISATION { take: ch_datasets normalisation_method - + quant_norm_target_distrib main: @@ -45,14 +45,24 @@ workflow EXPRESSION_NORMALISATION { // // putting all normalised count datasets together and performing quantile normalisation - ch_datasets.normalised.concat( ch_raw_rnaseq_datasets_normalised ) | QUANTILE_NORMALISATION + ch_datasets.normalised + .mix( ch_raw_rnaseq_datasets_normalised ) + .set { quant_norm_input } + + QUANTILE_NORMALISATION ( + quant_norm_input, + quant_norm_target_distrib + ) ch_quantile_normalised_datasets = QUANTILE_NORMALISATION.out.counts // // MODULE: Dataset statistics // - DATASET_STATISTICS( ch_quantile_normalised_datasets ) + DATASET_STATISTICS( + ch_quantile_normalised_datasets, + quant_norm_target_distrib + ) emit: normalised_counts = ch_quantile_normalised_datasets diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 1a12485c..3a05fc29 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -62,7 +62,8 @@ workflow STABLEEXPRESSION { EXPRESSION_NORMALISATION( IDMAPPING.out.datasets, - params.normalisation_method + params.normalisation_method, + params.quant_norm_target_distrib ) EXPRESSION_NORMALISATION.out.normalised_counts.set { ch_normalised_counts } From 21f740ab2b3a19ff1a051e238b5a1ee730a51dc0 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sun, 31 Aug 2025 23:17:58 +0200 Subject: [PATCH 042/258] fix http issue in id mapping --- bin/map_ids_to_ensembl.py | 15 +++++++++++++++ modules/local/idmapping/gprofiler/main.nf | 10 +++++----- modules/local/idmapping/gprofiler/spec-file.txt | 1 + 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/bin/map_ids_to_ensembl.py b/bin/map_ids_to_ensembl.py index 0b1d6b15..8c0e6fe7 100755 --- a/bin/map_ids_to_ensembl.py +++ b/bin/map_ids_to_ensembl.py @@ -7,8 +7,17 @@ from pathlib import Path import argparse import logging +import urllib3 import sys +from tenacity import ( + retry, + retry_if_exception_type, + stop_after_delay, + wait_exponential, + before_sleep_log, +) + logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -86,6 +95,12 @@ def chunk_list(lst: list, chunksize: int): return [lst[i : i + chunksize] for i in range(0, len(lst), chunksize)] +@retry( + retry=retry_if_exception_type(urllib3.exceptions.ProtocolError), + stop=stop_after_delay(600), + wait=wait_exponential(multiplier=1, min=1, max=30), + before_sleep=before_sleep_log(logger, logging.WARNING), +) def request_conversion( gene_ids: list, species: str, diff --git a/modules/local/idmapping/gprofiler/main.nf b/modules/local/idmapping/gprofiler/main.nf index bb5c8bfa..d837189e 100644 --- a/modules/local/idmapping/gprofiler/main.nf +++ b/modules/local/idmapping/gprofiler/main.nf @@ -27,8 +27,8 @@ process IDMAPPING_GPROFILER { conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/aa/aad4e61f15d97b7c0a24a4e3ee87a11552464fb7110f530e43bdc9acc374cf13/data': - 'community.wave.seqera.io/library/pandas_python_requests:8c6da05a2935a952' }" + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/5c/5c28c8e613c062828aaee4b950029bc90a1a1aa94d5f61016a588c8ec7be8b65/data': + 'community.wave.seqera.io/library/pandas_requests_tenacity:5ba56df089a9d718' }" input: tuple val(meta), path(count_file) @@ -49,9 +49,9 @@ process IDMAPPING_GPROFILER { script: def custom_mapping_arg = gene_id_mapping_file ? "--custom-mappings $gene_id_mapping_file" : "" """ - map_ids_to_ensembl.py \ - --count-file "$count_file" \ - --species "$species" \ + map_ids_to_ensembl.py \\ + --count-file "$count_file" \\ + --species "$species" \\ $custom_mapping_arg """ diff --git a/modules/local/idmapping/gprofiler/spec-file.txt b/modules/local/idmapping/gprofiler/spec-file.txt index 1eda65c4..3233c10b 100644 --- a/modules/local/idmapping/gprofiler/spec-file.txt +++ b/modules/local/idmapping/gprofiler/spec-file.txt @@ -54,3 +54,4 @@ https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda#4 https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py312h66e93f0_2.conda#630db208bc7bbb96725ce9832c7423bb https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda#436c165519e140cb08d246a4472a9d6a https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda#a9b9368f3701a417eac9edbcae7cb737 +https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.2-pyhd8ed1ab_0.conda#5d99943f2ae3cc69e1ada12ce9d4d701 From 4bd7fe748426166eea9f594f9db5e26f1f7483ed Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sun, 31 Aug 2025 23:18:35 +0200 Subject: [PATCH 043/258] add micromamba profile --- conf/test_full.config | 1 - nextflow.config | 11 +++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/conf/test_full.config b/conf/test_full.config index 647d081b..43f1a9ef 100644 --- a/conf/test_full.config +++ b/conf/test_full.config @@ -17,6 +17,5 @@ params { // Input data species = 'arabidopsis thaliana' - fetch_eatlas_accessions = true outdir = "results/test_full" } diff --git a/nextflow.config b/nextflow.config index 15ef937e..46c9d0e4 100644 --- a/nextflow.config +++ b/nextflow.config @@ -104,6 +104,16 @@ profiles { charliecloud.enabled = false apptainer.enabled = false } + micromamba { + onda.enabled = true + conda.useMicromamba = true + docker.enabled = false + singularity.enabled = false + podman.enabled = false + shifter.enabled = false + charliecloud.enabled = false + apptainer.enabled = false + } docker { docker.enabled = true conda.enabled = false @@ -184,6 +194,7 @@ profiles { } } + test { includeConfig 'conf/test.config' } test_ignore_errors { includeConfig 'conf/test_ignore_errors.config' } test_eatlas_only { includeConfig 'conf/test_eatlas_only.config' } test_full { includeConfig 'conf/test_full.config' } From a25ba660e8ab904bfebcde686737b52c7b41f379 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sun, 31 Aug 2025 23:18:55 +0200 Subject: [PATCH 044/258] add micromamba profile --- conf/test.config | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 conf/test.config diff --git a/conf/test.config b/conf/test.config new file mode 100644 index 00000000..a81201c3 --- /dev/null +++ b/conf/test.config @@ -0,0 +1,22 @@ +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Nextflow config file for running minimal tests +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Defines input files and everything required to run a fast and simple pipeline test. + It tests the different ways to use the pipeline, with small data + + Use as follows: + nextflow run nf-core/stableexpression -profile test_dataset, --outdir + +---------------------------------------------------------------------------------------- +*/ + +params { + config_profile_name = 'Test dataset profile' + config_profile_description = 'Minimal test dataset to check pipeline function' + + // Input data + species = 'beta vulgaris' + eatlas_keywords = "leaf" + outdir = "results/test" +} From 4b448df39910e3b543e655fca8daa22cd109faf6 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Tue, 2 Sep 2025 12:26:54 +0200 Subject: [PATCH 045/258] add parameter to select only one platform from expression atlas --- bin/get_eatlas_accessions.py | 37 +++++++++++++++++++ conf/test_one_rnaseq_one_microarray.config | 24 ++++++++++++ .../expressionatlas/getaccessions/main.nf | 29 ++++++++------- nextflow.config | 2 + nextflow_schema.json | 9 ++++- .../local/expressionatlas_fetchdata/main.nf | 4 +- 6 files changed, 89 insertions(+), 16 deletions(-) create mode 100644 conf/test_one_rnaseq_one_microarray.config diff --git a/bin/get_eatlas_accessions.py b/bin/get_eatlas_accessions.py index 31bce80c..b36abeca 100755 --- a/bin/get_eatlas_accessions.py +++ b/bin/get_eatlas_accessions.py @@ -72,6 +72,11 @@ def parse_args(): nargs="*", help="Keywords to search for in experiment description", ) + parser.add_argument( + "--platform", + type=str, + help="Platform type" + ) return parser.parse_args() @@ -337,12 +342,39 @@ def get_eatlas_experiments(): return data["experiments"] +def get_platform_specific_experiments(experiments: list[dict], platform: str): + """ + Gets all experiments for a given platform from Expression Atlas + Possible platforms in Expression Atlas are 'rnaseq', 'microarray', 'proteomics' + + Parameters + ---------- + experiments: list[str] + platform : str + Name of platform. Example: "rnaseq" + + Returns + ------- + experiments : list + A list of experiment dictionaries + """ + platform_experiments = [] + for exp_dict in experiments: + if technology_type := exp_dict.get("technologyType"): + parsed_technology_type = technology_type[0] if isinstance(technology_type, list) else technology_type + parsed_platform = parsed_technology_type.lower().split(" ")[0].replace("-", "") + if platform == parsed_platform: + platform_experiments.append(exp_dict) + return platform_experiments + + def get_species_experiments(experiments: list[dict], species: str): """ Gets all experiments for a given species from Expression Atlas Parameters ---------- + experiments: list[str] species : str Name of species. Example: "Arabidopsis thaliana" @@ -450,6 +482,11 @@ def main(): logger.info(f"Getting experiments corresponding to species {species_name}") all_experiments = get_eatlas_experiments() + + if args.platform: + logger.info(f"Getting experiments corresponding to platform {args.platform}") + all_experiments = get_platform_specific_experiments(all_experiments, args.platform) + species_experiments = get_species_experiments(all_experiments, species_name) logger.info( f"Found {len(species_experiments)} experiments for species {species_name}" diff --git a/conf/test_one_rnaseq_one_microarray.config b/conf/test_one_rnaseq_one_microarray.config new file mode 100644 index 00000000..a86eb1ec --- /dev/null +++ b/conf/test_one_rnaseq_one_microarray.config @@ -0,0 +1,24 @@ +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Nextflow config file for running minimal tests +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Defines input files and everything required to run a fast and simple pipeline test. + It tests the different ways to use the pipeline, with small data + + Use as follows: + nextflow run nf-core/stableexpression -profile test_one_rnaseq_one_microarray, --outdir + +---------------------------------------------------------------------------------------- +*/ + +params { + config_profile_name = 'Test dataset custom gene data profile' + config_profile_description = 'Minimal test dataset with custom gene metadata to check pipeline function' + + // Input data + species = 'arabidopsis thaliana' + eatlas_accessions = 'E-GEOD-52806,E-GEOD-21945' + skip_fetch_eatlas_accessions = true + + outdir = "results/test_one_rnaseq_one_microarray" +} diff --git a/modules/local/expressionatlas/getaccessions/main.nf b/modules/local/expressionatlas/getaccessions/main.nf index 22ca8ee5..e71656f3 100644 --- a/modules/local/expressionatlas/getaccessions/main.nf +++ b/modules/local/expressionatlas/getaccessions/main.nf @@ -10,6 +10,7 @@ process EXPRESSIONATLAS_GETACCESSIONS { input: val species val keywords + val platform output: path "accessions.txt", emit: accessions @@ -28,24 +29,24 @@ process EXPRESSIONATLAS_GETACCESSIONS { script: def keywords_string = keywords.split(',').collect { it.trim() }.join(' ') - - // the folder where nltk will download data needs to be writable (necessary for singularity) - if (keywords_string == "") { - """ - NLTK_DATA=$PWD get_eatlas_accessions.py \\ - --species $species - """ - } else { - """ - NLTK_DATA=$PWD get_eatlas_accessions.py \\ - --species $species \\ - --keywords $keywords_string - """ + def args = " --species $species" + if ( keywords_string != "" ) { + args += " --keywords $keywords_string" + } + if ( platform != 'none' ) { + args += " --platform $platform" } + // the folder where nltk will download data needs to be writable (necessary for singularity) + """ + NLTK_DATA=$PWD get_eatlas_accessions.py $args + """ stub: """ - touch accessions.txt all_experiments.metadata.tsv filtered_experiments.metadata.tsv filtered_experiments.keywords.yaml + touch accessions.txt \\ + all_experiments.metadata.tsv \\ + filtered_experiments.metadata.tsv \\ + filtered_experiments.keywords.yaml """ } diff --git a/nextflow.config b/nextflow.config index 46c9d0e4..8c7f7153 100644 --- a/nextflow.config +++ b/nextflow.config @@ -30,6 +30,7 @@ params { skip_fetch_eatlas_accessions = false eatlas_keywords = "" eatlas_accessions = "" + eatlas_platform = null accessions_only = false exclude_eatlas_accessions = "" eatlas_accessions_file = null @@ -202,6 +203,7 @@ profiles { test_one_accession { includeConfig 'conf/test_one_accession.config' } test_one_accession_low_gene_count { includeConfig 'conf/test_one_accession_low_gene_count.config' } test_local_and_downloaded { includeConfig 'conf/test_local_and_downloaded.config' } + test_one_rnaseq_one_microarray { includeConfig 'conf/test_one_rnaseq_one_microarray.config' } } // Load nf-core custom profiles from different Institutions diff --git a/nextflow_schema.json b/nextflow_schema.json index 0f0c46a3..25e8ad94 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -63,7 +63,7 @@ "fa_icon": "fas fa-book-atlas", "description": "Options for fetching datasets from Expression Atlas.", "properties": { - "eatlas_keywords": { + "eatlas_keywords": { "type": "string", "description": "Expression Atlas keywords", "fa_icon": "fas fa-highlighter", @@ -77,6 +77,13 @@ "fa_icon": "fas fa-id-card", "help_text": "Provide directly in command line Expression Atlas accession(s) (separated by commas) that you want to download. Example: `--eatlas_accessions E-MTAB-552,E-GEOD-61690`. Combine with --skip_fetch_eatlas_accessions if you want only these accessions to be used." }, + "eatlas_platform": { + "type": "string", + "enum": ["rnaseq", "microarray"], + "description": "Only download Expression Atlas experiments from this platform", + "fa_icon": "fas fa-id-card", + "help_text": "By default, data from all platform are downloaded. If this parameter is specified, a filter is applied to get data from only one specific type of platform." + }, "eatlas_accessions_file": { "type": "string", "format": "file-path", diff --git a/subworkflows/local/expressionatlas_fetchdata/main.nf b/subworkflows/local/expressionatlas_fetchdata/main.nf index b94925e6..9c1ef0eb 100644 --- a/subworkflows/local/expressionatlas_fetchdata/main.nf +++ b/subworkflows/local/expressionatlas_fetchdata/main.nf @@ -31,9 +31,11 @@ workflow EXPRESSIONATLAS_FETCHDATA { // getting Expression Atlas accessions given a species name and keywords // keywords can be an empty string + def eatlas_platform = params.eatlas_platform?: 'none' EXPRESSIONATLAS_GETACCESSIONS( ch_species, - params.eatlas_keywords + params.eatlas_keywords, + eatlas_platform ) EXPRESSIONATLAS_GETACCESSIONS.out.accessions From 1f8bebd34ad72961983b6d18eb6d499a35ddc1db Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 3 Sep 2025 10:15:14 +0200 Subject: [PATCH 046/258] partial refactor of the pipeline; add steps to compute statistics for both rnaseq and microarray platform --- assets/multiqc_config.yml | 118 +++++-- bin/clean_count_data.py | 182 ++++++++++ ...cs.py => compute_final_gene_statistics.py} | 158 +++++---- bin/compute_gene_statistics_per_platform.py | 331 ++++++++++++++++++ bin/{merge_data.py => merge_counts.py} | 130 +------ modules/local/clean_count_data/main.nf | 38 ++ .../spec-file.txt | 0 .../global}/main.nf | 26 +- .../global}/spec-file.txt | 0 .../per_platform/main.nf | 36 ++ .../per_platform/spec-file.txt | 40 +++ modules/local/merge_counts/main.nf | 27 ++ modules/local/merge_counts/spec-file.txt | 40 +++ modules/local/merge_data/main.nf | 36 -- nextflow.config | 2 +- nextflow_schema.json | 6 +- subworkflows/local/data_cleansing/main.nf | 45 +++ .../local/expression_normalisation/main.nf | 17 +- .../local/merge_compute_stats/main.nf | 64 ++++ .../merge_compute_stats_per_platform/main.nf | 40 +++ workflows/stableexpression.nf | 54 +-- 21 files changed, 1054 insertions(+), 336 deletions(-) create mode 100755 bin/clean_count_data.py rename bin/{get_gene_statistics.py => compute_final_gene_statistics.py} (83%) create mode 100755 bin/compute_gene_statistics_per_platform.py rename bin/{merge_data.py => merge_counts.py} (51%) create mode 100644 modules/local/clean_count_data/main.nf rename modules/local/{gene_statistics => clean_count_data}/spec-file.txt (100%) rename modules/local/{gene_statistics => compute_gene_statistics/global}/main.nf (64%) rename modules/local/{merge_data => compute_gene_statistics/global}/spec-file.txt (100%) create mode 100644 modules/local/compute_gene_statistics/per_platform/main.nf create mode 100644 modules/local/compute_gene_statistics/per_platform/spec-file.txt create mode 100644 modules/local/merge_counts/main.nf create mode 100644 modules/local/merge_counts/spec-file.txt delete mode 100644 modules/local/merge_data/main.nf create mode 100644 subworkflows/local/data_cleansing/main.nf create mode 100644 subworkflows/local/merge_compute_stats/main.nf create mode 100644 subworkflows/local/merge_compute_stats_per_platform/main.nf diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index 5538d79c..6d87c57d 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -48,14 +48,6 @@ custom_data: title: "Ensembl Gene ID" description: | Gene IDs as shown in Ensembl - m_measure: - title: "Stability" - description: | - Gene stability measure M as defined in Vandesompele et al., Genome Biology (2002). - Lower values indicate higher stability. - M-measures were calculated directly from cpm (normalised count per million). - format: "{:,.4f}" - #minrange: 0 standard_deviation: title: "Std" description: | @@ -106,6 +98,82 @@ custom_data: title: "Original gene IDs" description: | Original gene IDs as stated in the input (provided or downloaded) datasets + rnaseq_standard_deviation: + title: "Std [RNA-seq only]" + description: | + Standard deviation of the expression across samples. + For each sample, expression was first computed as log2(cpm + 1), then fitted to [0, 1] using scikit-learn's QuantileTransformer. + format: "{:,.4f}" + rnaseq_variation_coefficient: + title: "Var coeff [RNA-seq only]" + description: | + Variation coefficient: std(expression) / mean(expression). + For each sample, expression was first computed as log2(cpm + 1), then fitted to [0, 1] using scikit-learn's QuantileTransformer. + format: "{:,.4f}" + rnaseq_mean: + title: "Average [RNA-seq only]" + description: | + Average expression across samples. + For each sample, expression was first computed as log2(cpm + 1), then fitted to [0, 1] using scikit-learn's QuantileTransformer. + format: "{:,.4f}" + rnaseq_expression_level_status: + title: "Expression level [RNA-seq only]" + description: | + Indication about the average gene expression level compared to the whole pool of genes. + Expression in [0, 0.05]: Very low expression. + Expression in [0.05, 0.1]: Low expression. + Expression in [0.1, 0.9]: Medium range. + Expression in [0.9, 0.95]: High expression. + Expression in [0.95, 1]: Very high expression. + cond_formatting_rules: + very_high: + - s_eq: "Very high expression" + high: + - s_eq: "High expression" + medium: + - s_eq: "Medium range" + low: + - s_eq: "Low expression" + very_low: + - s_eq: "Very low expression" + microarray_standard_deviation: + title: "Std [Microarray only]" + description: | + Standard deviation of the expression across samples. + For each sample, expression was first computed as log2(cpm + 1), then fitted to [0, 1] using scikit-learn's QuantileTransformer. + format: "{:,.4f}" + microarray_variation_coefficient: + title: "Var coeff [Microarray only]" + description: | + Variation coefficient: std(expression) / mean(expression). + For each sample, expression was first computed as log2(cpm + 1), then fitted to [0, 1] using scikit-learn's QuantileTransformer. + format: "{:,.4f}" + microarray_mean: + title: "Average [Microarray only]" + description: | + Average expression across samples. + For each sample, expression was first computed as log2(cpm + 1), then fitted to [0, 1] using scikit-learn's QuantileTransformer. + format: "{:,.4f}" + microarray_expression_level_status: + title: "Expression level [Microarray only]" + description: | + Indication about the average gene expression level compared to the whole pool of genes. + Expression in [0, 0.05]: Very low expression. + Expression in [0.05, 0.1]: Low expression. + Expression in [0.1, 0.9]: Medium range. + Expression in [0.9, 0.95]: High expression. + Expression in [0.95, 1]: Very high expression. + cond_formatting_rules: + very_high: + - s_eq: "Very high expression" + high: + - s_eq: "High expression" + medium: + - s_eq: "Medium range" + low: + - s_eq: "Low expression" + very_low: + - s_eq: "Very low expression" total_nb_nulls: title: "Nb nulls" description: | @@ -114,6 +182,22 @@ custom_data: title: "Nb nulls (valid samples)" description: | Number of samples in which the gene is not represented, excluding samples with particularly low overall gene count. + rnaseq_total_nb_nulls: + title: "Nb nulls [RNA-seq only]" + description: | + Number of samples in which the gene is not represented. + rnaseq_nb_nulls_valid_samples: + title: "Nb nulls (valid samples) [RNA-seq only]" + description: | + Number of samples in which the gene is not represented, excluding samples with particularly low overall gene count. + microarray_total_nb_nulls: + title: "Nb nulls [Microarray only]" + description: | + Number of samples in which the gene is not represented. + microarray_nb_nulls_valid_samples: + title: "Nb nulls (valid samples) [Microarray only]" + description: | + Number of samples in which the gene is not represented, excluding samples with particularly low overall gene count. expression_distributions_top_stable_genes: section_name: "Expression distribution of the top stable genes (ranked by stability)" @@ -212,22 +296,6 @@ custom_data: Pvalue of Kolmogorov-Smirnov test to normal / uniform distribution. color: "#bf812d" - distribution_correlations: - section_name: "Correlation to mean count distribution" - file_format: "csv" - description: | - For each sample, the correlation between the count distribution and the overall mean count distribution (including all samples) was computed. - This graph can help identify samples that are deviating from the mean count distribution. - plot_type: "linegraph" - pconfig: - categories: true - headers: - correlation: - title: "Pearson correlation" - description: | - Pearson correlation - color: "#bf812d" - eatlas_filtered_experiments_metadata: section_name: "Expression Atlas metadata - filtered" file_format: "tsv" @@ -266,8 +334,6 @@ sp: fn: "*skewness_statistics.csv" uniform_distribution_probabilities: fn: "*ks_test_statistics.csv" - distribution_correlations: - fn: "*distribution_correlations.csv" eatlas_filtered_experiments_metadata: fn: "*filtered_experiments.metadata.tsv" eatlas_all_experiments_metadata: diff --git a/bin/clean_count_data.py b/bin/clean_count_data.py new file mode 100755 index 00000000..78ac4806 --- /dev/null +++ b/bin/clean_count_data.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 + +# Written by Olivier Coen. Released under the MIT license. + +import argparse +import sys +import polars as pl +from pathlib import Path +from dataclasses import dataclass, field +import logging + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# outfile names +ALL_COUNTS_FILTERED_PARQUET_OUTFILENAME = "cleaned_counts_filtered.parquet" + +# column names +ENSEMBL_GENE_ID_COLNAME = "ensembl_gene_id" +SAMPLE_COLNAME = "sample" +KS_TEST_COLNAME = "kolmogorov_smirnov_to_uniform_dist_pvalue" + +##################################################### +##################################################### +# FUNCTIONS +##################################################### +##################################################### + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Clean data by removing aberrant samples and performing some other cleaning operations." + ) + parser.add_argument( + "--counts", type=Path, dest="count_file", required=True, help="Count file" + ) + parser.add_argument( + "--ks-stats", + type=Path, + dest="ks_stats_file", + required=True, + help="KS stats file", + ) + parser.add_argument( + "--ks-pvalue-threshold", + type=str, + dest="ks_pvalue_threshold", + required=True, + help="KS p-value threshold", + ) + return parser.parse_args() + + +def is_valid_lf(lf: pl.LazyFrame, file: Path) -> bool: + """Check if a LazyFrame is valid. + + A LazyFrame is considered valid if it contains at least one row. + """ + try: + return not lf.limit(1).collect().is_empty() + except FileNotFoundError: + # strangely enough we get this error for some files existing but empty + logger.error(f"Could not find file {str(file)}") + return False + except pl.exceptions.NoDataError as err: + logger.error(f"File {str(file)} is empty: {err}") + return False + + +def get_valid_lazy_lfs(files: list[Path]) -> list[pl.LazyFrame]: + """Get a list of valid LazyFrames from a list of files. + + A LazyFrame is considered valid if it contains at least one row. + """ + lf_dict = {file: pl.scan_csv(file) for file in files} + return [lf for file, lf in lf_dict.items() if is_valid_lf(lf, file)] + + +def cast_cols_to_string(lf: pl.LazyFrame) -> pl.LazyFrame: + return lf.select( + [pl.col(column).cast(pl.String) for column in lf.collect_schema().names()] + ) + + +def concat_cast_to_string_and_drop_duplicates(files: list[Path]) -> pl.LazyFrame: + """Concatenate LazyFrames, cast all columns to String, and drop duplicates. + + The first step is to concatenate the LazyFrames. Then, the dataframe is cast + to String to ensure that all columns have the same data type. Finally, duplicate + rows are dropped. + """ + lfs = get_valid_lazy_lfs(files) + lfs = [cast_cols_to_string(lf) for lf in lfs] + concat_lf = pl.concat(lfs) + # dropping duplicates + # casting all columns to String + return concat_lf.unique() + + +def get_count_columns(lf: pl.LazyFrame) -> list[str]: + """Get all column names except the ENSEMBL_GENE_ID_COLNAME column. + + The ENSEMBL_GENE_ID_COLNAME column contains only gene IDs. + """ + return lf.select(pl.exclude(ENSEMBL_GENE_ID_COLNAME)).collect_schema().names() + + +def get_counts( + file: Path, +) -> pl.LazyFrame: + # sorting dataframe (necessary to get consistent output) + return pl.scan_parquet(file).sort(ENSEMBL_GENE_ID_COLNAME, descending=False) + + +def remove_samples_with_low_ks_pvalue( + count_lf: pl.LazyFrame, ks_stats_file: Path, ks_pvalue_threshold: str +) -> pl.LazyFrame: + + ks_stats_df = pl.read_csv( + ks_stats_file, has_header=True, new_columns=[SAMPLE_COLNAME, KS_TEST_COLNAME] + ) + + # parsing threshold + try: + ks_pvalue_threshold = float(ks_pvalue_threshold) + except ValueError: + raise ValueError( + f"KS p-value threshold {ks_pvalue_threshold} could not be cast to float" + ) + + # logging number of samples excluded from analysis + not_valid_samples = ks_stats_df.filter( + ks_stats_df[KS_TEST_COLNAME] <= ks_pvalue_threshold + )[SAMPLE_COLNAME].to_list() + + if not_valid_samples: + logger.warning( + f"Excluded {len(not_valid_samples)} samples showing a KS p-value below {ks_pvalue_threshold}" + ) + else: + logger.info("No sample was excluded") + + # getting samples for which the Kolmogorov-Smirnov test pvalue is above the threshold + valid_samples = ks_stats_df.filter( + ks_stats_df[KS_TEST_COLNAME] > ks_pvalue_threshold + )[SAMPLE_COLNAME].to_list() + + if not valid_samples: + logger.error("No more valid sample to process...") + sys.exit(101) + + # filtering the count dataframe to keep only the valid samples + return count_lf.select([ENSEMBL_GENE_ID_COLNAME] + valid_samples) + + +def export_data( all_counts_lf: pl.LazyFrame): + all_counts_lf.collect().write_parquet(ALL_COUNTS_FILTERED_PARQUET_OUTFILENAME) + logger.info("Done") + + +##################################################### +##################################################### +# MAIN +##################################################### +##################################################### + + +def main(): + args = parse_args() + + # putting all counts into a single dataframe + count_lf = get_counts(args.count_file) + + # removing aberrant samples (ks p-value under the threshold) + count_lf = remove_samples_with_low_ks_pvalue(count_lf, args.ks_stats_file, args.ks_pvalue_threshold) + + # exporting computed data + export_data(count_lf) + + +if __name__ == "__main__": + main() diff --git a/bin/get_gene_statistics.py b/bin/compute_final_gene_statistics.py similarity index 83% rename from bin/get_gene_statistics.py rename to bin/compute_final_gene_statistics.py index f307b0ad..6eced895 100755 --- a/bin/get_gene_statistics.py +++ b/bin/compute_final_gene_statistics.py @@ -32,18 +32,35 @@ ENSEMBL_GENE_ID_COLNAME = "ensembl_gene_id" GENE_NAME_COLNAME = "name" GENE_DESCRIPTION_COLNAME = "description" + +GENE_COUNT_COLNAME = "count" +SAMPLE_COLNAME = "sample" +NB_ZEROS_COLNAME = "nb_zeros" +STABILITY_SCORE_COLNAME = "stability_score" + VARIATION_COEFFICIENT_COLNAME = "variation_coefficient" STANDARD_DEVIATION_COLNAME = "standard_deviation" MEAN_COLNAME = "mean" EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME = "expression_level_quantile_interval" EXPRESSION_LEVEL_STATUS_COLNAME = "expression_level_status" -GENE_COUNT_COLNAME = "count" -SAMPLE_COLNAME = "sample" NB_NULLS_COLNAME = "total_nb_nulls" NB_NULLS_VALID_SAMPLES_COLNAME = "nb_nulls_valid_samples" -NB_ZEROS_COLNAME = "nb_zeros" -STABILITY_SCORE_COLNAME = "stability_score" -KS_TEST_COLNAME = "kolmogorov_smirnov_to_uniform_dist_pvalue" + +RNASEQ_VARIATION_COEFFICIENT_COLNAME = "rnaseq_variation_coefficient" +RNASEQ_STANDARD_DEVIATION_COLNAME = "rnaseq_standard_deviation" +RNASEQ_MEAN_COLNAME = "rnaseq_mean" +RNASEQ_EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME = "rnaseq_expression_level_quantile_interval" +RNASEQ_EXPRESSION_LEVEL_STATUS_COLNAME = "rnaseq_expression_level_status" +RNASEQ_NB_NULLS_COLNAME = "rnaseq_total_nb_nulls" +RNASEQ_NB_NULLS_VALID_SAMPLES_COLNAME = "rnaseq_nb_nulls_valid_samples" + +MICROARRAY_VARIATION_COEFFICIENT_COLNAME = "microarray_variation_coefficient" +MICROARRAY_STANDARD_DEVIATION_COLNAME = "microarray_standard_deviation" +MICROARRAY_MEAN_COLNAME = "microarray_mean" +MICROARRAY_EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME = "microarray_expression_level_quantile_interval" +MICROARRAY_EXPRESSION_LEVEL_STATUS_COLNAME = "microarray_expression_level_status" +MICROARRAY_NB_NULLS_COLNAME = "microarray_total_nb_nulls" +MICROARRAY_NB_NULLS_VALID_SAMPLES_COLNAME = "microarray_nb_nulls_valid_samples" STATISTICS_COLS = [ RANK_COLNAME, @@ -55,6 +72,20 @@ EXPRESSION_LEVEL_STATUS_COLNAME, NB_NULLS_COLNAME, NB_NULLS_VALID_SAMPLES_COLNAME, + RNASEQ_VARIATION_COEFFICIENT_COLNAME, + RNASEQ_STANDARD_DEVIATION_COLNAME, + RNASEQ_MEAN_COLNAME, + RNASEQ_EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME, + RNASEQ_EXPRESSION_LEVEL_STATUS_COLNAME, + RNASEQ_NB_NULLS_COLNAME, + RNASEQ_NB_NULLS_VALID_SAMPLES_COLNAME, + MICROARRAY_VARIATION_COEFFICIENT_COLNAME, + MICROARRAY_STANDARD_DEVIATION_COLNAME, + MICROARRAY_MEAN_COLNAME, + MICROARRAY_EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME, + MICROARRAY_EXPRESSION_LEVEL_STATUS_COLNAME, + MICROARRAY_NB_NULLS_COLNAME, + MICROARRAY_NB_NULLS_VALID_SAMPLES_COLNAME, GENE_NAME_COLNAME, GENE_DESCRIPTION_COLNAME, ORIGINAL_GENE_IDS_COLNAME, @@ -88,6 +119,9 @@ def parse_args(): parser.add_argument( "--counts", type=Path, dest="count_file", required=True, help="Count file" ) + parser.add_argument( + "--stats", type=str, dest="platform_stat_files", required=True, help="Platform stat file" + ) parser.add_argument( "--metadata", type=str, @@ -105,20 +139,6 @@ def parse_args(): required=True, help="Number of top stable genes to show", ) - parser.add_argument( - "--ks-stats", - type=Path, - dest="ks_stats_file", - required=True, - help="KS stats file", - ) - parser.add_argument( - "--ks-pvalue-threshold", - type=str, - dest="ks_pvalue_threshold", - required=True, - help="KS p-value threshold", - ) return parser.parse_args() @@ -183,42 +203,9 @@ def cast_count_columns_to_float32(lf: pl.LazyFrame) -> pl.LazyFrame: ) -def get_counts( - file: Path, ks_stats_file: Path, ks_pvalue_threshold: str -) -> pl.LazyFrame: +def get_counts(file: Path) -> pl.LazyFrame: # sorting dataframe (necessary to get consistent output) - count_lf = pl.scan_parquet(file).sort(ENSEMBL_GENE_ID_COLNAME, descending=False) - ks_stats_df = pl.read_csv( - ks_stats_file, has_header=False, new_columns=[SAMPLE_COLNAME, KS_TEST_COLNAME] - ) - - # parsing threshold - try: - ks_pvalue_threshold = float(ks_pvalue_threshold) - except ValueError: - raise ValueError( - f"KS p-value threshold {ks_pvalue_threshold} could not be cast to float" - ) - - # logging number of samples excluded from analysis - not_valid_samples = ks_stats_df.filter( - ks_stats_df[KS_TEST_COLNAME] <= ks_pvalue_threshold - )[SAMPLE_COLNAME].to_list() - logger.warning( - f"Excluded {len(not_valid_samples)} samples showing a KS p-value below {ks_pvalue_threshold}" - ) - - # getting samples for which the Kolmogorov-Smirnov test pvalue is above the threshold - valid_samples = ks_stats_df.filter( - ks_stats_df[KS_TEST_COLNAME] > ks_pvalue_threshold - )[SAMPLE_COLNAME].to_list() - - if not valid_samples: - logger.error("No more valid sample to process...") - sys.exit(101) - - # filtering the count dataframe to keep only the valid samples - return count_lf.select([ENSEMBL_GENE_ID_COLNAME] + valid_samples) + return pl.scan_parquet(file).sort(ENSEMBL_GENE_ID_COLNAME, descending=False) def get_metadata(metadata_files: list[Path]) -> pl.LazyFrame: @@ -241,14 +228,26 @@ def get_mappings(mapping_files: list[Path]) -> pl.LazyFrame: .alias(ORIGINAL_GENE_IDS_COLNAME) ) +def get_platform_statistics(platform_stat_files: list[Path]) -> pl.LazyFrame: + """Retrieve and concatenate metadata from a list of platform-specific statistics files.""" + lf = pl.scan_csv(platform_stat_files[0]) + if len(platform_stat_files) > 1: + for file in platform_stat_files[1:]: + new_df = pl.scan_csv(file) + lf = lf.join(new_df, on=ENSEMBL_GENE_ID_COLNAME, how="left") + return lf + def merge_data( - stat_lf: pl.LazyFrame, metadata_lf: pl.LazyFrame, mapping_lf: pl.LazyFrame + stat_lf: pl.LazyFrame, platform_stat_lf: pl.LazyFrame, metadata_lf: pl.LazyFrame, mapping_lf: pl.LazyFrame ) -> pl.LazyFrame: """Merge the statistics dataframe with the metadata dataframe and the mapping dataframe.""" # we need to ensure that the index of stat_lf are strings - return stat_lf.join(metadata_lf, on=ENSEMBL_GENE_ID_COLNAME, how="left").join( - mapping_lf, on=ENSEMBL_GENE_ID_COLNAME, how="left" + return ( + stat_lf + .join(platform_stat_lf, on=ENSEMBL_GENE_ID_COLNAME, how="left") + .join(metadata_lf, on=ENSEMBL_GENE_ID_COLNAME, how="left") + .join(mapping_lf, on=ENSEMBL_GENE_ID_COLNAME, how="left") ) @@ -313,29 +312,35 @@ def format_all_genes_statistics(stat_lf: pl.LazyFrame) -> pl.LazyFrame: def get_top_stable_genes_counts( log_count_lf: pl.LazyFrame, top_stable_genes_summary_lf: pl.LazyFrame ) -> pl.DataFrame: - # getting list of top stable genes in the order - sorted_stable_genes = ( + # getting list of top stable genes with their order + top_genes_with_order = ( top_stable_genes_summary_lf.head(NB_TOP_GENES_TO_SHOW_IN_LOG_COUNTS) .select(ENSEMBL_GENE_ID_COLNAME) - .collect() - .to_series() - .to_list() + .with_row_index("sort_order") ) - mapping_dict = {item: index for index, item in enumerate(sorted_stable_genes)} - # extracting log counts of top stable genes + # join to get only existing genes and maintain order sorted_transposed_counts_df = ( - log_count_lf.filter(pl.col(ENSEMBL_GENE_ID_COLNAME).is_in(sorted_stable_genes)) - .with_columns( - pl.col(ENSEMBL_GENE_ID_COLNAME) - .replace_strict(mapping_dict) - .alias("sort_order") + log_count_lf.join( + top_genes_with_order, + on=ENSEMBL_GENE_ID_COLNAME, + how="inner" ) .sort("sort_order", descending=False) - .drop(["sort_order", ENSEMBL_GENE_ID_COLNAME]) ).collect() - return sorted_transposed_counts_df.transpose(column_names=sorted_stable_genes) + # get the actual gene names that were found (in order) + actual_gene_names = ( + sorted_transposed_counts_df.select(ENSEMBL_GENE_ID_COLNAME) + .to_series() + .to_list() + ) + + return ( + sorted_transposed_counts_df + .drop(["sort_order", ENSEMBL_GENE_ID_COLNAME]) + .transpose(column_names=actual_gene_names) + ) def export_data( @@ -513,20 +518,21 @@ def main(): args = parse_args() metadata_files = [Path(file) for file in args.metadata_files.split(" ")] mapping_files = [Path(file) for file in args.mapping_files.split(" ")] - - # putting all counts into a single dataframe - count_lf = get_counts(args.count_file, args.ks_stats_file, args.ks_pvalue_threshold) + platform_stat_files = [Path(file) for file in args.platform_stat_files.split(" ")] + print(platform_stat_files) + count_lf = get_counts(args.count_file) # getting metadata and mappings metadata_lf = get_metadata(metadata_files) mapping_lf = get_mappings(mapping_files) + platform_stat_df = get_platform_statistics(platform_stat_files) # computing statistics (mean, standard deviation, coefficient of variation, quantiles) stability_scorer = StabilityScorer(count_lf) stat_lf = stability_scorer.compute_statistics_and_score() # add gene name, description and original gene IDs - stat_lf = merge_data(stat_lf, metadata_lf, mapping_lf) + stat_lf = merge_data(stat_lf, platform_stat_df, metadata_lf, mapping_lf) # sort genes according to the metrics present in the dataframe stat_lf = sort_dataframe(stat_lf) @@ -547,6 +553,8 @@ def main(): count_lf, top_stable_genes_summary_lf ) + print(top_stable_genes_counts_df) + # exporting computed data export_data( top_stable_genes_summary_lf, diff --git a/bin/compute_gene_statistics_per_platform.py b/bin/compute_gene_statistics_per_platform.py new file mode 100755 index 00000000..9330e163 --- /dev/null +++ b/bin/compute_gene_statistics_per_platform.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python3 + +# Written by Olivier Coen. Released under the MIT license. + +import argparse +import sys +import polars as pl +from pathlib import Path +from dataclasses import dataclass, field +import logging + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# nb of top stable genes to select and to display at the end +DEFAULT_NB_TOP_STABLE_GENES = 1000 +# we want to select samples that show a particularly low nb of genes +MIN_RATIO_GENE_COUNT_TO_MEAN = 0.75 # experimentally chosen +WEIGHT_RATIO_NB_NULLS = 1 + + +# outfile names +ALL_GENES_RESULT_OUTFILENAME = "stats_all_genes.csv" + +# column names +ENSEMBL_GENE_ID_COLNAME = "ensembl_gene_id" +VARIATION_COEFFICIENT_COLNAME = "variation_coefficient" +STANDARD_DEVIATION_COLNAME = "standard_deviation" +MEAN_COLNAME = "mean" +EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME = "expression_level_quantile_interval" +EXPRESSION_LEVEL_STATUS_COLNAME = "expression_level_status" +GENE_COUNT_COLNAME = "count" +SAMPLE_COLNAME = "sample" +NB_NULLS_COLNAME = "total_nb_nulls" +NB_NULLS_VALID_SAMPLES_COLNAME = "nb_nulls_valid_samples" +NB_ZEROS_COLNAME = "nb_zeros" +STABILITY_SCORE_COLNAME = "stability_score" + + +# quantile intervals +NB_QUANTILES = 100 + +NB_TOP_GENES_TO_SHOW_IN_LOG_COUNTS = 100 + + +##################################################### +##################################################### +# FUNCTIONS +##################################################### +##################################################### + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Get base statistics from count data for each gene. Excludes aberrant datasets." + ) + parser.add_argument( + "--counts", type=Path, dest="count_file", required=True, help="Count file" + ) + parser.add_argument( + "--platform", type=str, required=True, help="Platform name" + ) + return parser.parse_args() + + +def is_valid_lf(lf: pl.LazyFrame, file: Path) -> bool: + """Check if a LazyFrame is valid. + + A LazyFrame is considered valid if it contains at least one row. + """ + try: + return not lf.limit(1).collect().is_empty() + except FileNotFoundError: + # strangely enough we get this error for some files existing but empty + logger.error(f"Could not find file {str(file)}") + return False + except pl.exceptions.NoDataError as err: + logger.error(f"File {str(file)} is empty: {err}") + return False + + +def get_valid_lazy_lfs(files: list[Path]) -> list[pl.LazyFrame]: + """Get a list of valid LazyFrames from a list of files. + + A LazyFrame is considered valid if it contains at least one row. + """ + lf_dict = {file: pl.scan_csv(file) for file in files} + return [lf for file, lf in lf_dict.items() if is_valid_lf(lf, file)] + + +def cast_cols_to_string(lf: pl.LazyFrame) -> pl.LazyFrame: + return lf.select( + [pl.col(column).cast(pl.String) for column in lf.collect_schema().names()] + ) + + +def concat_cast_to_string_and_drop_duplicates(files: list[Path]) -> pl.LazyFrame: + """Concatenate LazyFrames, cast all columns to String, and drop duplicates. + + The first step is to concatenate the LazyFrames. Then, the dataframe is cast + to String to ensure that all columns have the same data type. Finally, duplicate + rows are dropped. + """ + lfs = get_valid_lazy_lfs(files) + lfs = [cast_cols_to_string(lf) for lf in lfs] + concat_lf = pl.concat(lfs) + # dropping duplicates + # casting all columns to String + return concat_lf.unique() + + +def get_count_columns(lf: pl.LazyFrame) -> list[str]: + """Get all column names except the ENSEMBL_GENE_ID_COLNAME column. + + The ENSEMBL_GENE_ID_COLNAME column contains only gene IDs. + """ + return lf.select(pl.exclude(ENSEMBL_GENE_ID_COLNAME)).collect_schema().names() + + +def cast_count_columns_to_float32(lf: pl.LazyFrame) -> pl.LazyFrame: + return lf.select( + [pl.col(ENSEMBL_GENE_ID_COLNAME)] + + [pl.col(column).cast(pl.Float32) for column in get_count_columns(lf)] + ) + + +def get_counts(file: Path) -> pl.LazyFrame: + # sorting dataframe (necessary to get consistent output) + return pl.scan_parquet(file).sort(ENSEMBL_GENE_ID_COLNAME, descending=False) + + +def merge_data( + stat_lf: pl.LazyFrame, metadata_lf: pl.LazyFrame, mapping_lf: pl.LazyFrame +) -> pl.LazyFrame: + """Merge the statistics dataframe with the metadata dataframe and the mapping dataframe.""" + # we need to ensure that the index of stat_lf are strings + return stat_lf.join(metadata_lf, on=ENSEMBL_GENE_ID_COLNAME, how="left").join( + mapping_lf, on=ENSEMBL_GENE_ID_COLNAME, how="left" + ) + + + +def export_data( + stat_lf: pl.LazyFrame, platform: str +): + """Export gene expression data to CSV files.""" + outfilename = f"{platform}_{ALL_GENES_RESULT_OUTFILENAME}" + logger.info( + f"Exporting statistics for all genes to: {outfilename}" + ) + stat_lf.collect().write_csv(outfilename) + logger.info("Done") + + +##################################################### +##################################################### +# CLASSES +##################################################### +##################################################### + + +@dataclass +class StabilityScorer: + + count_lf: pl.LazyFrame + platform: str + + mean_colname: str = field(init=False) + std_colname: str = field(init=False) + var_coeff_colname: str = field(init=False) + nb_nulls_colname: str = field(init=False) + nb_nulls_valid_samples_colname: str = field(init=False) + + gene_count_per_sample_df: pl.DataFrame = field(init=False) + stat_lf: pl.LazyFrame = field(init=False) + count_columns: list[str] = field(init=False) + samples_with_low_gene_count: list[str] = field(init=False) + exp_level_quantile_interval_colname: str = field(init=False) + stability_score_colname: str = field(init=False) + + def __post_init__(self): + self.count_columns = get_count_columns(self.count_lf) + self.gene_count_per_sample_df = self.get_gene_counts_per_sample() + self.samples_with_low_gene_count = self.get_samples_with_low_gene_count() + + self.mean_colname = f"{self.platform}_{MEAN_COLNAME}" + self.std_colname = f"{self.platform}_{STANDARD_DEVIATION_COLNAME}" + self.var_coeff_colname = f"{self.platform}_{VARIATION_COEFFICIENT_COLNAME}" + self.nb_nulls_colname = f"{self.platform}_{NB_NULLS_COLNAME}" + self.nb_nulls_valid_samples_colname = f"{self.platform}_{NB_NULLS_VALID_SAMPLES_COLNAME}" + self.exp_level_quantile_interval_colname = f"{self.platform}_{EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME}" + self.stability_score_colname = f"{self.platform}_{STABILITY_SCORE_COLNAME}" + + def get_valid_counts(self) -> pl.LazyFrame: + return self.count_lf.select(pl.exclude(ENSEMBL_GENE_ID_COLNAME)) + + def get_gene_counts_per_sample(self) -> pl.DataFrame: + """ + Get the number of non-null values per sample. + :return: + A polars dataframe containing 2 columns: + - sample: name of the sample + - nb_not_nulls: number of non-null values + """ + return ( + self.count_lf.select(pl.exclude(ENSEMBL_GENE_ID_COLNAME)) + .count() + .collect() + .transpose( + include_header=True, header_name="sample", column_names=["count"] + ) + ) + + def get_samples_with_low_gene_count(self) -> list[str]: + mean_gene_count = self.gene_count_per_sample_df[GENE_COUNT_COLNAME].mean() + return ( + self.gene_count_per_sample_df.filter( + (pl.col(GENE_COUNT_COLNAME) / mean_gene_count) + < MIN_RATIO_GENE_COUNT_TO_MEAN + ) + .select(SAMPLE_COLNAME) + .to_series() + .to_list() + ) + + def get_main_statistics(self) -> pl.LazyFrame: + """ + Compute count descriptive statistics for each gene in the count dataframe. + """ + logger.info("Getting descriptive statistics") + # computing main stats + augmented_count_lf = self.count_lf.with_columns( + mean=pl.concat_list(self.count_columns).list.drop_nulls().list.mean(), + std=pl.concat_list(self.count_columns).list.drop_nulls().list.std(), + ) + return augmented_count_lf.select( + pl.col(ENSEMBL_GENE_ID_COLNAME), + pl.col("mean").alias(self.mean_colname), + pl.col("std").alias(self.std_colname), + (pl.col("std") / pl.col("mean")).alias(self.var_coeff_colname), + ) + + def compute_nb_null_values(self): + # the samples showing a low gene count will not be taken into account for the zero count penalty + cols_to_exclude = [ENSEMBL_GENE_ID_COLNAME] + self.samples_with_low_gene_count + total_nb_nulls = ( + self.count_lf.select(pl.exclude(ENSEMBL_GENE_ID_COLNAME).is_null()) + .collect() + .sum_horizontal() + ) + nb_nulls_valid_samples = ( + self.count_lf.select(pl.exclude(cols_to_exclude).is_null()) + .collect() + .sum_horizontal() + ) + self.stat_lf = self.stat_lf.with_columns( + total_nb_nulls.alias(self.nb_nulls_colname), + nb_nulls_valid_samples.alias(self.nb_nulls_valid_samples_colname), + ) + + def get_quantile_intervals(self): + """ + Compute the quantile intervals for the mean expression levels of each gene in the dataframe. + + The function assigns to each gene a quantile interval of its mean cpm compared to all genes. + """ + logger.info("Getting cpm quantiles") + self.stat_lf = self.stat_lf.with_columns( + (pl.col(self.mean_colname).rank() / pl.col(self.mean_colname).count() * NB_QUANTILES) + .floor() + .cast(pl.Int8) + # we want the only value = NB_QUANTILES to be NB_QUANTILES - 1 + # because the last quantile interval is [NB_QUANTILES - 1, NB_QUANTILES] + .replace({NB_QUANTILES: NB_QUANTILES - 1}) + .alias(self.exp_level_quantile_interval_colname) + ) + + def compute_stability_score(self): + logger.info("Computing stability score") + nb_valid_samples = self.gene_count_per_sample_df.select(pl.len()).item() - len( + self.samples_with_low_gene_count + ) + ratio_nb_nulls = ( + self.stat_lf.select( + pl.col(self.nb_nulls_valid_samples_colname) / nb_valid_samples + ) + .collect() + .to_series() + ) + expr = ( + pl.col(self.std_colname) + ratio_nb_nulls * WEIGHT_RATIO_NB_NULLS + ) + self.stat_lf = self.stat_lf.with_columns(expr.alias(self.stability_score_colname)) + + def compute_statistics_and_score(self) -> pl.LazyFrame: + logger.info("Computing statistics and stability score") + # getting expression statistics + self.stat_lf = self.get_main_statistics() + # adding column for nb of null values for each gene + self.compute_nb_null_values() + # computing stability score + self.compute_stability_score() + # getting quantile intervals + self.get_quantile_intervals() + + return self.stat_lf + + +##################################################### +##################################################### +# MAIN +##################################################### +##################################################### + + +def main(): + args = parse_args() + + # putting all counts into a single dataframe + count_lf = get_counts(args.count_file) + + # computing statistics (mean, standard deviation, coefficient of variation, quantiles) + stability_scorer = StabilityScorer(count_lf, args.platform) + stat_lf = stability_scorer.compute_statistics_and_score() + + # exporting computed data + export_data( stat_lf, args.platform ) + + +if __name__ == "__main__": + main() diff --git a/bin/merge_data.py b/bin/merge_counts.py similarity index 51% rename from bin/merge_data.py rename to bin/merge_counts.py index ae269924..84024ad4 100755 --- a/bin/merge_data.py +++ b/bin/merge_counts.py @@ -12,27 +12,8 @@ logger = logging.getLogger(__name__) ALL_COUNTS_PARQUET_OUTFILENAME = "all_counts.parquet" -GENE_COUNT_STATS_OUTFILENAME = "gene_count_statistics.csv" -SKEWNESS_STATS_OUTFILENAME = "skewness_statistics.csv" -KS_TEST_STATS_OUTFILENAME = "ks_test_statistics.csv" -CANDIDATE_GENE_COUNTS_PARQUET_OUTFILENAME = "candidate_gene_counts.parquet" -DISTRIBUTION_CORRELATIONS_OUTFILENAME = "distribution_correlations.csv" ENSEMBL_GENE_ID_COLNAME = "ensembl_gene_id" -STATISTIC_TYPE_COLNAME = "stat_type" -GENE_COUNT_COLNAME = "count" -SKEWNESS_COLNAME = "skewness" -KS_TEST_COLNAME = "kolmogorov_smirnov_pvalue" -SAMPLE_COLNAME = "sample" - -STAT_COLNAME_TO_PARAMS = { - GENE_COUNT_COLNAME: { - "outfilename": GENE_COUNT_STATS_OUTFILENAME, - "descending": False, - }, - SKEWNESS_COLNAME: {"outfilename": SKEWNESS_STATS_OUTFILENAME, "descending": False}, - KS_TEST_COLNAME: {"outfilename": KS_TEST_STATS_OUTFILENAME, "descending": True}, -} ##################################################### @@ -44,25 +25,11 @@ def parse_args(): parser = argparse.ArgumentParser( - description="Get variation from count data for each gene" + description="Merge count datasets" ) parser.add_argument( "--counts", type=str, dest="count_files", required=True, help="Count files" ) - parser.add_argument( - "--stats", - type=str, - dest="dataset_stat_files", - required=True, - help="Dataset stats files", - ) - parser.add_argument( - "--nb-candidate-genes", - type=int, - dest="nb_candidate_genes", - required=True, - help="Number of candidate genes to keep", - ) return parser.parse_args() @@ -153,95 +120,16 @@ def get_nb_rows(lf: pl.LazyFrame) -> int: return lf.select(pl.len()).collect().item() -##################################################### -# STATISTICS -##################################################### - - -def parse_stat_file(stat_file: Path) -> pl.DataFrame: - return pl.read_csv(stat_file, has_header=True) - - -def merge_stats(stat_files: list[Path]) -> pl.DataFrame: - stat_dfs = [parse_stat_file(stat_file) for stat_file in stat_files] - return pl.concat(stat_dfs, how="vertical") - - -def compute_distances_to_mean(count_df: pl.DataFrame) -> pl.DataFrame: - corr_dict = {"sample": [], "correlation": []} - - count_df = count_df.select(pl.exclude(ENSEMBL_GENE_ID_COLNAME)) - mean_series = count_df.mean_horizontal() - - for sample in count_df.columns: - correlation = count_df.select(pl.corr(count_df[sample], mean_series)) - corr_dict["sample"].append(sample) - corr_dict["correlation"].append(correlation.item()) - - return ( - pl.DataFrame(corr_dict) - .fill_nan(None) - .sort(by="correlation", descending=True, nulls_last=True) - ) - - -##################################################### -# CANDIDATE GENES -##################################################### - - -def get_candidate_gene_counts( - count_df: pl.DataFrame, nb_candidate_genes: int -) -> pl.DataFrame: - candidate_gene_lf = ( - count_df.with_columns( - std=pl.concat_list(pl.exclude(ENSEMBL_GENE_ID_COLNAME)) - .list.drop_nulls() - .list.std() - ) - .sort("std", descending=False) - .head(nb_candidate_genes) - ) - candidate_gene_ids = ( - candidate_gene_lf.select(ENSEMBL_GENE_ID_COLNAME).to_series().to_list() - ) - return count_df.filter(pl.col(ENSEMBL_GENE_ID_COLNAME).is_in(candidate_gene_ids)) - - ##################################################### # EXPORT ##################################################### -def export_data( - count_df: pl.DataFrame, - candidate_gene_counts_df: pl.DataFrame, - corr_df: pl.DataFrame, -): +def export_data(count_df: pl.DataFrame ): """Export gene expression data.""" logger.info(f"Exporting normalised counts to: {ALL_COUNTS_PARQUET_OUTFILENAME}") count_df.write_parquet(ALL_COUNTS_PARQUET_OUTFILENAME) - logger.info( - f"Exporting candidate gene counts to: {CANDIDATE_GENE_COUNTS_PARQUET_OUTFILENAME}" - ) - candidate_gene_counts_df.write_parquet(CANDIDATE_GENE_COUNTS_PARQUET_OUTFILENAME) - - logger.info( - f"Exporting distribution correlations to: {DISTRIBUTION_CORRELATIONS_OUTFILENAME}" - ) - corr_df.write_csv(DISTRIBUTION_CORRELATIONS_OUTFILENAME, include_header=False) - - -def export_individual_statistics(dataset_stats_df: pl.DataFrame): - for data_col, params in STAT_COLNAME_TO_PARAMS.items(): - outfilename = params["outfilename"] - logger.info(f"Exporting {data_col} statistics to: {outfilename}") - sorted_data = dataset_stats_df[[SAMPLE_COLNAME, data_col]].sort( - data_col, descending=params["descending"] - ) - sorted_data.write_csv(outfilename, include_header=False) - ##################################################### ##################################################### @@ -253,22 +141,10 @@ def export_individual_statistics(dataset_stats_df: pl.DataFrame): def main(): args = parse_args() count_files = [Path(file) for file in args.count_files.split(" ")] - dataset_stat_files = [Path(file) for file in args.dataset_stat_files.split(" ")] # putting all counts into a single dataframe count_df = get_counts(count_files) - # putting all stats data into a single dataframe - dataset_stats_df = merge_stats(dataset_stat_files) - - candidate_gene_counts_df = get_candidate_gene_counts( - count_df, args.nb_candidate_genes - ) - - # adding stat about divergence to mean distribution - corr_df = compute_distances_to_mean(count_df) - - export_data(count_df, candidate_gene_counts_df, corr_df) - export_individual_statistics(dataset_stats_df) + export_data(count_df) if __name__ == "__main__": diff --git a/modules/local/clean_count_data/main.nf b/modules/local/clean_count_data/main.nf new file mode 100644 index 00000000..dbf5c57f --- /dev/null +++ b/modules/local/clean_count_data/main.nf @@ -0,0 +1,38 @@ +process CLEAN_COUNT_DATA { + + label 'process_low' + + errorStrategy = { + if (task.exitStatus == 101) { + log.error( + "No more valid sample after checking p-value of Kolmogorow-Smirnoff test against target distribution! " + + "You can try a more flexible approach by setting again the value of the ks_pvalue_threshold parameter. " + + "Provide a negative value to disable this filter." + ) + return 'terminate' + } + } + + conda "${moduleDir}/spec-file.txt" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': + 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" + + input: + tuple val(meta), path(count_file), path(ks_stats_file) + val ks_pvalue_threshold + + output: + tuple val(meta), path('cleaned_counts_filtered.parquet'), emit: counts + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions + tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions + + script: + """ + clean_count_data.py \\ + --counts $count_file \\ + --ks-stats $ks_stats_file \\ + --ks-pvalue-threshold $ks_pvalue_threshold + """ + +} diff --git a/modules/local/gene_statistics/spec-file.txt b/modules/local/clean_count_data/spec-file.txt similarity index 100% rename from modules/local/gene_statistics/spec-file.txt rename to modules/local/clean_count_data/spec-file.txt diff --git a/modules/local/gene_statistics/main.nf b/modules/local/compute_gene_statistics/global/main.nf similarity index 64% rename from modules/local/gene_statistics/main.nf rename to modules/local/compute_gene_statistics/global/main.nf index 145b3aeb..02527b0f 100644 --- a/modules/local/gene_statistics/main.nf +++ b/modules/local/compute_gene_statistics/global/main.nf @@ -1,4 +1,4 @@ -process GENE_STATISTICS { +process COMPUTE_GLOBAL_GENE_STATISTICS { label 'process_low' @@ -9,13 +9,6 @@ process GENE_STATISTICS { + "Please check the provided accessions and datasets and run again" ) return 'terminate' - } else if (task.exitStatus == 101) { - log.error( - "No more valid sample after checking p-value of Kolmogorow-Smirnoff test against target distribution! " - + "You can try a more flexible approach by setting again the value of the ks_pvalue_threshold parameter. " - + "Provide a negative value to disable this filter." - ) - return 'terminate' } } @@ -26,16 +19,14 @@ process GENE_STATISTICS { input: path count_file + path platform_statistic_files, stageAs: "?/*" path metadata_files, stageAs: "?/*" path mapping_files, stageAs: "?/*" val nb_top_stable_genes - path ks_stats_file - val ks_pvalue_threshold output: path 'top_stable_genes_summary.csv', emit: top_stable_genes_summary path 'stats_all_genes.csv', emit: all_statistics - path 'all_counts_filtered.parquet', emit: all_counts path 'top_stable_genes_transposed_counts_filtered.csv', emit: top_stable_genes_transposed_counts tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions @@ -45,13 +36,12 @@ process GENE_STATISTICS { script: """ - get_gene_statistics.py \ - --counts $count_file \ - --metadata "$metadata_files" \ - --mappings "$mapping_files" \ - --nb-top-stable-genes $nb_top_stable_genes \ - --ks-stats $ks_stats_file \ - --ks-pvalue-threshold $ks_pvalue_threshold + compute_final_gene_statistics.py \\ + --counts $count_file \\ + --stats "$platform_statistic_files" \\ + --metadata "$metadata_files" \\ + --mappings "$mapping_files" \\ + --nb-top-stable-genes $nb_top_stable_genes """ } diff --git a/modules/local/merge_data/spec-file.txt b/modules/local/compute_gene_statistics/global/spec-file.txt similarity index 100% rename from modules/local/merge_data/spec-file.txt rename to modules/local/compute_gene_statistics/global/spec-file.txt diff --git a/modules/local/compute_gene_statistics/per_platform/main.nf b/modules/local/compute_gene_statistics/per_platform/main.nf new file mode 100644 index 00000000..9aee3dd7 --- /dev/null +++ b/modules/local/compute_gene_statistics/per_platform/main.nf @@ -0,0 +1,36 @@ +process COMPUTE_GENE_STATISTICS_PER_PLATFORM { + + label 'process_low' + + errorStrategy = { + if (task.exitStatus == 100) { + log.error( + "No count could be found before merging datasets! " + + "Please check the provided accessions and datasets and run again" + ) + return 'terminate' + } + } + + conda "${moduleDir}/spec-file.txt" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': + 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" + + input: + path count_file + val platform + + output: + path '*_stats_all_genes.csv', emit: stats + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions + tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions + + script: + """ + compute_gene_statistics_per_platform.py \\ + --counts $count_file \\ + --platform $platform + """ + +} diff --git a/modules/local/compute_gene_statistics/per_platform/spec-file.txt b/modules/local/compute_gene_statistics/per_platform/spec-file.txt new file mode 100644 index 00000000..1bf5d691 --- /dev/null +++ b/modules/local/compute_gene_statistics/per_platform/spec-file.txt @@ -0,0 +1,40 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda#3cd1a7238a0dd3d0860fdefc496cc854 +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda#9e60c55e725c20d23125a5f0dd69af5d +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda#e66f2b8ad787e7beb0f846e4bd7e8493 +https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62ee74e96c5ebb0af99386de58cf9553 +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.6.15-hbd8a1cb_0.conda#72525f07d72806e3b639ad4504c30ce5 +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_0.conda#e31316a586cac398b1fcdb10ace786b9 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda#530566b68c3b8ce7eec4cd047eae19fe +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda#bfbca721fd33188ef923dfe9ba172f29 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda#323dc8f259224d13078aaf7ce96c3efe +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-32_h59b9bed_openblas.conda#2af9f3d5c2e39f417ce040f5a35c40c6 +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-32_he106b2a_openblas.conda#3d3f9355e52f269cd8bc2c440d8a5263 +https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda#db0bfbe7dd197b68ad5f30333bae6ce0 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-32_h7ac8fdf_openblas.conda#6c3f04ccb6c578138e9f9899da0bd714 +https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc +https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda#d864d34357c3b65a4b731f78c0801dc4 +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.2-h6cd9bfd_0.conda#b04c7eda6d7dab1e6503135e7fad4d25 +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda#6d11a5edae89fe413c0569f16d308f5a +https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b +https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda#c87df2ab1448ba69169652ab9547082d +https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 +https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 +https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a +https://conda.anaconda.org/conda-forge/linux-64/python-3.12.8-h9e4cc4f_1_cpython.conda#7fd2fd79436d9b473812f14e86746844 +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda#0dfcdc155cf23812a0c9deada86fb723 +https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.1-py312h6cf2f7f_0.conda#7e086a30150af2536a1059885368dcf0 +https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda#58335b26c38bf4a20f399384c33cbcf9 +https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e +https://conda.anaconda.org/conda-forge/noarch/wheel-0.45.1-pyhd8ed1ab_1.conda#75cb7132eb58d97896e173ef12ac9986 +https://conda.anaconda.org/conda-forge/noarch/pip-25.1.1-pyh8b19718_0.conda#32d0781ace05105cc99af55d36cbec7c +https://conda.anaconda.org/conda-forge/linux-64/polars-1.17.1-py312hda0fa55_1.conda#d9d77bfc286b6044dc045d1696c6acdc diff --git a/modules/local/merge_counts/main.nf b/modules/local/merge_counts/main.nf new file mode 100644 index 00000000..8bfb4ad9 --- /dev/null +++ b/modules/local/merge_counts/main.nf @@ -0,0 +1,27 @@ +process MERGE_COUNTS { + + label 'process_low' + + conda "${moduleDir}/spec-file.txt" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': + 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" + + input: + path count_files, stageAs: "?/*" + + output: + path 'all_counts.parquet', emit: all_counts + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions + tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions + + when: + task.ext.when == null || task.ext.when + + script: + """ + merge_counts.py \\ + --counts "$count_files" + """ + +} diff --git a/modules/local/merge_counts/spec-file.txt b/modules/local/merge_counts/spec-file.txt new file mode 100644 index 00000000..1bf5d691 --- /dev/null +++ b/modules/local/merge_counts/spec-file.txt @@ -0,0 +1,40 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda#3cd1a7238a0dd3d0860fdefc496cc854 +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda#9e60c55e725c20d23125a5f0dd69af5d +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda#e66f2b8ad787e7beb0f846e4bd7e8493 +https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62ee74e96c5ebb0af99386de58cf9553 +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.6.15-hbd8a1cb_0.conda#72525f07d72806e3b639ad4504c30ce5 +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_0.conda#e31316a586cac398b1fcdb10ace786b9 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda#530566b68c3b8ce7eec4cd047eae19fe +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda#bfbca721fd33188ef923dfe9ba172f29 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda#323dc8f259224d13078aaf7ce96c3efe +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-32_h59b9bed_openblas.conda#2af9f3d5c2e39f417ce040f5a35c40c6 +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-32_he106b2a_openblas.conda#3d3f9355e52f269cd8bc2c440d8a5263 +https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda#db0bfbe7dd197b68ad5f30333bae6ce0 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-32_h7ac8fdf_openblas.conda#6c3f04ccb6c578138e9f9899da0bd714 +https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc +https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda#d864d34357c3b65a4b731f78c0801dc4 +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.2-h6cd9bfd_0.conda#b04c7eda6d7dab1e6503135e7fad4d25 +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda#6d11a5edae89fe413c0569f16d308f5a +https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b +https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda#c87df2ab1448ba69169652ab9547082d +https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 +https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 +https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a +https://conda.anaconda.org/conda-forge/linux-64/python-3.12.8-h9e4cc4f_1_cpython.conda#7fd2fd79436d9b473812f14e86746844 +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda#0dfcdc155cf23812a0c9deada86fb723 +https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.1-py312h6cf2f7f_0.conda#7e086a30150af2536a1059885368dcf0 +https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda#58335b26c38bf4a20f399384c33cbcf9 +https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e +https://conda.anaconda.org/conda-forge/noarch/wheel-0.45.1-pyhd8ed1ab_1.conda#75cb7132eb58d97896e173ef12ac9986 +https://conda.anaconda.org/conda-forge/noarch/pip-25.1.1-pyh8b19718_0.conda#32d0781ace05105cc99af55d36cbec7c +https://conda.anaconda.org/conda-forge/linux-64/polars-1.17.1-py312hda0fa55_1.conda#d9d77bfc286b6044dc045d1696c6acdc diff --git a/modules/local/merge_data/main.nf b/modules/local/merge_data/main.nf deleted file mode 100644 index ea96e22b..00000000 --- a/modules/local/merge_data/main.nf +++ /dev/null @@ -1,36 +0,0 @@ -process MERGE_DATA { - - label 'process_low' - - conda "${moduleDir}/spec-file.txt" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': - 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" - - input: - path count_files, stageAs: "?/*" - path dataset_stat_files, stageAs: "?/*" - val nb_candidate_genes - - output: - path 'all_counts.parquet', emit: all_counts - path 'gene_count_statistics.csv', emit: gene_count_statistics - path 'skewness_statistics.csv', emit: skewness_statistics - path 'ks_test_statistics.csv', emit: ks_test_statistics - path 'candidate_gene_counts.parquet', emit: candidate_gene_counts - path 'distribution_correlations.csv', emit: distribution_correlations - tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions - tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions - - when: - task.ext.when == null || task.ext.when - - script: - """ - merge_data.py \ - --counts "$count_files" \ - --stats "$dataset_stat_files" \ - --nb-candidate-genes $nb_candidate_genes - """ - -} diff --git a/nextflow.config b/nextflow.config index 8c7f7153..f806afb0 100644 --- a/nextflow.config +++ b/nextflow.config @@ -17,7 +17,7 @@ params { // statistics normalisation_method = 'deseq2' - quant_norm_target_distrib = 'normal' + quantile_normalisation_target_distribution = 'uniform' nb_top_gene_candidates = 1000 ks_pvalue_threshold = 0 diff --git a/nextflow_schema.json b/nextflow_schema.json index 25e8ad94..718b2d6b 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -82,7 +82,7 @@ "enum": ["rnaseq", "microarray"], "description": "Only download Expression Atlas experiments from this platform", "fa_icon": "fas fa-id-card", - "help_text": "By default, data from all platform are downloaded. If this parameter is specified, a filter is applied to get data from only one specific type of platform." + "help_text": "By default, data from all platform are downloaded. If this parameter is specified, a filter is applied to get data from only one specific type of platform. This filter is only used while fetching appropriate Expression atlas accessions. It will not filter accessions provided with --eatlas_accessions or --eatlas_accessions_file." }, "eatlas_accessions_file": { "type": "string", @@ -165,12 +165,12 @@ "default": "deseq2", "help_text": "Raw RNAseq data must be normalised before further processing. You can select the package used for normalisation." }, - "quant_norm_target_distrib": { + "quantile_normalisation_target_distribution": { "type": "string", "description": "Target distribution to map to during quantile normalisation", "fa_icon": "fas fa-chart-simple", "enum": ["normal", "uniform"], - "default": "normal", + "default": "uniform", "help_text": "All sample counts get quantile normalised and mapped to a specific distribution so that subsequent can compare them. The pipeline uses scikit-learn's quantile_transform function. You can select the target distribution to map counts to." }, "nb_top_gene_candidates": { diff --git a/subworkflows/local/data_cleansing/main.nf b/subworkflows/local/data_cleansing/main.nf new file mode 100644 index 00000000..9c1ae1f4 --- /dev/null +++ b/subworkflows/local/data_cleansing/main.nf @@ -0,0 +1,45 @@ +include { DATASET_STATISTICS } from '../../../modules/local/dataset_statistics' +include { CLEAN_COUNT_DATA } from '../../../modules/local/clean_count_data' + +/* +======================================================================================== + SUBWORKFLOW TO NORMALISE AND HARMONISE EXPRESSION DATASETS +======================================================================================== +*/ + +workflow DATA_CLEANSING { + + take: + ch_quantile_normalised_datasets + quantile_normalisation_target_distribution + ks_pvalue_threshold + + main: + + // + // Get global stats for each sample in each dataset + // + + DATASET_STATISTICS( + ch_quantile_normalised_datasets, + quantile_normalisation_target_distribution + ) + + // + // Filter out aberrant samples and perform some sorting / cleaning + // + + CLEAN_COUNT_DATA ( + ch_quantile_normalised_datasets.join( DATASET_STATISTICS.out.stats ), + ks_pvalue_threshold + ) + + + emit: + cleaned_counts = CLEAN_COUNT_DATA.out.counts + +} + + + + diff --git a/subworkflows/local/expression_normalisation/main.nf b/subworkflows/local/expression_normalisation/main.nf index ec3de5fb..9928c023 100644 --- a/subworkflows/local/expression_normalisation/main.nf +++ b/subworkflows/local/expression_normalisation/main.nf @@ -1,7 +1,6 @@ include { NORMALISATION_DESEQ2 } from '../../../modules/local/normalisation/deseq2' include { NORMALISATION_EDGER } from '../../../modules/local/normalisation/edger' include { QUANTILE_NORMALISATION } from '../../../modules/local/quantile_normalisation' -include { DATASET_STATISTICS } from '../../../modules/local/dataset_statistics' /* ======================================================================================== @@ -14,7 +13,7 @@ workflow EXPRESSION_NORMALISATION { take: ch_datasets normalisation_method - quant_norm_target_distrib + quantile_normalisation_target_distribution main: @@ -51,22 +50,12 @@ workflow EXPRESSION_NORMALISATION { QUANTILE_NORMALISATION ( quant_norm_input, - quant_norm_target_distrib + quantile_normalisation_target_distribution ) - ch_quantile_normalised_datasets = QUANTILE_NORMALISATION.out.counts - // - // MODULE: Dataset statistics - // - - DATASET_STATISTICS( - ch_quantile_normalised_datasets, - quant_norm_target_distrib - ) emit: - normalised_counts = ch_quantile_normalised_datasets - dataset_statistics = DATASET_STATISTICS.out.stats + normalised_counts = QUANTILE_NORMALISATION.out.counts } diff --git a/subworkflows/local/merge_compute_stats/main.nf b/subworkflows/local/merge_compute_stats/main.nf new file mode 100644 index 00000000..7aa37555 --- /dev/null +++ b/subworkflows/local/merge_compute_stats/main.nf @@ -0,0 +1,64 @@ +include { MERGE_COUNTS } from '../../../modules/local/merge_counts' +include { COMPUTE_GLOBAL_GENE_STATISTICS } from '../../../modules/local/compute_gene_statistics/global' + +include { MERGE_COMPUTE_STATS_PER_PLATFORM as MERGE_COMPUTE_STATS_MICROARRAY } from '../merge_compute_stats_per_platform' +include { MERGE_COMPUTE_STATS_PER_PLATFORM as MERGE_COMPUTE_STATS_RNASEQ } from '../merge_compute_stats_per_platform' + +/* +======================================================================================== + SUBWORKFLOW TO DOWNLOAD EXPRESSIONATLAS ACCESSIONS AND DATASETS +======================================================================================== +*/ + +workflow MERGE_COMPUTE_STATS { + + take: + ch_normalised_counts + ch_gene_metadata + ch_gene_id_mapping + + main: + + MERGE_COMPUTE_STATS_RNASEQ ( + ch_normalised_counts.filter { meta, file -> meta.platform == "rnaseq" }, + "rnaseq" + ) + + MERGE_COMPUTE_STATS_MICROARRAY ( + ch_normalised_counts.filter { meta, file -> meta.platform == "microarray" }, + "microarray" + ) + + // ----------------------------------------------------------------- + // MERGE RNASEQ AND MICROARRAY COUNTS + // ----------------------------------------------------------------- + + Channel.empty() + .mix ( MERGE_COMPUTE_STATS_RNASEQ.out.counts ) + .mix ( MERGE_COMPUTE_STATS_MICROARRAY.out.counts ) + .set { ch_all_counts } + + MERGE_COUNTS( ch_all_counts.collect() ) + + // ----------------------------------------------------------------- + // GENE STATISTICS + // ----------------------------------------------------------------- + + Channel.empty() + .mix ( MERGE_COMPUTE_STATS_RNASEQ.out.stats ) + .mix ( MERGE_COMPUTE_STATS_MICROARRAY.out.stats ) + .set { ch_platform_statistics } + + COMPUTE_GLOBAL_GENE_STATISTICS( + MERGE_COUNTS.out.all_counts, + ch_platform_statistics.collect(), + ch_gene_metadata.collect(), + ch_gene_id_mapping.collect(), + params.nb_top_gene_candidates + ) + + emit: + top_stable_genes_summary = COMPUTE_GLOBAL_GENE_STATISTICS.out.top_stable_genes_summary + all_genes_statistics = COMPUTE_GLOBAL_GENE_STATISTICS.out.all_statistics + top_stable_genes_transposed_counts = COMPUTE_GLOBAL_GENE_STATISTICS.out.top_stable_genes_transposed_counts +} diff --git a/subworkflows/local/merge_compute_stats_per_platform/main.nf b/subworkflows/local/merge_compute_stats_per_platform/main.nf new file mode 100644 index 00000000..fcbe1711 --- /dev/null +++ b/subworkflows/local/merge_compute_stats_per_platform/main.nf @@ -0,0 +1,40 @@ +include { MERGE_COUNTS } from '../../../modules/local/merge_counts' +include { COMPUTE_GENE_STATISTICS_PER_PLATFORM } from '../../../modules/local/compute_gene_statistics/per_platform' + +/* +======================================================================================== + SUBWORKFLOW TO DOWNLOAD EXPRESSIONATLAS ACCESSIONS AND DATASETS +======================================================================================== +*/ + +workflow MERGE_COMPUTE_STATS_PER_PLATFORM { + + take: + ch_normalised_counts + platform + + main: + + // ----------------------------------------------------------------- + // MERGE COUNT FILES AND DESIGN FILES AND FILTER OUT ZERO COUNTS + // ----------------------------------------------------------------- + + MERGE_COUNTS( + ch_normalised_counts.map { meta, file -> [file] }.collect() + ) + MERGE_COUNTS.out.all_counts.set { ch_merged_counts } + + // ----------------------------------------------------------------- + // GENE STATISTICS + // ----------------------------------------------------------------- + + COMPUTE_GENE_STATISTICS_PER_PLATFORM( + ch_merged_counts, + platform + ) + + emit: + counts = ch_merged_counts + stats = COMPUTE_GENE_STATISTICS_PER_PLATFORM.out.stats + +} diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 3a05fc29..689f7391 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -7,10 +7,11 @@ include { EXPRESSIONATLAS_FETCHDATA } from '../subworkflows/local/expressionatlas_fetchdata' include { IDMAPPING } from '../subworkflows/local/idmapping' include { EXPRESSION_NORMALISATION } from '../subworkflows/local/expression_normalisation' +include { DATA_CLEANSING } from '../subworkflows/local/data_cleansing' +include { MERGE_COMPUTE_STATS } from '../subworkflows/local/merge_compute_stats' include { MULTIQC_WORKFLOW } from '../subworkflows/local/multiqc' -include { MERGE_DATA } from '../modules/local/merge_data' -include { GENE_STATISTICS } from '../modules/local/gene_statistics' + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -30,10 +31,6 @@ workflow STABLEEXPRESSION { ch_top_stable_genes_summary = Channel.empty() ch_all_genes_statistics = Channel.empty() ch_top_stable_genes_transposed_counts = Channel.empty() - ch_gene_count_statistics = Channel.empty() - ch_skewness_statistics = Channel.empty() - ch_ks_stats = Channel.empty() - ch_distribution_correlations = Channel.empty() ch_species = Channel.value( params.species.split(' ').join('_') ) @@ -63,44 +60,33 @@ workflow STABLEEXPRESSION { EXPRESSION_NORMALISATION( IDMAPPING.out.datasets, params.normalisation_method, - params.quant_norm_target_distrib + params.quantile_normalisation_target_distribution ) - EXPRESSION_NORMALISATION.out.normalised_counts.set { ch_normalised_counts } - EXPRESSION_NORMALISATION.out.dataset_statistics.set { ch_dataset_statistics } - // ----------------------------------------------------------------- - // MERGE COUNT FILES AND DESIGN FILES AND FILTER OUT ZERO COUNTS + // GET STATISTICS DATASET BY DATASET AND PERFORM SOME CLEANING OPERATIONS // ----------------------------------------------------------------- - MERGE_DATA( - ch_normalised_counts.map { meta, file -> [file] }.collect(), - ch_dataset_statistics.map { meta, file -> [file] }.collect(), - params.nb_top_gene_candidates + DATA_CLEANSING( + EXPRESSION_NORMALISATION.out.normalised_counts, + params.quantile_normalisation_target_distribution, + params.ks_pvalue_threshold ) - MERGE_DATA.out.candidate_gene_counts.set { ch_candidate_gene_counts } - MERGE_DATA.out.ks_test_statistics.set { ch_ks_stats } - MERGE_DATA.out.gene_count_statistics.set { ch_gene_count_statistics } - MERGE_DATA.out.skewness_statistics.set { ch_skewness_statistics } - MERGE_DATA.out.distribution_correlations.set { ch_distribution_correlations } - // ----------------------------------------------------------------- - // GENE STATISTICS + // MERGE DATA AND COMPUTE VARIOUS STATISTICS // ----------------------------------------------------------------- - GENE_STATISTICS( - MERGE_DATA.out.all_counts, - IDMAPPING.out.gene_metadata.collect(), - IDMAPPING.out.gene_id_mapping.collect(), - params.nb_top_gene_candidates, - ch_ks_stats, - params.ks_pvalue_threshold + MERGE_COMPUTE_STATS ( + DATA_CLEANSING.out.cleaned_counts, + IDMAPPING.out.gene_metadata, + IDMAPPING.out.gene_id_mapping + ) - GENE_STATISTICS.out.top_stable_genes_summary.set { ch_top_stable_genes_summary } - GENE_STATISTICS.out.all_statistics.set { ch_all_genes_statistics } - GENE_STATISTICS.out.top_stable_genes_transposed_counts.set { ch_top_stable_genes_transposed_counts } + MERGE_COMPUTE_STATS.out.top_stable_genes_summary.set { ch_top_stable_genes_summary } + MERGE_COMPUTE_STATS.out.all_genes_statistics.set { ch_all_genes_statistics } + MERGE_COMPUTE_STATS.out.top_stable_genes_transposed_counts.set { ch_top_stable_genes_transposed_counts } } @@ -112,10 +98,6 @@ workflow STABLEEXPRESSION { .mix( ch_top_stable_genes_summary.collect() ) .mix( ch_all_genes_statistics.collect() ) .mix( ch_top_stable_genes_transposed_counts.collect() ) - .mix( ch_gene_count_statistics.collect() ) - .mix( ch_skewness_statistics.collect() ) - .mix( ch_ks_stats.collect() ) - .mix( ch_distribution_correlations.collect() ) .mix( Channel.topic('all_eatlas_experiment_metadata').collect() ) .mix( Channel.topic('filtered_eatlas_experiment_metadata').collect() ) .set { ch_multiqc_files } From afc8109227e7e5cf9e894356a2bce1bb6acdfca6 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 3 Sep 2025 11:39:26 +0200 Subject: [PATCH 047/258] factorise code for computing gene statistics --- bin/compute_final_gene_statistics.py | 232 +++-------------- bin/compute_gene_statistics_per_platform.py | 261 +------------------- bin/stability_scorer.py | 178 +++++++++++++ 3 files changed, 219 insertions(+), 452 deletions(-) create mode 100644 bin/stability_scorer.py diff --git a/bin/compute_final_gene_statistics.py b/bin/compute_final_gene_statistics.py index 6eced895..fa2227b7 100755 --- a/bin/compute_final_gene_statistics.py +++ b/bin/compute_final_gene_statistics.py @@ -3,21 +3,17 @@ # Written by Olivier Coen. Released under the MIT license. import argparse -import sys import polars as pl from pathlib import Path -from dataclasses import dataclass, field import logging +from stability_scorer import StabilityScorer + logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # nb of top stable genes to select and to display at the end DEFAULT_NB_TOP_STABLE_GENES = 1000 -# we want to select samples that show a particularly low nb of genes -MIN_RATIO_GENE_COUNT_TO_MEAN = 0.75 # experimentally chosen -WEIGHT_RATIO_NB_NULLS = 1 - # outfile names TOP_STABLE_GENE_SUMMARY_OUTFILENAME = "top_stable_genes_summary.csv" @@ -46,22 +42,6 @@ NB_NULLS_COLNAME = "total_nb_nulls" NB_NULLS_VALID_SAMPLES_COLNAME = "nb_nulls_valid_samples" -RNASEQ_VARIATION_COEFFICIENT_COLNAME = "rnaseq_variation_coefficient" -RNASEQ_STANDARD_DEVIATION_COLNAME = "rnaseq_standard_deviation" -RNASEQ_MEAN_COLNAME = "rnaseq_mean" -RNASEQ_EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME = "rnaseq_expression_level_quantile_interval" -RNASEQ_EXPRESSION_LEVEL_STATUS_COLNAME = "rnaseq_expression_level_status" -RNASEQ_NB_NULLS_COLNAME = "rnaseq_total_nb_nulls" -RNASEQ_NB_NULLS_VALID_SAMPLES_COLNAME = "rnaseq_nb_nulls_valid_samples" - -MICROARRAY_VARIATION_COEFFICIENT_COLNAME = "microarray_variation_coefficient" -MICROARRAY_STANDARD_DEVIATION_COLNAME = "microarray_standard_deviation" -MICROARRAY_MEAN_COLNAME = "microarray_mean" -MICROARRAY_EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME = "microarray_expression_level_quantile_interval" -MICROARRAY_EXPRESSION_LEVEL_STATUS_COLNAME = "microarray_expression_level_status" -MICROARRAY_NB_NULLS_COLNAME = "microarray_total_nb_nulls" -MICROARRAY_NB_NULLS_VALID_SAMPLES_COLNAME = "microarray_nb_nulls_valid_samples" - STATISTICS_COLS = [ RANK_COLNAME, ENSEMBL_GENE_ID_COLNAME, @@ -71,26 +51,18 @@ MEAN_COLNAME, EXPRESSION_LEVEL_STATUS_COLNAME, NB_NULLS_COLNAME, - NB_NULLS_VALID_SAMPLES_COLNAME, - RNASEQ_VARIATION_COEFFICIENT_COLNAME, - RNASEQ_STANDARD_DEVIATION_COLNAME, - RNASEQ_MEAN_COLNAME, - RNASEQ_EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME, - RNASEQ_EXPRESSION_LEVEL_STATUS_COLNAME, - RNASEQ_NB_NULLS_COLNAME, - RNASEQ_NB_NULLS_VALID_SAMPLES_COLNAME, - MICROARRAY_VARIATION_COEFFICIENT_COLNAME, - MICROARRAY_STANDARD_DEVIATION_COLNAME, - MICROARRAY_MEAN_COLNAME, - MICROARRAY_EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME, - MICROARRAY_EXPRESSION_LEVEL_STATUS_COLNAME, - MICROARRAY_NB_NULLS_COLNAME, - MICROARRAY_NB_NULLS_VALID_SAMPLES_COLNAME, - GENE_NAME_COLNAME, - GENE_DESCRIPTION_COLNAME, - ORIGINAL_GENE_IDS_COLNAME, + NB_NULLS_VALID_SAMPLES_COLNAME ] +# making complete list of columns to export +final_cols = [] +for col in STATISTICS_COLS: + final_cols.append(col) + for platform in ["rnaseq", "microarray"]: + final_cols.append(f"{platform}_{col}") +# adding gene description columns +final_cols += [GENE_NAME_COLNAME, GENE_DESCRIPTION_COLNAME, ORIGINAL_GENE_IDS_COLNAME] + ALL_GENES_STATS_COLS = [ ENSEMBL_GENE_ID_COLNAME, STABILITY_SCORE_COLNAME, @@ -117,10 +89,18 @@ def parse_args(): description="Get statistics from count data for each gene" ) parser.add_argument( - "--counts", type=Path, dest="count_file", required=True, help="Count file" + "--counts", + type=Path, + dest="count_file", + required=True, + help="Count file" ) parser.add_argument( - "--stats", type=str, dest="platform_stat_files", required=True, help="Platform stat file" + "--stats", + type=str, + dest="platform_stat_files", + required=True, + help="Platform stat file" ) parser.add_argument( "--metadata", @@ -142,6 +122,7 @@ def parse_args(): return parser.parse_args() + def is_valid_lf(lf: pl.LazyFrame, file: Path) -> bool: """Check if a LazyFrame is valid. @@ -203,6 +184,14 @@ def cast_count_columns_to_float32(lf: pl.LazyFrame) -> pl.LazyFrame: ) +def join_data_on_gene_id( stat_lf: pl.LazyFrame, *lfs) -> pl.LazyFrame: + """Merge the statistics dataframe with the metadata dataframe and the mapping dataframe.""" + # we need to ensure that the index of stat_lf are strings + for lf in lfs: + stat_lf = stat_lf.join(lf, on=ENSEMBL_GENE_ID_COLNAME, how="left") + return stat_lf + + def get_counts(file: Path) -> pl.LazyFrame: # sorting dataframe (necessary to get consistent output) return pl.scan_parquet(file).sort(ENSEMBL_GENE_ID_COLNAME, descending=False) @@ -228,6 +217,7 @@ def get_mappings(mapping_files: list[Path]) -> pl.LazyFrame: .alias(ORIGINAL_GENE_IDS_COLNAME) ) + def get_platform_statistics(platform_stat_files: list[Path]) -> pl.LazyFrame: """Retrieve and concatenate metadata from a list of platform-specific statistics files.""" lf = pl.scan_csv(platform_stat_files[0]) @@ -238,19 +228,6 @@ def get_platform_statistics(platform_stat_files: list[Path]) -> pl.LazyFrame: return lf -def merge_data( - stat_lf: pl.LazyFrame, platform_stat_lf: pl.LazyFrame, metadata_lf: pl.LazyFrame, mapping_lf: pl.LazyFrame -) -> pl.LazyFrame: - """Merge the statistics dataframe with the metadata dataframe and the mapping dataframe.""" - # we need to ensure that the index of stat_lf are strings - return ( - stat_lf - .join(platform_stat_lf, on=ENSEMBL_GENE_ID_COLNAME, how="left") - .join(metadata_lf, on=ENSEMBL_GENE_ID_COLNAME, how="left") - .join(mapping_lf, on=ENSEMBL_GENE_ID_COLNAME, how="left") - ) - - def sort_dataframe(lf: pl.LazyFrame) -> pl.LazyFrame: return ( lf.sort(STABILITY_SCORE_COLNAME, descending=False, nulls_last=True) @@ -290,8 +267,9 @@ def get_top_stable_gene_summary( .replace_strict(mapping_dict) .alias(EXPRESSION_LEVEL_STATUS_COLNAME) ) + return lf.select( - [column for column in STATISTICS_COLS if column in lf.collect_schema().names()] + [column for column in final_cols if column in lf.collect_schema().names()] ) @@ -371,142 +349,6 @@ def export_data( logger.info("Done") -##################################################### -##################################################### -# CLASSES -##################################################### -##################################################### - - -@dataclass -class StabilityScorer: - count_lf: pl.LazyFrame - - gene_count_per_sample_df: pl.DataFrame = field(init=False) - stat_lf: pl.LazyFrame = field(init=False) - count_columns: list[str] = field(init=False) - samples_with_low_gene_count: list[str] = field(init=False) - - def __post_init__(self): - self.count_columns = get_count_columns(self.count_lf) - self.gene_count_per_sample_df = self.get_gene_counts_per_sample() - self.samples_with_low_gene_count = self.get_samples_with_low_gene_count() - - def get_valid_counts(self) -> pl.LazyFrame: - return self.count_lf.select(pl.exclude(ENSEMBL_GENE_ID_COLNAME)) - - def get_gene_counts_per_sample(self) -> pl.DataFrame: - """ - Get the number of non-null values per sample. - :return: - A polars dataframe containing 2 columns: - - sample: name of the sample - - nb_not_nulls: number of non-null values - """ - return ( - self.count_lf.select(pl.exclude(ENSEMBL_GENE_ID_COLNAME)) - .count() - .collect() - .transpose( - include_header=True, header_name="sample", column_names=["count"] - ) - ) - - def get_samples_with_low_gene_count(self) -> list[str]: - mean_gene_count = self.gene_count_per_sample_df[GENE_COUNT_COLNAME].mean() - return ( - self.gene_count_per_sample_df.filter( - (pl.col(GENE_COUNT_COLNAME) / mean_gene_count) - < MIN_RATIO_GENE_COUNT_TO_MEAN - ) - .select(SAMPLE_COLNAME) - .to_series() - .to_list() - ) - - def get_main_statistics(self) -> pl.LazyFrame: - """ - Compute count descriptive statistics for each gene in the count dataframe. - """ - logger.info("Getting descriptive statistics") - # computing main stats - augmented_count_lf = self.count_lf.with_columns( - mean=pl.concat_list(self.count_columns).list.drop_nulls().list.mean(), - std=pl.concat_list(self.count_columns).list.drop_nulls().list.std(), - ) - return augmented_count_lf.select( - pl.col(ENSEMBL_GENE_ID_COLNAME), - pl.col("mean").alias(MEAN_COLNAME), - pl.col("std").alias(STANDARD_DEVIATION_COLNAME), - (pl.col("std") / pl.col("mean")).alias(VARIATION_COEFFICIENT_COLNAME), - ) - - def compute_nb_null_values(self): - # the samples showing a low gene count will not be taken into account for the zero count penalty - cols_to_exclude = [ENSEMBL_GENE_ID_COLNAME] + self.samples_with_low_gene_count - total_nb_nulls = ( - self.count_lf.select(pl.exclude(ENSEMBL_GENE_ID_COLNAME).is_null()) - .collect() - .sum_horizontal() - ) - nb_nulls_valid_samples = ( - self.count_lf.select(pl.exclude(cols_to_exclude).is_null()) - .collect() - .sum_horizontal() - ) - self.stat_lf = self.stat_lf.with_columns( - total_nb_nulls.alias(NB_NULLS_COLNAME), - nb_nulls_valid_samples.alias(NB_NULLS_VALID_SAMPLES_COLNAME), - ) - - def get_quantile_intervals(self): - """ - Compute the quantile intervals for the mean expression levels of each gene in the dataframe. - - The function assigns to each gene a quantile interval of its mean cpm compared to all genes. - """ - logger.info("Getting cpm quantiles") - self.stat_lf = self.stat_lf.with_columns( - (pl.col(MEAN_COLNAME).rank() / pl.col(MEAN_COLNAME).count() * NB_QUANTILES) - .floor() - .cast(pl.Int8) - # we want the only value = NB_QUANTILES to be NB_QUANTILES - 1 - # because the last quantile interval is [NB_QUANTILES - 1, NB_QUANTILES] - .replace({NB_QUANTILES: NB_QUANTILES - 1}) - .alias(EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME) - ) - - def compute_stability_score(self): - logger.info("Computing stability score") - nb_valid_samples = self.gene_count_per_sample_df.select(pl.len()).item() - len( - self.samples_with_low_gene_count - ) - ratio_nb_nulls = ( - self.stat_lf.select( - pl.col(NB_NULLS_VALID_SAMPLES_COLNAME) / nb_valid_samples - ) - .collect() - .to_series() - ) - expr = ( - pl.col(STANDARD_DEVIATION_COLNAME) + ratio_nb_nulls * WEIGHT_RATIO_NB_NULLS - ) - self.stat_lf = self.stat_lf.with_columns(expr.alias(STABILITY_SCORE_COLNAME)) - - def compute_statistics_and_score(self) -> pl.LazyFrame: - logger.info("Computing statistics and stability score") - # getting expression statistics - self.stat_lf = self.get_main_statistics() - # adding column for nb of null values for each gene - self.compute_nb_null_values() - # computing stability score - self.compute_stability_score() - # getting quantile intervals - self.get_quantile_intervals() - - return self.stat_lf - - ##################################################### ##################################################### # MAIN @@ -519,7 +361,7 @@ def main(): metadata_files = [Path(file) for file in args.metadata_files.split(" ")] mapping_files = [Path(file) for file in args.mapping_files.split(" ")] platform_stat_files = [Path(file) for file in args.platform_stat_files.split(" ")] - print(platform_stat_files) + count_lf = get_counts(args.count_file) # getting metadata and mappings @@ -532,7 +374,7 @@ def main(): stat_lf = stability_scorer.compute_statistics_and_score() # add gene name, description and original gene IDs - stat_lf = merge_data(stat_lf, platform_stat_df, metadata_lf, mapping_lf) + stat_lf = join_data_on_gene_id(stat_lf, platform_stat_df, metadata_lf, mapping_lf) # sort genes according to the metrics present in the dataframe stat_lf = sort_dataframe(stat_lf) @@ -553,8 +395,6 @@ def main(): count_lf, top_stable_genes_summary_lf ) - print(top_stable_genes_counts_df) - # exporting computed data export_data( top_stable_genes_summary_lf, diff --git a/bin/compute_gene_statistics_per_platform.py b/bin/compute_gene_statistics_per_platform.py index 9330e163..a1f3ef3b 100755 --- a/bin/compute_gene_statistics_per_platform.py +++ b/bin/compute_gene_statistics_per_platform.py @@ -3,45 +3,20 @@ # Written by Olivier Coen. Released under the MIT license. import argparse -import sys import polars as pl from pathlib import Path -from dataclasses import dataclass, field import logging +from stability_scorer import StabilityScorer + logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -# nb of top stable genes to select and to display at the end -DEFAULT_NB_TOP_STABLE_GENES = 1000 -# we want to select samples that show a particularly low nb of genes -MIN_RATIO_GENE_COUNT_TO_MEAN = 0.75 # experimentally chosen -WEIGHT_RATIO_NB_NULLS = 1 - +ENSEMBL_GENE_ID_COLNAME = "ensembl_gene_id" # outfile names ALL_GENES_RESULT_OUTFILENAME = "stats_all_genes.csv" -# column names -ENSEMBL_GENE_ID_COLNAME = "ensembl_gene_id" -VARIATION_COEFFICIENT_COLNAME = "variation_coefficient" -STANDARD_DEVIATION_COLNAME = "standard_deviation" -MEAN_COLNAME = "mean" -EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME = "expression_level_quantile_interval" -EXPRESSION_LEVEL_STATUS_COLNAME = "expression_level_status" -GENE_COUNT_COLNAME = "count" -SAMPLE_COLNAME = "sample" -NB_NULLS_COLNAME = "total_nb_nulls" -NB_NULLS_VALID_SAMPLES_COLNAME = "nb_nulls_valid_samples" -NB_ZEROS_COLNAME = "nb_zeros" -STABILITY_SCORE_COLNAME = "stability_score" - - -# quantile intervals -NB_QUANTILES = 100 - -NB_TOP_GENES_TO_SHOW_IN_LOG_COUNTS = 100 - ##################################################### ##################################################### @@ -52,7 +27,7 @@ def parse_args(): parser = argparse.ArgumentParser( - description="Get base statistics from count data for each gene. Excludes aberrant datasets." + description="Get base statistics from count data for each gene" ) parser.add_argument( "--counts", type=Path, dest="count_file", required=True, help="Count file" @@ -63,83 +38,11 @@ def parse_args(): return parser.parse_args() -def is_valid_lf(lf: pl.LazyFrame, file: Path) -> bool: - """Check if a LazyFrame is valid. - - A LazyFrame is considered valid if it contains at least one row. - """ - try: - return not lf.limit(1).collect().is_empty() - except FileNotFoundError: - # strangely enough we get this error for some files existing but empty - logger.error(f"Could not find file {str(file)}") - return False - except pl.exceptions.NoDataError as err: - logger.error(f"File {str(file)} is empty: {err}") - return False - - -def get_valid_lazy_lfs(files: list[Path]) -> list[pl.LazyFrame]: - """Get a list of valid LazyFrames from a list of files. - - A LazyFrame is considered valid if it contains at least one row. - """ - lf_dict = {file: pl.scan_csv(file) for file in files} - return [lf for file, lf in lf_dict.items() if is_valid_lf(lf, file)] - - -def cast_cols_to_string(lf: pl.LazyFrame) -> pl.LazyFrame: - return lf.select( - [pl.col(column).cast(pl.String) for column in lf.collect_schema().names()] - ) - - -def concat_cast_to_string_and_drop_duplicates(files: list[Path]) -> pl.LazyFrame: - """Concatenate LazyFrames, cast all columns to String, and drop duplicates. - - The first step is to concatenate the LazyFrames. Then, the dataframe is cast - to String to ensure that all columns have the same data type. Finally, duplicate - rows are dropped. - """ - lfs = get_valid_lazy_lfs(files) - lfs = [cast_cols_to_string(lf) for lf in lfs] - concat_lf = pl.concat(lfs) - # dropping duplicates - # casting all columns to String - return concat_lf.unique() - - -def get_count_columns(lf: pl.LazyFrame) -> list[str]: - """Get all column names except the ENSEMBL_GENE_ID_COLNAME column. - - The ENSEMBL_GENE_ID_COLNAME column contains only gene IDs. - """ - return lf.select(pl.exclude(ENSEMBL_GENE_ID_COLNAME)).collect_schema().names() - - -def cast_count_columns_to_float32(lf: pl.LazyFrame) -> pl.LazyFrame: - return lf.select( - [pl.col(ENSEMBL_GENE_ID_COLNAME)] - + [pl.col(column).cast(pl.Float32) for column in get_count_columns(lf)] - ) - - def get_counts(file: Path) -> pl.LazyFrame: # sorting dataframe (necessary to get consistent output) return pl.scan_parquet(file).sort(ENSEMBL_GENE_ID_COLNAME, descending=False) -def merge_data( - stat_lf: pl.LazyFrame, metadata_lf: pl.LazyFrame, mapping_lf: pl.LazyFrame -) -> pl.LazyFrame: - """Merge the statistics dataframe with the metadata dataframe and the mapping dataframe.""" - # we need to ensure that the index of stat_lf are strings - return stat_lf.join(metadata_lf, on=ENSEMBL_GENE_ID_COLNAME, how="left").join( - mapping_lf, on=ENSEMBL_GENE_ID_COLNAME, how="left" - ) - - - def export_data( stat_lf: pl.LazyFrame, platform: str ): @@ -152,160 +55,6 @@ def export_data( logger.info("Done") -##################################################### -##################################################### -# CLASSES -##################################################### -##################################################### - - -@dataclass -class StabilityScorer: - - count_lf: pl.LazyFrame - platform: str - - mean_colname: str = field(init=False) - std_colname: str = field(init=False) - var_coeff_colname: str = field(init=False) - nb_nulls_colname: str = field(init=False) - nb_nulls_valid_samples_colname: str = field(init=False) - - gene_count_per_sample_df: pl.DataFrame = field(init=False) - stat_lf: pl.LazyFrame = field(init=False) - count_columns: list[str] = field(init=False) - samples_with_low_gene_count: list[str] = field(init=False) - exp_level_quantile_interval_colname: str = field(init=False) - stability_score_colname: str = field(init=False) - - def __post_init__(self): - self.count_columns = get_count_columns(self.count_lf) - self.gene_count_per_sample_df = self.get_gene_counts_per_sample() - self.samples_with_low_gene_count = self.get_samples_with_low_gene_count() - - self.mean_colname = f"{self.platform}_{MEAN_COLNAME}" - self.std_colname = f"{self.platform}_{STANDARD_DEVIATION_COLNAME}" - self.var_coeff_colname = f"{self.platform}_{VARIATION_COEFFICIENT_COLNAME}" - self.nb_nulls_colname = f"{self.platform}_{NB_NULLS_COLNAME}" - self.nb_nulls_valid_samples_colname = f"{self.platform}_{NB_NULLS_VALID_SAMPLES_COLNAME}" - self.exp_level_quantile_interval_colname = f"{self.platform}_{EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME}" - self.stability_score_colname = f"{self.platform}_{STABILITY_SCORE_COLNAME}" - - def get_valid_counts(self) -> pl.LazyFrame: - return self.count_lf.select(pl.exclude(ENSEMBL_GENE_ID_COLNAME)) - - def get_gene_counts_per_sample(self) -> pl.DataFrame: - """ - Get the number of non-null values per sample. - :return: - A polars dataframe containing 2 columns: - - sample: name of the sample - - nb_not_nulls: number of non-null values - """ - return ( - self.count_lf.select(pl.exclude(ENSEMBL_GENE_ID_COLNAME)) - .count() - .collect() - .transpose( - include_header=True, header_name="sample", column_names=["count"] - ) - ) - - def get_samples_with_low_gene_count(self) -> list[str]: - mean_gene_count = self.gene_count_per_sample_df[GENE_COUNT_COLNAME].mean() - return ( - self.gene_count_per_sample_df.filter( - (pl.col(GENE_COUNT_COLNAME) / mean_gene_count) - < MIN_RATIO_GENE_COUNT_TO_MEAN - ) - .select(SAMPLE_COLNAME) - .to_series() - .to_list() - ) - - def get_main_statistics(self) -> pl.LazyFrame: - """ - Compute count descriptive statistics for each gene in the count dataframe. - """ - logger.info("Getting descriptive statistics") - # computing main stats - augmented_count_lf = self.count_lf.with_columns( - mean=pl.concat_list(self.count_columns).list.drop_nulls().list.mean(), - std=pl.concat_list(self.count_columns).list.drop_nulls().list.std(), - ) - return augmented_count_lf.select( - pl.col(ENSEMBL_GENE_ID_COLNAME), - pl.col("mean").alias(self.mean_colname), - pl.col("std").alias(self.std_colname), - (pl.col("std") / pl.col("mean")).alias(self.var_coeff_colname), - ) - - def compute_nb_null_values(self): - # the samples showing a low gene count will not be taken into account for the zero count penalty - cols_to_exclude = [ENSEMBL_GENE_ID_COLNAME] + self.samples_with_low_gene_count - total_nb_nulls = ( - self.count_lf.select(pl.exclude(ENSEMBL_GENE_ID_COLNAME).is_null()) - .collect() - .sum_horizontal() - ) - nb_nulls_valid_samples = ( - self.count_lf.select(pl.exclude(cols_to_exclude).is_null()) - .collect() - .sum_horizontal() - ) - self.stat_lf = self.stat_lf.with_columns( - total_nb_nulls.alias(self.nb_nulls_colname), - nb_nulls_valid_samples.alias(self.nb_nulls_valid_samples_colname), - ) - - def get_quantile_intervals(self): - """ - Compute the quantile intervals for the mean expression levels of each gene in the dataframe. - - The function assigns to each gene a quantile interval of its mean cpm compared to all genes. - """ - logger.info("Getting cpm quantiles") - self.stat_lf = self.stat_lf.with_columns( - (pl.col(self.mean_colname).rank() / pl.col(self.mean_colname).count() * NB_QUANTILES) - .floor() - .cast(pl.Int8) - # we want the only value = NB_QUANTILES to be NB_QUANTILES - 1 - # because the last quantile interval is [NB_QUANTILES - 1, NB_QUANTILES] - .replace({NB_QUANTILES: NB_QUANTILES - 1}) - .alias(self.exp_level_quantile_interval_colname) - ) - - def compute_stability_score(self): - logger.info("Computing stability score") - nb_valid_samples = self.gene_count_per_sample_df.select(pl.len()).item() - len( - self.samples_with_low_gene_count - ) - ratio_nb_nulls = ( - self.stat_lf.select( - pl.col(self.nb_nulls_valid_samples_colname) / nb_valid_samples - ) - .collect() - .to_series() - ) - expr = ( - pl.col(self.std_colname) + ratio_nb_nulls * WEIGHT_RATIO_NB_NULLS - ) - self.stat_lf = self.stat_lf.with_columns(expr.alias(self.stability_score_colname)) - - def compute_statistics_and_score(self) -> pl.LazyFrame: - logger.info("Computing statistics and stability score") - # getting expression statistics - self.stat_lf = self.get_main_statistics() - # adding column for nb of null values for each gene - self.compute_nb_null_values() - # computing stability score - self.compute_stability_score() - # getting quantile intervals - self.get_quantile_intervals() - - return self.stat_lf - - ##################################################### ##################################################### # MAIN @@ -324,7 +73,7 @@ def main(): stat_lf = stability_scorer.compute_statistics_and_score() # exporting computed data - export_data( stat_lf, args.platform ) + export_data(stat_lf, args.platform ) if __name__ == "__main__": diff --git a/bin/stability_scorer.py b/bin/stability_scorer.py new file mode 100644 index 00000000..ac59c523 --- /dev/null +++ b/bin/stability_scorer.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 + +# Written by Olivier Coen. Released under the MIT license. + +from typing import ClassVar +import polars as pl +from dataclasses import dataclass, field +import logging + +logger = logging.getLogger(__name__) + +ENSEMBL_GENE_ID_COLNAME = "ensembl_gene_id" +GENE_COUNT_COLNAME = "count" +SAMPLE_COLNAME = "sample" + + +def get_count_columns(lf: pl.LazyFrame) -> list[str]: + """Get all column names except the ENSEMBL_GENE_ID_COLNAME column. + + The ENSEMBL_GENE_ID_COLNAME column contains only gene IDs. + """ + return lf.select(pl.exclude(ENSEMBL_GENE_ID_COLNAME)).collect_schema().names() + + + +@dataclass +class StabilityScorer: + + STAT_COLS: ClassVar[dict] = dict( + VAR_COEFF="variation_coefficient", + STD="standard_deviation", + MEAN="mean", + EXPRESSION_LEVEL_QUANTILE_INTERVAL="expression_level_quantile_interval", + EXPRESSION_LEVEL_STATUS="expression_level_status", + NB_NULLS="total_nb_nulls", + NB_NULLS_VALID_SAMPLES="nb_nulls_valid_samples", + NB_ZEROS="nb_zeros", + STABILITY_SCORE="stability_score" + ) + + # we want to select samples that show a particularly low nb of genes + MIN_RATIO_GENE_COUNT_TO_MEAN: ClassVar[float] = 0.75 # experimentally chosen + WEIGHT_RATIO_NB_NULLS: ClassVar[float] = 1 + # quantile intervals + NB_QUANTILES: ClassVar[int] = 100 + + count_lf: pl.LazyFrame + platform: str | None = field(default=None) + + gene_count_per_sample_df: pl.DataFrame = field(init=False) + stat_lf: pl.LazyFrame = field(init=False) + count_columns: list[str] = field(init=False) + samples_with_low_gene_count: list[str] = field(init=False) + + def __post_init__(self): + self.count_columns = get_count_columns(self.count_lf) + self.gene_count_per_sample_df = self.get_gene_counts_per_sample() + self.samples_with_low_gene_count = self.get_samples_with_low_gene_count() + + + def get_colname(self, key: str) -> str: + return f"{self.platform}_{self.STAT_COLS[key]}" if self.platform else self.STAT_COLS[key] + + def get_valid_counts(self) -> pl.LazyFrame: + return self.count_lf.select(pl.exclude(ENSEMBL_GENE_ID_COLNAME)) + + def get_gene_counts_per_sample(self) -> pl.DataFrame: + """ + Get the number of non-null values per sample. + :return: + A polars dataframe containing 2 columns: + - sample: name of the sample + - nb_not_nulls: number of non-null values + """ + return ( + self.count_lf.select(pl.exclude(ENSEMBL_GENE_ID_COLNAME)) + .count() + .collect() + .transpose( + include_header=True, + header_name="sample", + column_names=["count"] + ) + ) + + def get_samples_with_low_gene_count(self) -> list[str]: + mean_gene_count = self.gene_count_per_sample_df[GENE_COUNT_COLNAME].mean() + return ( + self.gene_count_per_sample_df.filter( + (pl.col(GENE_COUNT_COLNAME) / mean_gene_count) + < self.MIN_RATIO_GENE_COUNT_TO_MEAN + ) + .select(SAMPLE_COLNAME) + .to_series() + .to_list() + ) + + def get_main_statistics(self) -> pl.LazyFrame: + """ + Compute count descriptive statistics for each gene in the count dataframe. + """ + logger.info("Getting descriptive statistics") + # computing main stats + augmented_count_lf = self.count_lf.with_columns( + mean=pl.concat_list(self.count_columns).list.drop_nulls().list.mean(), + std=pl.concat_list(self.count_columns).list.drop_nulls().list.std(), + ) + return augmented_count_lf.select( + pl.col(ENSEMBL_GENE_ID_COLNAME), + pl.col("mean").alias(self.get_colname("MEAN")), + pl.col("std").alias(self.get_colname("STD")), + (pl.col("std") / pl.col("mean")).alias(self.get_colname("VAR_COEFF")), + ) + + def compute_nb_null_values(self): + # the samples showing a low gene count will not be taken into account for the zero count penalty + cols_to_exclude = [ENSEMBL_GENE_ID_COLNAME] + self.samples_with_low_gene_count + total_nb_nulls = ( + self.count_lf.select(pl.exclude(ENSEMBL_GENE_ID_COLNAME).is_null()) + .collect() + .sum_horizontal() + ) + nb_nulls_valid_samples = ( + self.count_lf.select(pl.exclude(cols_to_exclude).is_null()) + .collect() + .sum_horizontal() + ) + self.stat_lf = self.stat_lf.with_columns( + total_nb_nulls.alias(self.get_colname("NB_NULLS")), + nb_nulls_valid_samples.alias(self.get_colname("NB_NULLS_VALID_SAMPLES")), + ) + + def get_quantile_intervals(self): + """ + Compute the quantile intervals for the mean expression levels of each gene in the dataframe. + + The function assigns to each gene a quantile interval of its mean cpm compared to all genes. + """ + logger.info("Getting cpm quantiles") + self.stat_lf = self.stat_lf.with_columns( + (pl.col(self.get_colname("MEAN")).rank() / pl.col(self.get_colname("MEAN")).count() * self.NB_QUANTILES) + .floor() + .cast(pl.Int8) + # we want the only value = NB_QUANTILES to be NB_QUANTILES - 1 + # because the last quantile interval is [NB_QUANTILES - 1, NB_QUANTILES] + .replace({self.NB_QUANTILES: self.NB_QUANTILES - 1}) + .alias(self.get_colname("EXPRESSION_LEVEL_QUANTILE_INTERVAL")) + ) + + def compute_stability_score(self): + logger.info("Computing stability score") + nb_valid_samples = self.gene_count_per_sample_df.select(pl.len()).item() - len( + self.samples_with_low_gene_count + ) + ratio_nb_nulls = ( + self.stat_lf.select( + pl.col(self.get_colname("NB_NULLS_VALID_SAMPLES")) / nb_valid_samples + ) + .collect() + .to_series() + ) + expr = ( + pl.col(self.get_colname("STD")) + ratio_nb_nulls * self.WEIGHT_RATIO_NB_NULLS + ) + self.stat_lf = self.stat_lf.with_columns(expr.alias(self.get_colname("STABILITY_SCORE"))) + + def compute_statistics_and_score(self) -> pl.LazyFrame: + logger.info("Computing statistics and stability score") + # getting expression statistics + self.stat_lf = self.get_main_statistics() + # adding column for nb of null values for each gene + self.compute_nb_null_values() + # computing stability score + self.compute_stability_score() + # getting quantile intervals + self.get_quantile_intervals() + + return self.stat_lf From fa9c75334660bc1cc9b59d8e40821a62046dfc75 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 3 Sep 2025 13:20:45 +0200 Subject: [PATCH 048/258] compute median and median absolute deviation and display in MultiQC report --- assets/multiqc_config.yml | 75 ++++++++++++++++++---------- bin/compute_final_gene_statistics.py | 4 ++ bin/stability_scorer.py | 66 ++++++++++++++++++++++-- 3 files changed, 114 insertions(+), 31 deletions(-) diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index 6d87c57d..872917f4 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -35,8 +35,8 @@ custom_data: sort_rows: false description: | Expression descriptive statistics of all genes, ranked by stability. - Expression were normalised and cpm (counts per million) were used for all calculations. - Genes are sorted by M-measure - from the most stable to the least stable. + Expression was first normalised dataset per dataset, then log2(cpm + 1) transformed (cpm :counts per million) and finally fitted to [0, 1] using quantile normalisation. + Genes are sorted by stability score - from the most stable to the least stable. plot_type: "table" pconfig: col1_header: "Rank" @@ -51,25 +51,32 @@ custom_data: standard_deviation: title: "Std" description: | - Standard deviation of the expression across samples. - For each sample, expression was first computed as log2(cpm + 1), then fitted to [0, 1] using scikit-learn's QuantileTransformer. + Standard deviation of the expression across all samples. format: "{:,.4f}" variation_coefficient: title: "Var coeff" description: | - Variation coefficient: std(expression) / mean(expression). - For each sample, expression was first computed as log2(cpm + 1), then fitted to [0, 1] using scikit-learn's QuantileTransformer. + Variation coefficient ( std(expression) / mean(expression) ) across all samples. format: "{:,.4f}" mean: title: "Average" description: | - Average expression across samples. - For each sample, expression was first computed as log2(cpm + 1), then fitted to [0, 1] using scikit-learn's QuantileTransformer. + Average expression across all samples. + format: "{:,.4f}" + median: + title: "Median" + description: | + Median expression across all samples. + format: "{:,.4f}" + median_absolute_deviation: + title: "Mad" + description: | + Median absolute deviation of the expression across all samples. format: "{:,.4f}" expression_level_status: title: "Expression level" description: | - Indication about the average gene expression level compared to the whole pool of genes. + Indication about the average gene expression level across all samples (compared to the whole pool of genes). Expression in [0, 0.05]: Very low expression. Expression in [0.05, 0.1]: Low expression. Expression in [0.1, 0.9]: Medium range. @@ -101,25 +108,32 @@ custom_data: rnaseq_standard_deviation: title: "Std [RNA-seq only]" description: | - Standard deviation of the expression across samples. - For each sample, expression was first computed as log2(cpm + 1), then fitted to [0, 1] using scikit-learn's QuantileTransformer. + Standard deviation of the expression across RNA-Seq samples. format: "{:,.4f}" rnaseq_variation_coefficient: title: "Var coeff [RNA-seq only]" description: | - Variation coefficient: std(expression) / mean(expression). - For each sample, expression was first computed as log2(cpm + 1), then fitted to [0, 1] using scikit-learn's QuantileTransformer. + Variation coefficient ( std(expression) / mean(expression) ) across RNA-Seq samples. format: "{:,.4f}" rnaseq_mean: title: "Average [RNA-seq only]" description: | Average expression across samples. - For each sample, expression was first computed as log2(cpm + 1), then fitted to [0, 1] using scikit-learn's QuantileTransformer. + format: "{:,.4f}" + rnaseq_median: + title: "Median [RNA-seq only]" + description: | + Median expression across RNA-Seq samples. + format: "{:,.4f}" + rnaseq_median_absolute_deviation: + title: "Mad [RNA-seq only]" + description: | + Median absolute deviation of the expression across RNA-Seq samples. format: "{:,.4f}" rnaseq_expression_level_status: title: "Expression level [RNA-seq only]" description: | - Indication about the average gene expression level compared to the whole pool of genes. + Indication about the average gene expression level across RNA-Seq samples (compared to the whole pool of genes). Expression in [0, 0.05]: Very low expression. Expression in [0.05, 0.1]: Low expression. Expression in [0.1, 0.9]: Medium range. @@ -139,25 +153,32 @@ custom_data: microarray_standard_deviation: title: "Std [Microarray only]" description: | - Standard deviation of the expression across samples. - For each sample, expression was first computed as log2(cpm + 1), then fitted to [0, 1] using scikit-learn's QuantileTransformer. + Standard deviation of the expression across Microarray samples. format: "{:,.4f}" microarray_variation_coefficient: title: "Var coeff [Microarray only]" description: | - Variation coefficient: std(expression) / mean(expression). - For each sample, expression was first computed as log2(cpm + 1), then fitted to [0, 1] using scikit-learn's QuantileTransformer. + Variation coefficient ( std(expression) / mean(expression) ) across Microarray samples. format: "{:,.4f}" microarray_mean: title: "Average [Microarray only]" description: | - Average expression across samples. - For each sample, expression was first computed as log2(cpm + 1), then fitted to [0, 1] using scikit-learn's QuantileTransformer. + Average expression across Microarray samples. + format: "{:,.4f}" + microarray_median: + title: "Median [Microarray only]" + description: | + Median expression across Microarray samples. + format: "{:,.4f}" + microarray_median_absolute_deviation: + title: "Mad [Microarray only]" + description: | + Median absolute deviation of the expression across Microarray samples. format: "{:,.4f}" microarray_expression_level_status: title: "Expression level [Microarray only]" description: | - Indication about the average gene expression level compared to the whole pool of genes. + Indication about the average gene expression level across Microarray samples (compared to the whole pool of genes). Expression in [0, 0.05]: Very low expression. Expression in [0.05, 0.1]: Low expression. Expression in [0.1, 0.9]: Medium range. @@ -185,19 +206,19 @@ custom_data: rnaseq_total_nb_nulls: title: "Nb nulls [RNA-seq only]" description: | - Number of samples in which the gene is not represented. + Number of RNA-Seq samples in which the gene is not represented. rnaseq_nb_nulls_valid_samples: title: "Nb nulls (valid samples) [RNA-seq only]" description: | - Number of samples in which the gene is not represented, excluding samples with particularly low overall gene count. + Number of RNA-Seq samples in which the gene is not represented, excluding samples with particularly low overall gene count. microarray_total_nb_nulls: title: "Nb nulls [Microarray only]" description: | - Number of samples in which the gene is not represented. + Number of Microarray samples in which the gene is not represented. microarray_nb_nulls_valid_samples: title: "Nb nulls (valid samples) [Microarray only]" description: | - Number of samples in which the gene is not represented, excluding samples with particularly low overall gene count. + Number of Microarray samples in which the gene is not represented, excluding samples with particularly low overall gene count. expression_distributions_top_stable_genes: section_name: "Expression distribution of the top stable genes (ranked by stability)" @@ -206,7 +227,7 @@ custom_data: sort_samples: false description: | Distribution of gene expression across samples for the most stable genes. - For each sample, expression was first computed as log2(cpm + 1), then fitted to [0, 1] using scikit-learn's QuantileTransformer. + Genes are ranked from the most stable to the least stable. plot_type: "boxplot" #xlab: Expression diff --git a/bin/compute_final_gene_statistics.py b/bin/compute_final_gene_statistics.py index fa2227b7..49e782bc 100755 --- a/bin/compute_final_gene_statistics.py +++ b/bin/compute_final_gene_statistics.py @@ -37,6 +37,8 @@ VARIATION_COEFFICIENT_COLNAME = "variation_coefficient" STANDARD_DEVIATION_COLNAME = "standard_deviation" MEAN_COLNAME = "mean" +MEDIAN_COLNAME = "median" +MAD_COLNAME = "median_absolute_deviation" EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME = "expression_level_quantile_interval" EXPRESSION_LEVEL_STATUS_COLNAME = "expression_level_status" NB_NULLS_COLNAME = "total_nb_nulls" @@ -49,6 +51,8 @@ STANDARD_DEVIATION_COLNAME, VARIATION_COEFFICIENT_COLNAME, MEAN_COLNAME, + MEDIAN_COLNAME, + MAD_COLNAME, EXPRESSION_LEVEL_STATUS_COLNAME, NB_NULLS_COLNAME, NB_NULLS_VALID_SAMPLES_COLNAME diff --git a/bin/stability_scorer.py b/bin/stability_scorer.py index ac59c523..2021e9fa 100644 --- a/bin/stability_scorer.py +++ b/bin/stability_scorer.py @@ -22,6 +22,47 @@ def get_count_columns(lf: pl.LazyFrame) -> list[str]: return lf.select(pl.exclude(ENSEMBL_GENE_ID_COLNAME)).collect_schema().names() +############################################################################ +# POLARS EXTENSIONS +############################################################################ + +@pl.api.register_expr_namespace("row") +class StatsExtension: + def __init__(self, expr: pl.Expr): + self._expr = expr + + def not_null_values(self): + return ( + self._expr + .list + .drop_nulls() + .list + ) + + def mean(self) -> pl.Expr: + """Mean over non nulls values in row""" + return self.not_null_values().mean() + + + def std(self) -> pl.Expr: + """Std over non nulls values in row""" + return self.not_null_values().std() + + def median(self) -> pl.Expr: + """Median over non nulls values in row""" + return self.not_null_values().median() + + def mad(self) -> pl.Expr: + """Median Absolute Deviation over non nulls values in row""" + return ( + self.not_null_values() + .eval( + (pl.element() - pl.element().median()).abs().median() + ) # returns a list with one element + .list.first() + ) + + @dataclass class StabilityScorer: @@ -30,6 +71,8 @@ class StabilityScorer: VAR_COEFF="variation_coefficient", STD="standard_deviation", MEAN="mean", + MEDIAN="median", + MAD="median_absolute_deviation", EXPRESSION_LEVEL_QUANTILE_INTERVAL="expression_level_quantile_interval", EXPRESSION_LEVEL_STATUS="expression_level_status", NB_NULLS="total_nb_nulls", @@ -102,13 +145,18 @@ def get_main_statistics(self) -> pl.LazyFrame: logger.info("Getting descriptive statistics") # computing main stats augmented_count_lf = self.count_lf.with_columns( - mean=pl.concat_list(self.count_columns).list.drop_nulls().list.mean(), - std=pl.concat_list(self.count_columns).list.drop_nulls().list.std(), + mean=pl.concat_list(self.count_columns).row.mean(), + std=pl.concat_list(self.count_columns).row.std(), + median=pl.concat_list(self.count_columns).row.median(), + mad=pl.concat_list(self.count_columns).row.mad() ) + return augmented_count_lf.select( pl.col(ENSEMBL_GENE_ID_COLNAME), pl.col("mean").alias(self.get_colname("MEAN")), pl.col("std").alias(self.get_colname("STD")), + pl.col("median").alias(self.get_colname("MEDIAN")), + pl.col("mad").alias(self.get_colname("MAD")), (pl.col("std") / pl.col("mean")).alias(self.get_colname("VAR_COEFF")), ) @@ -149,9 +197,11 @@ def get_quantile_intervals(self): def compute_stability_score(self): logger.info("Computing stability score") + # get nb of valid samples (those not showing too many null values) nb_valid_samples = self.gene_count_per_sample_df.select(pl.len()).item() - len( self.samples_with_low_gene_count ) + # for each gene, get ratio of nb of null values among all valid samples ratio_nb_nulls = ( self.stat_lf.select( pl.col(self.get_colname("NB_NULLS_VALID_SAMPLES")) / nb_valid_samples @@ -159,10 +209,18 @@ def compute_stability_score(self): .collect() .to_series() ) + ################################################## + # FORMULA FOR STABILITY SCORE + ################################################## expr = ( - pl.col(self.get_colname("STD")) + ratio_nb_nulls * self.WEIGHT_RATIO_NB_NULLS + pl.col(self.get_colname("VAR_COEFF")) + ratio_nb_nulls * self.WEIGHT_RATIO_NB_NULLS + ) + ################################################## + ################################################## + # add stability score column + self.stat_lf = self.stat_lf.with_columns( + expr.alias(self.get_colname("STABILITY_SCORE")) ) - self.stat_lf = self.stat_lf.with_columns(expr.alias(self.get_colname("STABILITY_SCORE"))) def compute_statistics_and_score(self) -> pl.LazyFrame: logger.info("Computing statistics and stability score") From a3aa5d0a6732f8d3fad1156a6ef9dcc0c38aa1d1 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Thu, 4 Sep 2025 12:07:10 +0200 Subject: [PATCH 049/258] script to get accessions from GEO datasets --- bin/download_geo_data.R | 212 ++++++++ bin/get_eatlas_accessions.py | 163 +----- bin/get_geo_dataset_accessions.py | 501 ++++++++++++++++++ bin/natural_language_utils.py | 140 +++++ bin/stability_scorer.py | 0 .../expressionatlas/getaccessions/main.nf | 2 +- 6 files changed, 867 insertions(+), 151 deletions(-) create mode 100755 bin/download_geo_data.R create mode 100755 bin/get_geo_dataset_accessions.py create mode 100755 bin/natural_language_utils.py mode change 100644 => 100755 bin/stability_scorer.py diff --git a/bin/download_geo_data.R b/bin/download_geo_data.R new file mode 100755 index 00000000..79444a61 --- /dev/null +++ b/bin/download_geo_data.R @@ -0,0 +1,212 @@ +#!/usr/bin/env Rscript + +# Written by Olivier Coen. Released under the MIT license. + +suppressPackageStartupMessages(library("GEOquery")) +library(GEOquery) +library(optparse) +library(biomaRt) + + +##################################################### +##################################################### +# FUNCTIONS +##################################################### +##################################################### + +get_args <- function() { + option_list <- list( + make_option("--accession", type = "character", help = "Accession number of GEO dataset. Example: GSE56413"), + make_option("--species", type = "character", help = "Accession number of GEO dataset. Example: GSE56413") + ) + + args <- parse_args(OptionParser( + option_list = option_list, + description = "Get GEO data" + )) + return(args) +} + + +format_species_name <- function(x) { + x <- tools::toTitleCase(x) + x <- gsub("[_-]", " ", x) + return(x) +} + + +get_samples_for_species <- function(eset, species) { + pheno <- pData(eset) + + # check if organism_ch2 exists + if ("organism_ch2" %in% colnames(pheno)) { + keep <- pheno$organism_ch1 == format_species_name(species) & pheno$organism_ch2 == format_species_name(species) + } else { + keep <- pheno$organism_ch1 == format_species_name(species) + } + + # return a data.frame with matching samples + pheno$geo_accession[keep] +} + +download_geo_data_with_retries <- function(accession, species, max_retries = 3, wait_time = 5) { + success <- FALSE + attempts <- 0 + print(listEnsemblGenomes()) + ensembl_plants <- useEnsemblGenomes(biomart = "plants_mart") + print(searchDatasets(ensembl_plants, pattern = species)) + + while (!success && attempts < max_retries) { + + attempts <- attempts + 1 + geo_data <- GEOquery::getGEO( accession ) + eset <- geo_data[[ 1 ]] + # inspect available sample metadata + species_samples <- get_samples_for_species(eset, species) + # List all variable names + #print(colnames(pData(eset))) + + for (file in names(geo_data)) { + + data <- geo_data [[ file ]] + + #print(data) + #counts <- exprs(data) + #samples <- pData(data) + #features <- fData(data) + #print("samples") + #print(samples) + #print("features") + #print(head(features)) + #print(counts) + #print(samples) + #print(features) + + } + success <- TRUE + + } + + return(geo_data) +} + +get_rnaseq_data <- function(data) { + return(list( + count_data = assays( data )$counts, + platform = 'rnaseq', + count_type = 'raw', # rnaseq data are raw in ExpressionAtlas + sample_groups = colData(data)$AtlasAssayGroup + )) +} + +get_one_colour_microarray_data <- function(data) { + return(list( + count_data = exprs( data ), + platform = 'microarray', + count_type = 'normalised', # one colour microarray data are already normalised in ExpressionAtlas + sample_groups = phenoData(data)$AtlasAssayGroup + )) +} + +get_batch_id <- function(accession, data_type) { + batch_id <- paste0(accession, '_', data_type) + # cleaning + batch_id <- gsub("-", "_", batch_id) + return(batch_id) +} + +get_new_sample_names <- function(result, batch_id) { + new_colnames <- paste0(batch_id, '_', colnames(result$count_data)) + return(new_colnames) +} + +export_count_data <- function(result, batch_id) { + + # renaming columns, to make them specific to accession and data type + colnames(result$count_data) <- get_new_sample_names(result, batch_id) + + outfilename <- paste0(batch_id, '.', result$platform, '.', result$count_type, '.counts.csv') + + # exporting to CSV file + # index represents gene names + cat(paste('Exporting count data to file', outfilename)) + write.table(result$count_data, outfilename, sep = ',', row.names = TRUE, col.names = TRUE, quote = FALSE) +} + +export_metadata <- function(result, batch_id) { + + new_colnames <- get_new_sample_names(result, batch_id) + batch_list <- rep(batch_id, length(new_colnames)) + + df <- data.frame( + batch = batch_list, + condition = result$sample_groups, + sample = new_colnames + ) + + outfilename <- paste0(batch_id, '.design.csv') + cat(paste('Exporting design data to file', outfilename)) + write.table(df, outfilename, sep = ',', row.names = FALSE, col.names = TRUE, quote = FALSE) +} + + +process_data <- function(atlas_data, accession) { + + eset <- atlas_data[[ accession ]] + + # looping through each data type (ex: 'rnaseq') in the experiment + for (data_type in names(eset)) { + + data <- eset[[ data_type ]] + + skip_iteration <- FALSE + # getting count dataframe + tryCatch({ + + if ( data_type == 'rnaseq' ) { + result <- get_rnaseq_data(data) + } else if ( startsWith(data_type, 'A-') ) { # typically: A-AFFY- or A-GEOD- + result <- get_one_colour_microarray_data(data) + } else { + stop(paste('ERROR: Unknown data type:', data_type)) + } + + }, error = function(e) { + print(paste("Caught an error: ", e$message)) + print(paste('ERROR: Could not get assay data for experiment ID', accession, 'and data type', data_type)) + skip_iteration <<- TRUE + }) + + # If an error occurred, skip to the next iteration + if (skip_iteration) { + next + } + + #batch_id <- get_batch_id(accession, data_type) + + # exporting count data to CSV + #export_count_data(result, batch_id) + + # exporting metadata to CSV + #export_metadata(result, batch_id) + } + +} + +##################################################### +##################################################### +# MAIN +##################################################### +##################################################### + +args <- get_args() + +cat(paste("Getting data for accession", args$accession, "\n")) + +species <- format_species_name(args$species) +# searching and downloading expression atlas data +geo_data <- download_geo_data_with_retries(args$accession, species) + +# writing count data in atlas_data to specific CSV files +#process_data(atlas_data, args$accession) + diff --git a/bin/get_eatlas_accessions.py b/bin/get_eatlas_accessions.py index b36abeca..7e929812 100755 --- a/bin/get_eatlas_accessions.py +++ b/bin/get_eatlas_accessions.py @@ -3,7 +3,6 @@ # Written by Olivier Coen. Released under the MIT license. import argparse - import requests import pandas as pd from tenacity import ( @@ -16,10 +15,10 @@ import yaml from functools import partial from multiprocessing import Pool -import nltk -from nltk.corpus import wordnet import logging +from natural_language_utils import keywords_in_fields + logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -30,18 +29,6 @@ FILTERED_EXPERIMENTS_METADATA_OUTFILE_NAME = "filtered_experiments.metadata.tsv" FILTERED_EXPERIMENTS_WITH_KEYWORDS_OUTFILE_NAME = "filtered_experiments.keywords.yaml" -################################################################## -################################################################## -# NLTK MODELS AND OBJECTS -################################################################## -################################################################## - -nltk.download("punkt_tab") -nltk.download("averaged_perceptron_tagger_eng") -nltk.download("wordnet") - -lemmatizer = nltk.WordNetLemmatizer() -stemmer = nltk.PorterStemmer() ################################################################## ################################################################## @@ -64,7 +51,10 @@ class ExpressionAtlasNothingFoundError(Exception): def parse_args(): parser = argparse.ArgumentParser("Get expression atlas accessions") parser.add_argument( - "--species", type=str, help="Search Expression Atlas for this specific species" + "--species", + type=str, + required=True, + help="Search Expression Atlas for this specific species" ) parser.add_argument( "--keywords", @@ -80,126 +70,6 @@ def parse_args(): return parser.parse_args() -def get_wordnet_pos(token: str): - tag = nltk.pos_tag([token])[0][1][0].upper() - tag_dict = { - "J": wordnet.ADJ, - "N": wordnet.NOUN, - "V": wordnet.VERB, - "R": wordnet.ADV, - } - return tag_dict.get(tag, wordnet.NOUN) # Default to NOUN if not found - - -def get_stemmed_tokens(sentence: str): - """ - Tokenize a sentence into its constituent words, and then stem each word - - Parameters - ---------- - sentence : str - The sentence to be tokenized and stemmed - - Returns - ------- - tokens : List[str] - The list of stemmed tokens - """ - - tokens = nltk.word_tokenize(sentence) - return [stemmer.stem(token) for token in tokens] - - -def get_lemmed_tokens(sentence: str): - """ - Tokenize a sentence into its constituent words, and then lemmatize each word - - Parameters - ---------- - sentence : str - The sentence to be tokenized and lemmatized - - Returns - ------- - tokens : List[str] - The list of lemmatized tokens - """ - tokens = nltk.word_tokenize(sentence) - return [lemmatizer.lemmatize(token, get_wordnet_pos(token)) for token in tokens] - - -def get_synonyms(word): - """ - Get all synonyms of a word from the wordnet database. - - Parameters - ---------- - word : str - The word for which to get synonyms - - Returns - ------- - synonyms : set - A set of all synonyms of the word - """ - synonyms = [] - for syn in wordnet.synsets(word): - for lemma in syn.lemmas(): - synonyms.append(lemma.name()) # Get the name of each lemma (synonym) - return set(synonyms) # Return as a set to avoid duplicates - - -def get_all_candidate_target_words(sentence: str): - """ - Get all candidate target words from a sentence by stemming and lemmatizing the - tokens and getting synonyms from the wordnet database. - - Parameters - ---------- - sentence : str - The sentence from which to get candidate target words - - Returns - ------- - candidates : list - A list of all candidate target words - """ - candidates = [] - lemmatized_tokens = get_stemmed_tokens(sentence) - stemmed_tokens = get_stemmed_tokens(sentence) - tokens = list(set(lemmatized_tokens + stemmed_tokens)) - for token in tokens: - candidates += get_synonyms(token) - return candidates - - -def word_in_sentence(word: str, sentence: str): - """ - Check if a word (or a stemmed version of it) is in a sentence, or if it is a - subword of a stemmed version of any word in the sentence. - - Parameters - ---------- - word : str - The word to be searched for - sentence : str - The sentence in which to search for the word - - Returns - ------- - bool - True if the word is found in the sentence, False otherwise - """ - for stemmed_word in [word] + get_stemmed_tokens(word): - # testing if stemmed word is in sentence as it is - if stemmed_word in sentence: - return True - # or testing if stemmed word is a subword of a stemmed word from the sentence - for target_word in get_all_candidate_target_words(sentence): - if stemmed_word in target_word: - return True - return False - @retry( retry=retry_if_exception_type(ExpressionAtlasNothingFoundError), @@ -207,7 +77,7 @@ def word_in_sentence(word: str, sentence: str): wait=wait_exponential(multiplier=1, min=1, max=30), before_sleep=before_sleep_log(logger, logging.WARNING), ) -def get_data(url: str): +def get_data(url: str) -> dict: """ Queries a URL and returns the data as a JSON object @@ -265,7 +135,7 @@ def get_experiment_description(exp_dict: dict): raise KeyError(f"Could not find description field in {exp_dict}") -def get_experiment_accesssion(exp_dict: dict): +def get_experiment_accession(exp_dict: dict): """ Gets the accession from an experiment dictionary @@ -410,7 +280,7 @@ def get_experiment_data(exp_dict: dict): def parse_experiment(exp_dict: dict): # getting accession and description - accession = get_experiment_accesssion(exp_dict) + accession = get_experiment_accession(exp_dict) description = get_experiment_description(exp_dict) # getting properties of this experiment exp_data = get_experiment_data(exp_dict) @@ -423,18 +293,11 @@ def parse_experiment(exp_dict: dict): } -def keywords_in_experiment(fields: list[str], keywords: list[str]): - return [ - keyword - for keyword in keywords - for field in fields - if word_in_sentence(keyword, field) - ] -def filter_experiment_with_keywords(exp_dict: dict, keywords: list[str]): +def filter_experiment_with_keywords(exp_dict: dict, keywords: list[str]) -> dict | None: all_searchable_fields = [exp_dict["description"]] + exp_dict["properties"] - found_keywords = keywords_in_experiment(all_searchable_fields, keywords) + found_keywords = keywords_in_fields(all_searchable_fields, keywords) # only returning experiments if found keywords if found_keywords: exp_dict["found_keywords"] = list(set(found_keywords)) @@ -450,11 +313,11 @@ def get_metadata_for_selected_experiments( return [ exp_dict for exp_dict in experiments - if get_experiment_accesssion(exp_dict) in filtered_accessions + if get_experiment_accession(exp_dict) in filtered_accessions ] -def format_species_name(species: str): +def format_species_name(species: str) -> str: return species.replace("_", " ").capitalize().strip() diff --git a/bin/get_geo_dataset_accessions.py b/bin/get_geo_dataset_accessions.py new file mode 100755 index 00000000..20079241 --- /dev/null +++ b/bin/get_geo_dataset_accessions.py @@ -0,0 +1,501 @@ +#!/usr/bin/env python3 + +# Written by Olivier Coen. Released under the MIT license. + +import argparse +from parallelbar import progress_map +from Bio import Entrez +from pathlib import Path +import pandas as pd +import xmltodict +from urllib.request import urlretrieve +import tarfile +from tenacity import ( + retry, + retry_if_exception_type, + stop_after_delay, + wait_exponential, + before_sleep_log, +) +import yaml +from functools import partial +from multiprocessing import cpu_count +import logging + +from natural_language_utils import keywords_in_fields + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +ACCESSION_OUTFILE_NAME = "accessions.txt" +SPECIES_DATASETS_OUTFILE_NAME = "species_datasets.metadata.tsv" +FILTERED_DATASETS_METADATA_OUTFILE_NAME = "filtered_datasets.metadata.tsv" +REJECTED_DATASETS_METADATA_OUTFILE_NAME = "rejected_datasets.metadata.tsv" +SELECTED_DATASETS_METADATA_OUTFILE_NAME = "selected_datasets.metadata.tsv" +FILTERED_EXPERIMENTS_WITH_KEYWORDS_OUTFILE_NAME = "selected_datasets.keywords.yaml" + +ENTREZ_QUERY_MAX_RESULTS = 9999 + +# TODO: see how to integrate RNA-seq experiments as well +GEO_EXPERIMENT_TYPE_TO_PLATFORM = { + "Expression profiling by array": "microarray", + #"Expression profiling by high throughput sequencing": "rnaseq" +} + +MINIML_TMPDIR = "geo_miniml" +Path(MINIML_TMPDIR).mkdir(exist_ok=True) + + +################################################################## +################################################################## +# EXCEPTIONS +################################################################## +################################################################## + + +class GeoDatasetNothingFoundError(Exception): + pass + + +################################################################## +################################################################## +# FUNCTIONS +################################################################## +################################################################## + + +def parse_args(): + parser = argparse.ArgumentParser("Get GEO Datasets accessions") + parser.add_argument( + "--species", + type=str, + required=True, + help="Search GEO Datasets for this specific species" + ) + parser.add_argument( + "--keywords", + type=str, + nargs="*", + help="Keywords to search for in datasets description", + ) + parser.add_argument( + "--platform", + type=str, + #required=True, + help="Platform type" + ) + parser.add_argument( + "--exclude-accessions-in", + dest="excluded_accessions_file", + type=Path, + help="Exclude accessions contained in this file", + ) + return parser.parse_args() + + +@retry( + retry=retry_if_exception_type(Exception), + stop=stop_after_delay(600), + wait=wait_exponential(multiplier=1, min=1, max=30), + before_sleep=before_sleep_log(logger, logging.WARNING), +) +def fetch_geo_datasets_for_species(species: str) -> list[dict]: + """ + Fetch GEO datasets (GSE series) for a given species + + Args: + species (str): Scientific name of the species (e.g. "Homo sapiens"). + """ + + Entrez.email = "stableexpression@nfcore.com" + query = f'"{species}"[Organism] AND "gse"[Entry Type]' + + # getting list of all datasets IDs for this species + # we need possibly to perform multiple queries because the max number of returned results is capped + nb_entries = None + retstart = 0 + while not nb_entries or retstart < nb_entries: + + with Entrez.esearch(db="gds", term=query, retmax=ENTREZ_QUERY_MAX_RESULTS, retstart=retstart) as handle: + record = Entrez.read(handle) + + # getting total nb of entries + if not nb_entries: + nb_entries = int(record["Count"]) + # setting next cursor to the next group + retstart += ENTREZ_QUERY_MAX_RESULTS + + ids = record.get("IdList", []) + if not ids: + logger.warning("No GEO datasets found for your query.") + return [] + + # fetching summary info + with Entrez.esummary(db="gds", id=",".join(ids)) as handle: + results = Entrez.read(handle) + + # keeping only series datasets (just a double check here) + return [ r for r in results if "GSE" in r["Accession"] ] + + +def exclude_unwanted_accessions(datasets: list[dict], excluded_accessions_file: Path) -> list[dict]: + # parsing list of unwanted accessions + with open(excluded_accessions_file) as fin: + excluded_accessions = fin.read().splitlines() + + datasets_to_keep = [] + for dataset in datasets: + if dataset["Accession"] not in excluded_accessions: + datasets_to_keep.append(dataset) + + return datasets_to_keep + + +def format_species(species: str) -> str: + return species.lower().replace(" ", "_") + + +def species_is_ok(dataset: dict, species: str) -> bool: + accession = dataset['Accession'] + # we want datasets only specific to the species we are interested in + parsed_species = dataset["taxon"].split("; ") + if not parsed_species: + logger.warning(f'Accession {accession} rejected: Could not detect species.') + return False + if len(parsed_species) > 1: + logger.warning(f'Accession {accession} rejected: Found multiple species = {parsed_species}') + return False + if format_species(parsed_species[0]) != format_species(species): + logger.warning(f'Accession {accession} rejected: Found wrong species = {parsed_species}') + return False + return True + + +def download_dataset_metadata(ftp_link: str, accession: str) -> Path: + filename = f"miniml/{accession}_family.xml.tgz" + ftp_url = ftp_link + filename + output_file = Path(MINIML_TMPDIR) / f"{accession}.tar.gz" + urlretrieve(ftp_url, output_file) + return output_file + + +def parse_dataset_metadata(file: Path, accession: str) -> dict | None: + with tarfile.open(file, "r:gz") as tar: + file_to_read = f"{accession}_family.xml" + try: + f = tar.extractfile(file_to_read) + except KeyError: + file_to_read = f"{accession}_family.xml/{accession}_family.xml" + try: + f = tar.extractfile(file_to_read) + except KeyError: + return None + if f is None: + raise RuntimeError(f"Failed to get file: {file_to_read}") + xml_content = f.read().decode("utf-8") + return xmltodict.parse(xml_content)['MINiML'] + + +def parse_characteristics(characteristics: str | dict | list, stored_characteristics: list): + if isinstance(characteristics, str): + stored_characteristics.append(characteristics) + elif isinstance(characteristics, dict): + stored_characteristics.append(characteristics["#text"]) + elif isinstance(characteristics, list): + for c in characteristics: + parse_characteristics(c, stored_characteristics) + + +def parse_interesting_metadata(dataset_metadata: dict, additional_metadata: dict) -> dict: + sample_characteristics = [] + sample_library_strategies = [] + sample_library_sources = [] + sample_descriptions = [] + sample_titles = [] + sample_molecule_types = [] + + for sample in additional_metadata["Sample"]: + + # storing description if exists + if sample_description := sample.get("Description"): + sample_descriptions.append(sample_description) + + # storing title if exists + if sample_title := sample.get("Title"): + sample_titles.append(sample_title) + + # storing molecule type if exists + if sample_molecule_type := sample.get("Type"): + sample_molecule_types.append(sample_molecule_type) + + # storing library strategy if exists + if sample_library_strategy := sample.get("Library-Strategy"): + sample_library_strategies.append(sample_library_strategy) + + # storing library source if exists + if sample_library_source := sample.get("Library-Source"): + sample_library_sources.append(sample_library_source) + + # parsing sample metadata + if channels := sample.get("Channel"): + if isinstance(channels, dict): + channels = [channels] + for channel in channels: + parse_characteristics(channel["Characteristics"], sample_characteristics) + + return { + "accession": dataset_metadata["Accession"], + "summary": dataset_metadata["summary"], + "title": dataset_metadata["title"], + "overall_design": additional_metadata['Series']["Overall-Design"], + "experiment_types": dataset_metadata["gdsType"], + "sample_characteristics": list(set(sample_characteristics)), + "sample_library_strategies": list(set(sample_library_strategies)), + "sample_library_sources": list(set(sample_library_sources)), + "sample_descriptions": list(set(sample_descriptions)), + "sample_titles": list(set(sample_titles)), + "sample_molecule_types": list(set(sample_molecule_types)), + } + + +def format_platform_name(platform_name: str) -> str: + return ( + platform_name + .replace("_", "") + .replace("-", "") + .lower() + ) + +def contains_only_rna(molecules_types: list, accession) -> bool: + # we want only GEO series that contain only RNA molecules + # for other series, they should be superseries contained other series that are being parsed too + # so anyway, this would lead in duplicates + if all([ "rna" in molecule_type.lower() for molecule_type in molecules_types]): + return True + logger.info(f'Accession {accession} rejected: Molecule type(s) = {molecules_types}') + return False + + +def contains_proper_experiment_type(experiment_types: list, accession: str, platform: str) -> bool: + for experiment_type in experiment_types: + # if at least one experiment type is ok, we keep this dataset + if GEO_EXPERIMENT_TYPE_TO_PLATFORM.get(experiment_type) == platform: + return True + logger.info(f'Accession {accession} rejected: Experiment type(s) = {experiment_types}') + return False + +""" +def has_proper_library_strategies(library_strategies: list, accession: str, platform: str) -> bool: + if not library_strategies: + logger.warning(f'No library strategies found for accession {accession}') + # since we cannot infer, we return True + return True + + if len(library_strategies) > 1: # multiple different platform technologies found + logger.info(f'Multiple library strategies found for accession {accession}: {library_strategies}') + return False + + if platform is not None: + parsed_platform_name = library_strategies[0] + formatted_platform_name = format_platform_name(parsed_platform_name) + if formatted_platform_name != platform: + logger.info(f'Accession {accession} rejected: Platform = {parsed_platform_name}') + return False + + return True +""" + + +def contains_transcriptomic_source(library_sources: list, accession: str) -> bool: + if library_sources: + if "transcriptomic" not in library_sources: + logger.info(f'Accession {accession} rejected: Source(s) = {library_sources}') + return False + return True + + +def parse_metadata(dataset_metadata: dict) -> dict | None: + accession = dataset_metadata["Accession"] + ftp_link = dataset_metadata["FTPLink"].replace("ftp://", "https://") + downloaded_file = download_dataset_metadata(ftp_link, accession) + additional_metadata = parse_dataset_metadata(downloaded_file, accession) + + # if we could not get additional metadata, we lack too much information to conclude + if additional_metadata is None: + logger.warning(f"Skipping {accession} as additional metadata is missing") + return None + + # parsing interesting information in all available metadata + return parse_interesting_metadata(dataset_metadata, additional_metadata) + + +def dataset_is_valid(metadata: dict, platform: str) -> bool: + accession = metadata["accession"] + # checking platform + if not contains_proper_experiment_type(metadata["experiment_types"], accession, platform): + return False + + # checking that library sources fit + if not contains_transcriptomic_source(metadata["sample_library_sources"], accession): + return False + + # checking that all molecule types are RNA + molecules_types = metadata["sample_molecule_types"] + if not contains_only_rna(molecules_types, accession): + return False + + return True + + +def filter_metadata_with_keywords(metadata: dict, keywords: list[str]) -> dict | None: + all_searchable_fields = ( + [metadata["summary"], metadata["title"]] + + metadata["sample_characteristics"] + + metadata["sample_descriptions"] + + metadata["sample_titles"] + ) + found_keywords = keywords_in_fields(all_searchable_fields, keywords) + # only returning experiments if found keywords + if found_keywords: + metadata["found_keywords"] = list(set(found_keywords)) + logger.info(f"Found keywords: {found_keywords} in accession {metadata['accession']}") + return metadata + else: + return None + + +################################################################## +################################################################## +# MAIN +################################################################## +################################################################## + +def main(): + args = parse_args() + + selected_accessions = [] + + ncpus = cpu_count() - 1 + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # PARSING GEO DATASETS + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + logger.info(f"Getting datasets corresponding to species {args.species}") + dataset_metadata_list = fetch_geo_datasets_for_species(args.species) + logger.info(f"Found {len(dataset_metadata_list)} datasets for species {args.species}") + + #dataset_metadata_list = [d for d in dataset_metadata_list if d['Accession'] == 'GSE8203'] + #print(dataset_metadata_list[0]) + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # EXCLUDING UNWANTED ACCESSIONS + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + if args.excluded_accessions_file: + logger.info(f"Excluding unwanted datasets") + dataset_metadata_list = exclude_unwanted_accessions(dataset_metadata_list, args.excluded_accessions_file) + logger.info(f"{len(dataset_metadata_list)} datasets remaining after excluding unwanted accessions") + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # EXCLUDING DATASETS WITH MORE THAN ONE SPECIES OR WRONG SPECIES + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + logger.info(f"Excluding wrong species") + # TODO: see how to parse data from our species from combined GEO datasets + dataset_metadata_list = [dataset for dataset in dataset_metadata_list if species_is_ok(dataset, args.species)] + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # PARSING METADATA + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + logger.info("Parsing metadata") + metadata_list = progress_map(parse_metadata, dataset_metadata_list, n_cpu=ncpus) + metadata_list = [metadata for metadata in metadata_list if metadata is not None] + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # CHECKING MOLECULE TYPE / PLATFORM TECHNOLOGIES + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + logger.info("Validating datasets") + filtered_metadata_list = [metadata for metadata in metadata_list if dataset_is_valid(metadata, args.platform)] + rejected_metadata_list = [metadata for metadata in metadata_list if metadata not in filtered_metadata_list] + logger.info(f"{len(filtered_metadata_list)} datasets remaining after checking technology platform and molecule type") + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # FILTERING WITH KEYWORDS + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + if args.keywords: + logger.info(f"Filtering experiments with keywords {args.keywords}") + func = partial(filter_metadata_with_keywords, keywords=args.keywords) + selected_metadata_list = progress_map(func, filtered_metadata_list, n_cpu=ncpus) + selected_metadata_list = [metadata for metadata in selected_metadata_list if metadata is not None] + else: + selected_metadata_list = filtered_metadata_list + + if selected_metadata_list: + logger.info(f"Kept {len(selected_metadata_list)} datasets") + # getting accessions of selected experiments + selected_accessions = [metadata["accession"] for metadata in selected_metadata_list] + + else: + msg = f"Could not find experiments for species {args.species}" + if args.keywords: + msg += f" and keywords {args.keywords}" + logger.warning(msg) + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # EXPORTING DATA + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + # exporting list of accessions + logger.info(f"Writing accessions to {ACCESSION_OUTFILE_NAME}") + with open(ACCESSION_OUTFILE_NAME, "w") as fout: + fout.writelines([f"{acc}\n" for acc in selected_accessions]) + + # exporting metadata + logger.info( + f"Writing metadata of all experiments for species {args.species} to {SPECIES_DATASETS_OUTFILE_NAME}" + ) + df = pd.DataFrame.from_dict(dataset_metadata_list) + df.to_csv(SPECIES_DATASETS_OUTFILE_NAME, sep="\t", index=False, header=True) + + if filtered_metadata_list: + logger.info(f"Writing metadata of filtered datasets to {FILTERED_DATASETS_METADATA_OUTFILE_NAME}") + df = pd.DataFrame.from_dict(filtered_metadata_list) + df.to_csv( + FILTERED_DATASETS_METADATA_OUTFILE_NAME, + sep="\t", + index=False, + header=True, + ) + + if rejected_metadata_list: + logger.info(f"Writing metadata of rejected datasets to {REJECTED_DATASETS_METADATA_OUTFILE_NAME}") + df = pd.DataFrame.from_dict(rejected_metadata_list) + df.to_csv( + REJECTED_DATASETS_METADATA_OUTFILE_NAME, + sep="\t", + index=False, + header=True, + ) + + if selected_metadata_list: + logger.info(f"Writing metadata of selected datasets to {SELECTED_DATASETS_METADATA_OUTFILE_NAME}") + df = pd.DataFrame.from_dict(selected_metadata_list) + df.to_csv( + SELECTED_DATASETS_METADATA_OUTFILE_NAME, + sep="\t", + index=False, + header=True, + ) + + # exporting in YAML format too + logger.info( + f"Writing filtered experiments with keywords to {FILTERED_EXPERIMENTS_WITH_KEYWORDS_OUTFILE_NAME}" + ) + with open(FILTERED_EXPERIMENTS_WITH_KEYWORDS_OUTFILE_NAME, "w") as fout: + yaml.dump(selected_metadata_list, fout) + + +if __name__ == "__main__": + main() + diff --git a/bin/natural_language_utils.py b/bin/natural_language_utils.py new file mode 100755 index 00000000..2d774cc2 --- /dev/null +++ b/bin/natural_language_utils.py @@ -0,0 +1,140 @@ +import nltk +from nltk.corpus import wordnet + +nltk.download("punkt_tab") +nltk.download("averaged_perceptron_tagger_eng") +nltk.download("wordnet") + +lemmatizer = nltk.WordNetLemmatizer() +stemmer = nltk.PorterStemmer() + + +def get_wordnet_pos(token: str) -> str: + tag = nltk.pos_tag([token])[0][1][0].upper() + tag_dict = { + "J": wordnet.ADJ, + "N": wordnet.NOUN, + "V": wordnet.VERB, + "R": wordnet.ADV, + } + return tag_dict.get(tag, wordnet.NOUN) # Default to NOUN if not found + + +def get_stemmed_tokens(sentence: str) -> list[str]: + """ + Tokenize a sentence into its constituent words, and then stem each word + + Parameters + ---------- + sentence : str + The sentence to be tokenized and stemmed + + Returns + ------- + tokens : List[str] + The list of stemmed tokens + """ + + tokens = nltk.word_tokenize(sentence) + return [stemmer.stem(token) for token in tokens] + + +def get_lemmed_tokens(sentence: str) -> list[str]: + """ + Tokenize a sentence into its constituent words, and then lemmatize each word + + Parameters + ---------- + sentence : str + The sentence to be tokenized and lemmatized + + Returns + ------- + tokens : List[str] + The list of lemmatized tokens + """ + tokens = nltk.word_tokenize(sentence) + return [lemmatizer.lemmatize(token, get_wordnet_pos(token)) for token in tokens] + + +def get_synonyms(word) -> set[str]: + """ + Get all synonyms of a word from the wordnet database. + + Parameters + ---------- + word : str + The word for which to get synonyms + + Returns + ------- + synonyms : set + A set of all synonyms of the word + """ + synonyms = [] + for syn in wordnet.synsets(word): + for lemma in syn.lemmas(): + synonyms.append(lemma.name()) # Get the name of each lemma (synonym) + return set(synonyms) # Return as a set to avoid duplicates + + +def get_all_candidate_target_words(sentence: str) -> list[str]: + """ + Get all candidate target words from a sentence by stemming and lemmatizing the + tokens and getting synonyms from the wordnet database. + + Parameters + ---------- + sentence : str + The sentence from which to get candidate target words + + Returns + ------- + candidates : list + A list of all candidate target words + """ + candidates = [] + lemmatized_tokens = get_stemmed_tokens(sentence) + stemmed_tokens = get_stemmed_tokens(sentence) + tokens = list(set(lemmatized_tokens + stemmed_tokens)) + for token in tokens: + candidates += get_synonyms(token) + return candidates + + +def word_is_in_sentence(word: str, sentence: str) -> bool: + """ + Check if a word (or a stemmed version of it) is in a sentence, or if it is a + subword of a stemmed version of any word in the sentence. + + Parameters + ---------- + word : str + The word to be searched for + sentence : str + The sentence in which to search for the word + + Returns + ------- + bool + True if the word is found in the sentence, False otherwise + """ + for stemmed_word in [word] + get_stemmed_tokens(word): + # testing if stemmed word is in sentence as it is + if stemmed_word in sentence: + return True + # or testing if stemmed word is a subword of a stemmed word from the sentence + for target_word in get_all_candidate_target_words(sentence): + if stemmed_word in target_word: + return True + return False + + +def keywords_in_fields(fields: list[str], keywords: list[str]) -> list[str]: + return [ + keyword + for keyword in keywords + for field in fields + if word_is_in_sentence(keyword, field) + ] + diff --git a/bin/stability_scorer.py b/bin/stability_scorer.py old mode 100644 new mode 100755 diff --git a/modules/local/expressionatlas/getaccessions/main.nf b/modules/local/expressionatlas/getaccessions/main.nf index e71656f3..c1cc7c06 100644 --- a/modules/local/expressionatlas/getaccessions/main.nf +++ b/modules/local/expressionatlas/getaccessions/main.nf @@ -1,6 +1,6 @@ process EXPRESSIONATLAS_GETACCESSIONS { - label 'process_low' + label 'process_medium' conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? From 842516e3ffb246126097795cae1a168f51e56e80 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Mon, 8 Sep 2025 14:39:40 +0200 Subject: [PATCH 050/258] add gene id mapping test to filter out microarray datasets that are worthless --- bin/get_geo_dataset_accessions.py | 357 ++++++++++++++++++++++-------- bin/gprofiler_utils.py | 216 ++++++++++++++++++ bin/map_ids_to_ensembl.py | 190 +--------------- 3 files changed, 482 insertions(+), 281 deletions(-) create mode 100755 bin/gprofiler_utils.py diff --git a/bin/get_geo_dataset_accessions.py b/bin/get_geo_dataset_accessions.py index 20079241..313e8b5a 100755 --- a/bin/get_geo_dataset_accessions.py +++ b/bin/get_geo_dataset_accessions.py @@ -3,9 +3,13 @@ # Written by Olivier Coen. Released under the MIT license. import argparse +from tqdm import tqdm from parallelbar import progress_map from Bio import Entrez from pathlib import Path +from random import sample +import re +import requests import pandas as pd import xmltodict from urllib.request import urlretrieve @@ -23,6 +27,7 @@ import logging from natural_language_utils import keywords_in_fields +from gprofiler_utils import convert_ids logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -32,9 +37,20 @@ FILTERED_DATASETS_METADATA_OUTFILE_NAME = "filtered_datasets.metadata.tsv" REJECTED_DATASETS_METADATA_OUTFILE_NAME = "rejected_datasets.metadata.tsv" SELECTED_DATASETS_METADATA_OUTFILE_NAME = "selected_datasets.metadata.tsv" +FINAL_DATASETS_METADATA_OUTFILE_NAME = "final_datasets.metadata.tsv" FILTERED_EXPERIMENTS_WITH_KEYWORDS_OUTFILE_NAME = "selected_datasets.keywords.yaml" ENTREZ_QUERY_MAX_RESULTS = 9999 +ENTREZ_EMAIL = "stableexpression@nfcore.com" + +GPROFILER_CHUNKSIZE = 2000 +NB_PROBE_IDS_TO_PARSE = 1000 +NB_PROBE_IDS_TO_SAMPLE = 10 +PLATFORM_DATA_BASE_URL = 'https://www.ncbi.nlm.nih.gov/geo/query/acc.cgi?view=data&acc={platform_accession}' + +LAST_LINE_TO_SKIP = "!platform_table_begin" + +ALLOWED_LIBRARY_SOURCES = ["transcriptomic", "RNA"] # TODO: see how to integrate RNA-seq experiments as well GEO_EXPERIMENT_TYPE_TO_PLATFORM = { @@ -43,7 +59,9 @@ } MINIML_TMPDIR = "geo_miniml" +PLATFORM_SOFT_TMPDIR = "geo_platform_soft" Path(MINIML_TMPDIR).mkdir(exist_ok=True) +Path(PLATFORM_SOFT_TMPDIR).mkdir(exist_ok=True) ################################################################## @@ -57,6 +75,10 @@ class GeoDatasetNothingFoundError(Exception): pass +class GeoPlatformDataTableNotFound(Exception): + pass + + ################################################################## ################################################################## # FUNCTIONS @@ -93,6 +115,10 @@ def parse_args(): return parser.parse_args() +#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# GEO DATASETS +#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + @retry( retry=retry_if_exception_type(Exception), stop=stop_after_delay(600), @@ -107,8 +133,8 @@ def fetch_geo_datasets_for_species(species: str) -> list[dict]: species (str): Scientific name of the species (e.g. "Homo sapiens"). """ - Entrez.email = "stableexpression@nfcore.com" - query = f'"{species}"[Organism] AND "gse"[Entry Type]' + Entrez.email = ENTREZ_EMAIL + query = f'"{species}"[Organism] AND "gse"[Entry Type] AND "expression profiling by array"[DataSet Type]' # getting list of all datasets IDs for this species # we need possibly to perform multiple queries because the max number of returned results is capped @@ -138,39 +164,12 @@ def fetch_geo_datasets_for_species(species: str) -> list[dict]: return [ r for r in results if "GSE" in r["Accession"] ] -def exclude_unwanted_accessions(datasets: list[dict], excluded_accessions_file: Path) -> list[dict]: - # parsing list of unwanted accessions - with open(excluded_accessions_file) as fin: - excluded_accessions = fin.read().splitlines() - - datasets_to_keep = [] - for dataset in datasets: - if dataset["Accession"] not in excluded_accessions: - datasets_to_keep.append(dataset) - - return datasets_to_keep - - -def format_species(species: str) -> str: - return species.lower().replace(" ", "_") - - -def species_is_ok(dataset: dict, species: str) -> bool: - accession = dataset['Accession'] - # we want datasets only specific to the species we are interested in - parsed_species = dataset["taxon"].split("; ") - if not parsed_species: - logger.warning(f'Accession {accession} rejected: Could not detect species.') - return False - if len(parsed_species) > 1: - logger.warning(f'Accession {accession} rejected: Found multiple species = {parsed_species}') - return False - if format_species(parsed_species[0]) != format_species(species): - logger.warning(f'Accession {accession} rejected: Found wrong species = {parsed_species}') - return False - return True - - +@retry( + retry=retry_if_exception_type(Exception), + stop=stop_after_delay(600), + wait=wait_exponential(multiplier=1, min=1, max=30), + before_sleep=before_sleep_log(logger, logging.WARNING), +) def download_dataset_metadata(ftp_link: str, accession: str) -> Path: filename = f"miniml/{accession}_family.xml.tgz" ftp_url = ftp_link + filename @@ -196,6 +195,132 @@ def parse_dataset_metadata(file: Path, accession: str) -> dict | None: return xmltodict.parse(xml_content)['MINiML'] +#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# GEO PLATFORMS +#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +@retry( + retry=retry_if_exception_type(Exception), + stop=stop_after_delay(600), + wait=wait_exponential(multiplier=1, min=1, max=30), + before_sleep=before_sleep_log(logger, logging.WARNING), +) +def fetch_geo_platform_data(platform_accessions: list[str]) -> list[dict]: + """ + Fetch data for a GEO platform + + Args: + platform_accession (str): accession of the platform + """ + Entrez.email = ENTREZ_EMAIL + formatted_platform_accessions = [f'"{platform_accession}"[GEO Accession]' for platform_accession in platform_accessions] + platform_accessions_str = ' OR '.join(formatted_platform_accessions) + query = f'({platform_accessions_str}) AND "gpl"[Entry Type] ' + + with Entrez.esearch(db="gds", term=query, retmax=1) as handle: + record = Entrez.read(handle) + + ids = record.get("IdList", []) + if not ids: + logger.warning(f"No GEO platform found for accessions {platform_accessions}.") + return [] + + # fetching summary info + with Entrez.esummary(db="gds", id=",".join(ids)) as handle: + results = Entrez.read(handle) + + if len(results) > 1: + logger.warning(f"Multiple GEO platforms for accession {platform_accessions}. Taking the first one.") + + return results + + +@retry( + retry=retry_if_exception_type(Exception), + stop=stop_after_delay(600), + wait=wait_exponential(multiplier=1, min=1, max=30), + before_sleep=before_sleep_log(logger, logging.WARNING), +) +def download_platform_datatable(ftp_link: str, platform_accession: str) -> Path: + filename = f"soft/{platform_accession}_family.soft.gz" + ftp_url = ftp_link + filename + output_file = Path(PLATFORM_SOFT_TMPDIR) / f"{platform_accession}.gz" + urlretrieve(ftp_url, output_file) + return output_file + + +def get_platform_probe_id_samples(platform_accession: str): + url = PLATFORM_DATA_BASE_URL.format(platform_accession=platform_accession) + response = requests.get(url, stream=True) + response.raise_for_status() + + header_found = False + probe_ids = [] + counter = 0 + for line in response.iter_lines(decode_unicode=True): + if counter >= NB_PROBE_IDS_TO_PARSE: + break + if line: + # removing HTML patterns + line = re.sub('<[^<]+?>', '', line).strip() + line = re.sub(r'', '', line) + # first things first: try to get the header + if not header_found: + if line.startswith('ID'): + header_found = True + continue + else: + # once the header was gotten, all successive lines are the data + probe_id = line.split("\t")[0] + probe_ids.append(probe_id) + counter += 1 + + nb_samples = min(len(probe_ids), NB_PROBE_IDS_TO_SAMPLE) + return sample(probe_ids, nb_samples) + + +def probe_ids_can_be_converted(dataset_metadata: dict, species: str) -> bool: + platform_dict_list = fetch_geo_platform_data(dataset_metadata['platform_accessions']) + all_probe_ids = [] + + for platform_dict in platform_dict_list: + # looping until we find data for our species + if format_species(platform_dict['taxon']) != format_species(species): + continue + # getting a sample of the first probe ids + sampled_probe_ids = get_platform_probe_id_samples(platform_dict['Accession']) + # if we could not get any probe ids for a platform, we won't use this dataset + if not sampled_probe_ids: + return False + all_probe_ids += sampled_probe_ids + + # try to convert ids + mapping_dict, _ = convert_ids(all_probe_ids, species) + # if at least one ID could be converted + return True if mapping_dict else False + + +#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# FORMATTING +#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +def format_species(species: str) -> str: + return "_".join(species.lower().split(" ")[:2]) + + +def format_platform_name(platform_name: str) -> str: + return ( + platform_name + .replace("_", "") + .replace("-", "") + .lower() + ) + + +#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# METADATA +#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + def parse_characteristics(characteristics: str | dict | list, stored_characteristics: list): if isinstance(characteristics, str): stored_characteristics.append(characteristics) @@ -214,6 +339,8 @@ def parse_interesting_metadata(dataset_metadata: dict, additional_metadata: dict sample_titles = [] sample_molecule_types = [] + platform_accessions = ['GPL' + gpl_id for gpl_id in dataset_metadata["GPL"].split(";")] + for sample in additional_metadata["Sample"]: # storing description if exists @@ -244,7 +371,8 @@ def parse_interesting_metadata(dataset_metadata: dict, additional_metadata: dict parse_characteristics(channel["Characteristics"], sample_characteristics) return { - "accession": dataset_metadata["Accession"], + "accession": dataset_metadata['Accession'], + "platform_accessions": platform_accessions, "summary": dataset_metadata["summary"], "title": dataset_metadata["title"], "overall_design": additional_metadata['Series']["Overall-Design"], @@ -258,13 +386,52 @@ def parse_interesting_metadata(dataset_metadata: dict, additional_metadata: dict } -def format_platform_name(platform_name: str) -> str: - return ( - platform_name - .replace("_", "") - .replace("-", "") - .lower() - ) +def parse_metadata(dataset_metadata: dict) -> dict | None: + accession = dataset_metadata["Accession"] + ftp_link = dataset_metadata["FTPLink"].replace("ftp://", "https://") + downloaded_file = download_dataset_metadata(ftp_link, accession) + additional_metadata = parse_dataset_metadata(downloaded_file, accession) + + # if we could not get additional metadata, we lack too much information to conclude + if additional_metadata is None: + logger.warning(f"Skipping {accession} as additional metadata is missing") + return None + + # parsing interesting information in all available metadata + return parse_interesting_metadata(dataset_metadata, additional_metadata) + + +#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# TESTS +#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +def exclude_unwanted_accessions(datasets: list[dict], excluded_accessions_file: Path) -> list[dict]: + # parsing list of unwanted accessions + with open(excluded_accessions_file) as fin: + excluded_accessions = fin.read().splitlines() + datasets_to_keep = [] + for dataset in datasets: + if dataset["Accession"] not in excluded_accessions: + datasets_to_keep.append(dataset) + return datasets_to_keep + + +def species_is_ok(dataset: dict, species: str) -> bool: + accession = dataset['Accession'] + # we want datasets only specific to the species we are interested in + parsed_species_list = dataset["taxon"].split("; ") + if not parsed_species_list: + logger.warning(f'Accession {accession} rejected: Could not detect species.') + return False + # trying to find our species in the list of species parsed + for parsed_species in parsed_species_list: + if format_species(parsed_species) == format_species(species): + if len(parsed_species_list) > 1: + logger.info(f"Accession {accession}: multiple species detected = {parsed_species_list}") + return True + logger.warning(f'Accession {accession} rejected: Found wrong species = {parsed_species_list}') + return False + def contains_only_rna(molecules_types: list, accession) -> bool: # we want only GEO series that contain only RNA molecules @@ -284,49 +451,18 @@ def contains_proper_experiment_type(experiment_types: list, accession: str, plat logger.info(f'Accession {accession} rejected: Experiment type(s) = {experiment_types}') return False -""" -def has_proper_library_strategies(library_strategies: list, accession: str, platform: str) -> bool: - if not library_strategies: - logger.warning(f'No library strategies found for accession {accession}') - # since we cannot infer, we return True - return True - - if len(library_strategies) > 1: # multiple different platform technologies found - logger.info(f'Multiple library strategies found for accession {accession}: {library_strategies}') - return False - - if platform is not None: - parsed_platform_name = library_strategies[0] - formatted_platform_name = format_platform_name(parsed_platform_name) - if formatted_platform_name != platform: - logger.info(f'Accession {accession} rejected: Platform = {parsed_platform_name}') - return False - - return True -""" - def contains_transcriptomic_source(library_sources: list, accession: str) -> bool: - if library_sources: - if "transcriptomic" not in library_sources: - logger.info(f'Accession {accession} rejected: Source(s) = {library_sources}') - return False - return True - - -def parse_metadata(dataset_metadata: dict) -> dict | None: - accession = dataset_metadata["Accession"] - ftp_link = dataset_metadata["FTPLink"].replace("ftp://", "https://") - downloaded_file = download_dataset_metadata(ftp_link, accession) - additional_metadata = parse_dataset_metadata(downloaded_file, accession) - - # if we could not get additional metadata, we lack too much information to conclude - if additional_metadata is None: - logger.warning(f"Skipping {accession} as additional metadata is missing") - return None - - # parsing interesting information in all available metadata - return parse_interesting_metadata(dataset_metadata, additional_metadata) + # if we have no data about library sources, we just cannot infer + if not library_sources: + return True + # TODO: see how to process series with multiple library sources + if len(library_sources) > 1: + return False + if library_sources[0] in ALLOWED_LIBRARY_SOURCES: + return True + logger.warning(f'Accession {accession} rejected: Source(s) = {library_sources}') + return False def dataset_is_valid(metadata: dict, platform: str) -> bool: @@ -384,9 +520,8 @@ def main(): logger.info(f"Getting datasets corresponding to species {args.species}") dataset_metadata_list = fetch_geo_datasets_for_species(args.species) logger.info(f"Found {len(dataset_metadata_list)} datasets for species {args.species}") - - #dataset_metadata_list = [d for d in dataset_metadata_list if d['Accession'] == 'GSE8203'] - #print(dataset_metadata_list[0]) + #dataset_metadata_list=dataset_metadata_list[:3] + #dataset_metadata_list = [d for d in dataset_metadata_list if d['Accession'] in ['GSE97045', 'GSE6736', 'GSE9683']] #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # EXCLUDING UNWANTED ACCESSIONS # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -397,15 +532,16 @@ def main(): logger.info(f"{len(dataset_metadata_list)} datasets remaining after excluding unwanted accessions") # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # EXCLUDING DATASETS WITH MORE THAN ONE SPECIES OR WRONG SPECIES + # EXCLUDING DATASETS WITH THE WRONG SPECIES # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + logger.info(f"Excluding wrong species") - # TODO: see how to parse data from our species from combined GEO datasets dataset_metadata_list = [dataset for dataset in dataset_metadata_list if species_is_ok(dataset, args.species)] # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # PARSING METADATA # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + logger.info("Parsing metadata") metadata_list = progress_map(parse_metadata, dataset_metadata_list, n_cpu=ncpus) metadata_list = [metadata for metadata in metadata_list if metadata is not None] @@ -416,7 +552,6 @@ def main(): logger.info("Validating datasets") filtered_metadata_list = [metadata for metadata in metadata_list if dataset_is_valid(metadata, args.platform)] - rejected_metadata_list = [metadata for metadata in metadata_list if metadata not in filtered_metadata_list] logger.info(f"{len(filtered_metadata_list)} datasets remaining after checking technology platform and molecule type") # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -431,10 +566,25 @@ def main(): else: selected_metadata_list = filtered_metadata_list - if selected_metadata_list: - logger.info(f"Kept {len(selected_metadata_list)} datasets") + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # FILTERING OUT DATASETS FOR WHICH ID MAPPING DOES NOT WORK + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + logger.info("Checking gene ID mapping issues") + final_metadata_list = [ + metadata for metadata in tqdm(selected_metadata_list) + if probe_ids_can_be_converted(metadata, args.species) + ] + logger.info(f"{len(final_metadata_list)} datasets remaining after checking ID mapping issues") + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # GETTING ACCESSIONS TO DOWNLOAD + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + if final_metadata_list: + logger.info(f"Kept {len(final_metadata_list)} datasets") # getting accessions of selected experiments - selected_accessions = [metadata["accession"] for metadata in selected_metadata_list] + selected_accessions = [metadata["accession"] for metadata in final_metadata_list] else: msg = f"Could not find experiments for species {args.species}" @@ -468,6 +618,14 @@ def main(): header=True, ) + # exporting in YAML format too + logger.info( + f"Writing filtered experiments with keywords to {FILTERED_EXPERIMENTS_WITH_KEYWORDS_OUTFILE_NAME}" + ) + with open(FILTERED_EXPERIMENTS_WITH_KEYWORDS_OUTFILE_NAME, "w") as fout: + yaml.dump(selected_metadata_list, fout) + + rejected_metadata_list = [metadata for metadata in metadata_list if metadata not in filtered_metadata_list] if rejected_metadata_list: logger.info(f"Writing metadata of rejected datasets to {REJECTED_DATASETS_METADATA_OUTFILE_NAME}") df = pd.DataFrame.from_dict(rejected_metadata_list) @@ -488,12 +646,15 @@ def main(): header=True, ) - # exporting in YAML format too - logger.info( - f"Writing filtered experiments with keywords to {FILTERED_EXPERIMENTS_WITH_KEYWORDS_OUTFILE_NAME}" + if final_metadata_list: + logger.info(f"Writing metadata of selected datasets to {FINAL_DATASETS_METADATA_OUTFILE_NAME}") + df = pd.DataFrame.from_dict(final_metadata_list) + df.to_csv( + FINAL_DATASETS_METADATA_OUTFILE_NAME, + sep="\t", + index=False, + header=True, ) - with open(FILTERED_EXPERIMENTS_WITH_KEYWORDS_OUTFILE_NAME, "w") as fout: - yaml.dump(selected_metadata_list, fout) if __name__ == "__main__": diff --git a/bin/gprofiler_utils.py b/bin/gprofiler_utils.py new file mode 100755 index 00000000..566783e4 --- /dev/null +++ b/bin/gprofiler_utils.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 + +# Written by Olivier Coen. Released under the MIT license. + +import requests +import pandas as pd +import logging +import urllib3 +import sys + +from tenacity import ( + retry, + retry_if_exception_type, + stop_after_delay, + wait_exponential, + before_sleep_log, +) + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +################################################################## +# CONSTANTS +################################################################## + +GPROFILER_CONVERT_API_ENDPOINT = "https://biit.cs.ut.ee/gprofiler/api/convert/convert/" +GPROFILER_CONVERT_BETA_API_ENDPOINT = ( + "https://biit.cs.ut.ee/gprofiler_beta/api/convert/convert/" +) + +CHUNKSIZE = 2000 # number of IDs to convert at a time - may create trouble if > 2000 + +TARGET_DATABASE = "ENSG" # Ensembl database +COLS_TO_KEEP = ["incoming", "converted", "name", "description"] +DESCRIPTION_PART_TO_REMOVE_REGEX = r"\s*\[Source:.*?\]" +ENSEMBL_GENE_ID_COLNAME = "ensembl_gene_id" + + +################################################################## +# FUNCTIONS +################################################################## + +def format_species_name(species: str): + """ + Format a species name into a format accepted by g:Profiler. + Example: Arabidopsis thaliana -> athaliana + + Parameters + ---------- + species : str + The species name. + + Returns + ------- + str + The formatted species name. + """ + splitted_species = species.lower().replace("_", " ").split(" ") + return splitted_species[0][0] + splitted_species[1] + + +@retry( + retry=retry_if_exception_type(urllib3.exceptions.ProtocolError), + stop=stop_after_delay(600), + wait=wait_exponential(multiplier=1, min=1, max=30), + before_sleep=before_sleep_log(logger, logging.WARNING), +) +def request_conversion( + gene_ids: list, + species: str, + target_database: str, + url: str = GPROFILER_CONVERT_API_ENDPOINT, + attempts: int = 0, +) -> list[str]: + """ + Send a request to the g:Profiler API to convert a list of gene IDs. + + Parameters + ---------- + gene_ids : list + The list of gene IDs to convert. + species : str + The species to convert the IDs for. + url : str, optional + The URL to send the request to, by default GPROFILER_CONVERT_API_ENDPOINT + attempts : int, optional + The number of attempts already performed, by default 0 + + Returns + ------- + list + The list of dicts corresponding to the converted IDs. + """ + + # formatting species for g:Profiler + organism = format_species_name(species) + + if attempts > 0: + logger.warning( + "g:Profiler main server appears down, trying with the beta server..." + ) + + response = requests.post( + url=url, + json={ + "organism": organism, + "query": gene_ids, + "target": target_database + } + ) + + try: + response.raise_for_status() + except requests.exceptions.HTTPError as err: + if err.response.status_code == 502: + # server appears down + if attempts == 0: + # we only tried with the main server, we try with the beta server + return request_conversion( + gene_ids, + species, + target_database=target_database, + url=GPROFILER_CONVERT_BETA_API_ENDPOINT, + attempts=1, + ) + else: + # both servers appear down, we stop here... + logger.error( + "g:Profiler servers (main and beta) seem to be down... Please retry later... " + "If you have gene ID mappings and / or gene metadata for these datasets, you can provide them " + "directly using the `--gene_id_mapping` and `--gene_metadata` parameters respectively, " + "and by skipping the g:Profiler ID mapping step with `--skip_gprofiler`." + ) + sys.exit(102) + + logger.error(f"Error {err.response.status_code} while converting IDs: {err}") + sys.exit(101) + + return response.json()["result"] + + +def convert_chunk_of_ids(gene_ids: list, species: str) -> tuple[dict, pd.DataFrame]: + """ + Wrapper function that converts a list of gene IDs to another namespace. + + Parameters + ---------- + species : str + The species to convert the IDs for. + gene_ids : list + The IDs to convert. + target_database : str + The target database to convert to. + + Returns + ------- + dict + A dictionary where the keys are the original IDs and the values are the converted IDs. + """ + + results = request_conversion(gene_ids, species, TARGET_DATABASE) + df = pd.DataFrame.from_records(results) + + if df.empty: + return {}, pd.DataFrame() + + # keeping only rows where 'converted' is not null and only the columns of interest + df = df.loc[df["converted"] != "None", COLS_TO_KEEP] + + # dict associating incoming IDs to converted IDs + mapping_dict = df.set_index("incoming").to_dict()["converted"] + + # DataFrame associating converted IDs to name and description + meta_df = df.drop(columns=["incoming"]).rename( + columns={"converted": ENSEMBL_GENE_ID_COLNAME} + ) + + meta_df["name"] = meta_df["name"].str.replace(",", ";") + + # Extract the part before '[Source:...]', or the whole string if not found + meta_df["description"] = ( + meta_df["description"] + .str.replace(DESCRIPTION_PART_TO_REMOVE_REGEX, "", regex=True) + .str.replace(",", ";") + ) + + return mapping_dict, meta_df + + +def chunk_list(lst: list, chunksize: int): + """Splits a list into chunks of a given size. + + Args: + lst (list): The list to split. + chunksize (int): The size of each chunk. + + Returns: + list: A list of chunks, where each chunk is a list of len(chunksize). + """ + return [lst[i : i + chunksize] for i in range(0, len(lst), chunksize)] + + +def convert_ids(ids: list[str], species: str) -> tuple[dict, pd.DataFrame]: + + mapping_dict = {} + gene_metadata_dfs = [] + + chunks = chunk_list(ids, chunksize=CHUNKSIZE) + for chunk_gene_ids in chunks: + # converting to Ensembl IDs for all IDs comprised in this chunk + gene_mapping, meta_df = convert_chunk_of_ids(chunk_gene_ids, species) + mapping_dict.update(gene_mapping) + gene_metadata_dfs.append(meta_df) + + return mapping_dict, gene_metadata_dfs diff --git a/bin/map_ids_to_ensembl.py b/bin/map_ids_to_ensembl.py index 8c0e6fe7..742c90ca 100755 --- a/bin/map_ids_to_ensembl.py +++ b/bin/map_ids_to_ensembl.py @@ -2,21 +2,13 @@ # Written by Olivier Coen. Released under the MIT license. -import requests import pandas as pd from pathlib import Path import argparse import logging -import urllib3 import sys -from tenacity import ( - retry, - retry_if_exception_type, - stop_after_delay, - wait_exponential, - before_sleep_log, -) +from gprofiler_utils import convert_ids logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -30,20 +22,9 @@ METADATA_FILE_SUFFIX = ".metadata.csv" MAPPING_FILE_SUFFIX = ".mapping.csv" -CHUNKSIZE = 2000 # number of IDs to convert at a time - may create trouble if > 2000 - -GPROFILER_CONVERT_API_ENDPOINT = "https://biit.cs.ut.ee/gprofiler/api/convert/convert/" -GPROFILER_CONVERT_BETA_API_ENDPOINT = ( - "https://biit.cs.ut.ee/gprofiler_beta/api/convert/convert/" -) - -TARGET_DATABASE = "ENSG" # Ensembl database -COLS_TO_KEEP = ["incoming", "converted", "name", "description"] -DESCRIPTION_PART_TO_REMOVE_REGEX = r"\s*\[Source:.*?\]" ORIGINAL_GENE_ID_COLNAME = "original_gene_id" ENSEMBL_GENE_ID_COLNAME = "ensembl_gene_id" - ################################################################## # FUNCTIONS ################################################################## @@ -63,159 +44,6 @@ def parse_args(): return parser.parse_args() -def format_species_name(species: str): - """ - Format a species name into a format accepted by g:Profiler. - Example: Arabidopsis thaliana -> athaliana - - Parameters - ---------- - species : str - The species name. - - Returns - ------- - str - The formatted species name. - """ - splitted_species = species.lower().replace("_", " ").split(" ") - return splitted_species[0][0] + splitted_species[1] - - -def chunk_list(lst: list, chunksize: int): - """Splits a list into chunks of a given size. - - Args: - lst (list): The list to split. - chunksize (int): The size of each chunk. - - Returns: - list: A list of chunks, where each chunk is a list of len(chunksize). - """ - return [lst[i : i + chunksize] for i in range(0, len(lst), chunksize)] - - -@retry( - retry=retry_if_exception_type(urllib3.exceptions.ProtocolError), - stop=stop_after_delay(600), - wait=wait_exponential(multiplier=1, min=1, max=30), - before_sleep=before_sleep_log(logger, logging.WARNING), -) -def request_conversion( - gene_ids: list, - species: str, - target_database: str, - url: str = GPROFILER_CONVERT_API_ENDPOINT, - attempts: int = 0, -) -> list[str]: - """ - Send a request to the g:Profiler API to convert a list of gene IDs. - - Parameters - ---------- - gene_ids : list - The list of gene IDs to convert. - species : str - The species to convert the IDs for. - url : str, optional - The URL to send the request to, by default GPROFILER_CONVERT_API_ENDPOINT - attempts : int, optional - The number of attempts already performed, by default 0 - - Returns - ------- - list - The list of dicts corresponding to the converted IDs. - """ - - if attempts > 0: - logger.warning( - "g:Profiler main server appears down, trying with the beta server..." - ) - - response = requests.post( - url=url, - json={"organism": species, "query": gene_ids, "target": target_database}, - ) - - try: - response.raise_for_status() - except requests.exceptions.HTTPError as err: - if err.response.status_code == 502: - # server appears down - if attempts == 0: - # we only tried with the main server, we try with the beta server - return request_conversion( - gene_ids, - species, - target_database=target_database, - url=GPROFILER_CONVERT_BETA_API_ENDPOINT, - attempts=1, - ) - else: - # both servers appear down, we stop here... - logger.error( - "g:Profiler servers (main and beta) seem to be down... Please retry later... " - "If you have gene ID mappings and / or gene metadata for these datasets, you can provide them " - "directly using the `--gene_id_mapping` and `--gene_metadata` parameters respectively, " - "and by skipping the g:Profiler ID mapping step with `--skip_gprofiler`." - ) - sys.exit(102) - - logger.error(f"Error {err.response.status_code} while converting IDs: {err}") - sys.exit(101) - - return response.json()["result"] - - -def convert_ids(gene_ids: list, species: str): - """ - Wrapper function that converts a list of gene IDs to another namespace. - - Parameters - ---------- - species : str - The species to convert the IDs for. - gene_ids : list - The IDs to convert. - target_database : str - The target database to convert to. - - Returns - ------- - dict - A dictionary where the keys are the original IDs and the values are the converted IDs. - """ - - results = request_conversion(gene_ids, species, TARGET_DATABASE) - df = pd.DataFrame.from_records(results) - - if df.empty: - return {} - - # keeping only rows where 'converted' is not null and only the columns of interest - df = df.loc[df["converted"] != "None", COLS_TO_KEEP] - - # dict associating incoming IDs to converted IDs - mapping_dict = df.set_index("incoming").to_dict()["converted"] - - # DataFrame associating converted IDs to name and description - meta_df = df.drop(columns=["incoming"]).rename( - columns={"converted": ENSEMBL_GENE_ID_COLNAME} - ) - - meta_df["name"] = meta_df["name"].str.replace(",", ";") - - # Extract the part before '[Source:...]', or the whole string if not found - meta_df["description"] = ( - meta_df["description"] - .str.replace(DESCRIPTION_PART_TO_REMOVE_REGEX, "", regex=True) - .str.replace(",", ";") - ) - - return mapping_dict, meta_df - - ################################################################## # MAIN ################################################################## @@ -225,7 +53,6 @@ def main(): args = parse_args() count_file = args.count_file - species_name = format_species_name(args.species) logger.info( f"Converting IDs for species {args.species} and count file {count_file.name}..." ) @@ -251,23 +78,20 @@ def main(): )[ENSEMBL_GENE_ID_COLNAME].to_dict() gene_ids_left_to_map = [ - gene_id for gene_id in gene_ids if gene_id not in custom_mappings_dict.keys() + gene_id for gene_id in gene_ids + if gene_id not in custom_mappings_dict.keys() ] logger.info(f"Number of genes left to map: {len(gene_ids_left_to_map)}") + mapping_dict = {} + gene_metadata_dfs = [] + ############################################################# # QUERYING g:PROFILER SERVER ############################################################# - mapping_dict = {} - gene_metadata_dfs = [] if gene_ids_left_to_map: - chunks = chunk_list(gene_ids_left_to_map, chunksize=CHUNKSIZE) - for chunk_gene_ids in chunks: - # converting to Ensembl IDs for all IDs comprised in this chunk - gene_mapping, meta_df = convert_ids(chunk_gene_ids, species_name) - mapping_dict.update(gene_mapping) - gene_metadata_dfs.append(meta_df) + mapping_dict, gene_metadata_dfs = convert_ids(gene_ids_left_to_map, args.species) # adding custom mappings mapping_dict.update(custom_mappings_dict) From aa1105daacb68bc55900c773c613dcbcee336c47 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 10 Sep 2025 11:22:47 +0200 Subject: [PATCH 051/258] made script to download geo count and design functional --- bin/download_geo_data.R | 251 +++++++++++++++++++++++----------------- 1 file changed, 144 insertions(+), 107 deletions(-) diff --git a/bin/download_geo_data.R b/bin/download_geo_data.R index 79444a61..ab8c52b0 100755 --- a/bin/download_geo_data.R +++ b/bin/download_geo_data.R @@ -3,10 +3,12 @@ # Written by Olivier Coen. Released under the MIT license. suppressPackageStartupMessages(library("GEOquery")) +suppressPackageStartupMessages(library("dplyr")) library(GEOquery) library(optparse) -library(biomaRt) +library(dplyr) +options(error = traceback) ##################################################### ##################################################### @@ -49,150 +51,184 @@ get_samples_for_species <- function(eset, species) { pheno$geo_accession[keep] } + +get_columns_for_grouping <- function(df) { + + base_columns <- c("characteristics", "treatment_protocol", "label_protocol", "extract_protocol", "growth_protocol") + + columns_to_group <- c() + for (base_col in base_columns) { + ch1_col <- paste0(base_col, "_ch1") + ch2_col <- paste0(base_col, "_ch2") + + if (ch1_col %in% colnames(df)) { + columns_to_group <- c(columns_to_group, ch1_col) + } + if (ch2_col %in% colnames(df)) { + columns_to_group <- c(columns_to_group, ch2_col) + } + } + + return(columns_to_group) +} + + +build_design_dataframe <- function(df, accession) { + message("Build design dataframe") + + columns_to_group <- get_columns_for_grouping(df) + + design_df <- df %>% + mutate(sample = rownames(.)) %>% + group_by(!!!syms(columns_to_group)) %>% + mutate(group_num = cur_group_id()) %>% + ungroup() %>% + mutate( + group = paste0("G", group_num), + batch = accession + ) %>% + select(sample, group, batch) %>% + arrange(group) + + return(design_df) +} + + download_geo_data_with_retries <- function(accession, species, max_retries = 3, wait_time = 5) { + success <- FALSE attempts <- 0 - print(listEnsemblGenomes()) - ensembl_plants <- useEnsemblGenomes(biomart = "plants_mart") - print(searchDatasets(ensembl_plants, pattern = species)) while (!success && attempts < max_retries) { - attempts <- attempts + 1 - geo_data <- GEOquery::getGEO( accession ) - eset <- geo_data[[ 1 ]] - # inspect available sample metadata - species_samples <- get_samples_for_species(eset, species) - # List all variable names - #print(colnames(pData(eset))) - - for (file in names(geo_data)) { - - data <- geo_data [[ file ]] - - #print(data) - #counts <- exprs(data) - #samples <- pData(data) - #features <- fData(data) - #print("samples") - #print(samples) - #print("features") - #print(head(features)) - #print(counts) - #print(samples) - #print(features) - - } - success <- TRUE + + tryCatch({ + geo_data <- GEOquery::getGEO( accession ) + success <- TRUE + + }, error = function(e) { + + message("Attempt ", attempts, " Message: ", e$message) + + if (attempts < max_retries) { + warning("Retrying in ", wait_time, " seconds...") + Sys.sleep(wait_time) + + } else { + warning("Unhandled error: ", e$message) + quit(save = "no", status = 102) # quit & stop workflow + } + }) } return(geo_data) -} -get_rnaseq_data <- function(data) { - return(list( - count_data = assays( data )$counts, - platform = 'rnaseq', - count_type = 'raw', # rnaseq data are raw in ExpressionAtlas - sample_groups = colData(data)$AtlasAssayGroup - )) } -get_one_colour_microarray_data <- function(data) { - return(list( - count_data = exprs( data ), - platform = 'microarray', - count_type = 'normalised', # one colour microarray data are already normalised in ExpressionAtlas - sample_groups = phenoData(data)$AtlasAssayGroup - )) -} -get_batch_id <- function(accession, data_type) { - batch_id <- paste0(accession, '_', data_type) - # cleaning - batch_id <- gsub("-", "_", batch_id) - return(batch_id) +check_microarray_normalisation <- function(df) { + + vals <- unlist(df, use.names = FALSE) + vals <- vals[!is.na(vals)] + + all_integers <- all(abs(vals - round(vals)) < 1e-8) + value_range <- range(vals, na.rm = TRUE) + + if (value_range[2] <= 20) { + message("Normalized, log2 scale (e.g. RMA, quantile)") + } else if (all_integers) { + message("Raw probe intensities (unnormalized CEL-like data)") + quit(save = "no", status = 102) + } else if (value_range[2] > 1000) { + message("Normalized but not log-transformed (e.g. MAS5, raw intensities)") + quit(save = "no", status = 102) + } else { + message("Unclear data origin, check GEO metadata") + quit(save = "no", status = 102) + } } -get_new_sample_names <- function(result, batch_id) { - new_colnames <- paste0(batch_id, '_', colnames(result$count_data)) - return(new_colnames) + +clean_count_data <- function(df) { + message("Cleaning counts") + # removes rows that are all NA + df <- df[rowSums(!is.na(df)) > 0, ] + } -export_count_data <- function(result, batch_id) { - # renaming columns, to make them specific to accession and data type - colnames(result$count_data) <- get_new_sample_names(result, batch_id) +process_data <- function(atlas_data, accession, species) { - outfilename <- paste0(batch_id, '.', result$platform, '.', result$count_type, '.counts.csv') + eset <- geo_data[[ 1 ]] + #print(exprs(eset)) + # Get metadata table + metadata_df <- pData(eset) + design_df <- build_design_dataframe(metadata_df, accession) - # exporting to CSV file - # index represents gene names - cat(paste('Exporting count data to file', outfilename)) - write.table(result$count_data, outfilename, sep = ',', row.names = TRUE, col.names = TRUE, quote = FALSE) -} + # get samples corresponding to species + species_samples <- get_samples_for_species(eset, species) -export_metadata <- function(result, batch_id) { + # filter design dataframe + design_df <- design_df %>% + filter(sample %in% species_samples) - new_colnames <- get_new_sample_names(result, batch_id) - batch_list <- rep(batch_id, length(new_colnames)) + if ( length(names(geo_data)) > 1 ) { + warning("Multiple data files were found") + quit(save = "no", status = 100) # quit & ignore process + } - df <- data.frame( - batch = batch_list, - condition = result$sample_groups, - sample = new_colnames - ) + file <- names(geo_data)[[ 1 ]] - outfilename <- paste0(batch_id, '.design.csv') - cat(paste('Exporting design data to file', outfilename)) - write.table(df, outfilename, sep = ',', row.names = FALSE, col.names = TRUE, quote = FALSE) -} + data <- geo_data [[ file ]] + #print(fData(data)) + # get count data for samples corresponding to the species of interest + count_df <- data.frame(exprs(data)) %>% + select(all_of(species_samples)) + # checking that data are from RMA pipeline and followed proper normalisation + # raises error otherwise + check_microarray_normalisation(count_df) -process_data <- function(atlas_data, accession) { + # clean counts: + # * removes rows that are all NA + count_df <- clean_count_data(count_df) - eset <- atlas_data[[ accession ]] + # exporting count data to CSV + export_count_data(count_df, accession) - # looping through each data type (ex: 'rnaseq') in the experiment - for (data_type in names(eset)) { + # exporting metadata to CSV + export_metadata(design_df, accession) +} - data <- eset[[ data_type ]] - skip_iteration <- FALSE - # getting count dataframe - tryCatch({ +export_count_data <- function(count_df, batch_id) { - if ( data_type == 'rnaseq' ) { - result <- get_rnaseq_data(data) - } else if ( startsWith(data_type, 'A-') ) { # typically: A-AFFY- or A-GEOD- - result <- get_one_colour_microarray_data(data) - } else { - stop(paste('ERROR: Unknown data type:', data_type)) - } + # renaming columns, to make them specific to accession and data type + colnames(count_df) <- paste0(batch_id, '_', colnames(count_df)) - }, error = function(e) { - print(paste("Caught an error: ", e$message)) - print(paste('ERROR: Could not get assay data for experiment ID', accession, 'and data type', data_type)) - skip_iteration <<- TRUE - }) + outfilename <- paste0(batch_id, '.microarray.normalised.counts.csv') - # If an error occurred, skip to the next iteration - if (skip_iteration) { - next - } + # exporting to CSV file + # index represents gene names + message(paste('Exporting count data to file', outfilename)) + write.table(count_df, outfilename, sep = ',', row.names = TRUE, col.names = TRUE, quote = FALSE) +} - #batch_id <- get_batch_id(accession, data_type) +export_metadata <- function(design_df, batch_id) { - # exporting count data to CSV - #export_count_data(result, batch_id) + new_sample_names <- paste0(batch_id, '_', design_df$sample) - # exporting metadata to CSV - #export_metadata(result, batch_id) - } + df <- design_df %>% + mutate(sample = new_sample_names ) %>% + select(sample, group, batch) + outfilename <- paste0(batch_id, '.design.csv') + message(paste('Exporting design data to file', outfilename)) + write.table(df, outfilename, sep = ',', row.names = FALSE, col.names = TRUE, quote = FALSE) } + ##################################################### ##################################################### # MAIN @@ -207,6 +243,7 @@ species <- format_species_name(args$species) # searching and downloading expression atlas data geo_data <- download_geo_data_with_retries(args$accession, species) -# writing count data in atlas_data to specific CSV files -#process_data(atlas_data, args$accession) +process_data(geo_data, args$accession, args$species) + + From 746d2de0d1cc16d1beb2fb7cb13e8afedc543bb0 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Thu, 11 Sep 2025 09:15:20 +0200 Subject: [PATCH 052/258] finalised integration and testing of GEO in the pipeline --- README.md | 2 +- bin/download_geo_data.R | 10 +- bin/get_geo_dataset_accessions.py | 12 +- bin/gprofiler_utils.py | 77 ++++--- conf/base.config | 31 ++- conf/modules.config | 12 ++ conf/test.config | 2 +- conf/test_eatlas_geo.config | 21 ++ conf/test_eatlas_only.config | 2 +- conf/test_ignore_errors.config | 2 +- conf/test_local_and_downloaded.config | 2 +- docs/usage.md | 4 +- .../nf_core_stableexpression.boilerplate.xml | 2 +- galaxy/tool/nf_core_stableexpression.xml | 8 +- modules/local/clean_count_data/main.nf | 2 +- .../compute_gene_statistics/global/main.nf | 2 +- .../per_platform/main.nf | 2 +- modules/local/dataset_statistics/main.nf | 2 +- modules/local/expressionatlas/getdata/main.nf | 5 +- modules/local/geo/getaccessions/main.nf | 60 ++++++ modules/local/geo/getaccessions/spec-file.txt | 74 +++++++ modules/local/geo/getdata/main.nf | 70 ++++++ modules/local/geo/getdata/spec-file.txt | 201 ++++++++++++++++++ modules/local/idmapping/gprofiler/main.nf | 2 +- modules/local/merge_counts/main.nf | 2 +- modules/local/normalisation/deseq2/main.nf | 2 +- modules/local/normalisation/edger/main.nf | 2 +- modules/local/quantile_normalisation/main.nf | 2 +- nextflow.config | 39 ++-- nextflow_schema.json | 105 ++++++--- .../local/expressionatlas_fetchdata/main.nf | 88 ++------ subworkflows/local/geo_fetchdata/main.nf | 106 +++++++++ .../main.nf | 65 ++++++ .../local/geo/getaccessions/main.nf.test | 28 +++ .../local/geo/getaccessions/main.nf.test.snap | 87 ++++++++ .../expressionatlas_fetchdata/main.nf.test | 10 +- .../get_accessions/exclude_no_accessions.txt | 0 tests/workflows/stableexpression.nf.test | 4 +- workflows/stableexpression.nf | 16 +- 39 files changed, 961 insertions(+), 202 deletions(-) create mode 100644 conf/test_eatlas_geo.config create mode 100644 modules/local/geo/getaccessions/main.nf create mode 100644 modules/local/geo/getaccessions/spec-file.txt create mode 100644 modules/local/geo/getdata/main.nf create mode 100644 modules/local/geo/getdata/spec-file.txt create mode 100644 subworkflows/local/geo_fetchdata/main.nf create mode 100644 tests/modules/local/geo/getaccessions/main.nf.test create mode 100644 tests/modules/local/geo/getaccessions/main.nf.test.snap create mode 100644 tests/test_data/geo/get_accessions/exclude_no_accessions.txt diff --git a/README.md b/README.md index 44e4b8ce..9f0788e1 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ Now you can run the pipeline as follows: > -profile docker \ > --species \ > --eatlas_accessions \ -> --eatlas_keywords \ +> --keywords \ > --datasets ./datasets.csv \ > --outdir ./results > ``` diff --git a/bin/download_geo_data.R b/bin/download_geo_data.R index ab8c52b0..0b5cb337 100755 --- a/bin/download_geo_data.R +++ b/bin/download_geo_data.R @@ -116,7 +116,7 @@ download_geo_data_with_retries <- function(accession, species, max_retries = 3, } else { warning("Unhandled error: ", e$message) - quit(save = "no", status = 102) # quit & stop workflow + quit(save = "no", status = 100) # quit & stop workflow } }) @@ -139,13 +139,13 @@ check_microarray_normalisation <- function(df) { message("Normalized, log2 scale (e.g. RMA, quantile)") } else if (all_integers) { message("Raw probe intensities (unnormalized CEL-like data)") - quit(save = "no", status = 102) + quit(save = "no", status = 110) } else if (value_range[2] > 1000) { message("Normalized but not log-transformed (e.g. MAS5, raw intensities)") - quit(save = "no", status = 102) + quit(save = "no", status = 111) } else { message("Unclear data origin, check GEO metadata") - quit(save = "no", status = 102) + quit(save = "no", status = 112) } } @@ -175,7 +175,7 @@ process_data <- function(atlas_data, accession, species) { if ( length(names(geo_data)) > 1 ) { warning("Multiple data files were found") - quit(save = "no", status = 100) # quit & ignore process + quit(save = "no", status = 101) # quit & ignore process } file <- names(geo_data)[[ 1 ]] diff --git a/bin/get_geo_dataset_accessions.py b/bin/get_geo_dataset_accessions.py index 313e8b5a..b1f69fb4 100755 --- a/bin/get_geo_dataset_accessions.py +++ b/bin/get_geo_dataset_accessions.py @@ -32,6 +32,10 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) +# set a custom writable directory before any Entrez operations +# mandatory for running the script in an apptainer container +#Entrez.Parser.Parser.directory("/tmp/biopython") + ACCESSION_OUTFILE_NAME = "accessions.txt" SPECIES_DATASETS_OUTFILE_NAME = "species_datasets.metadata.tsv" FILTERED_DATASETS_METADATA_OUTFILE_NAME = "filtered_datasets.metadata.tsv" @@ -103,7 +107,6 @@ def parse_args(): parser.add_argument( "--platform", type=str, - #required=True, help="Platform type" ) parser.add_argument( @@ -135,6 +138,7 @@ def fetch_geo_datasets_for_species(species: str) -> list[dict]: Entrez.email = ENTREZ_EMAIL query = f'"{species}"[Organism] AND "gse"[Entry Type] AND "expression profiling by array"[DataSet Type]' + logger.info(f"Fetching GEO datasets with query: {query}") # getting list of all datasets IDs for this species # we need possibly to perform multiple queries because the max number of returned results is capped @@ -148,6 +152,12 @@ def fetch_geo_datasets_for_species(species: str) -> list[dict]: # getting total nb of entries if not nb_entries: nb_entries = int(record["Count"]) + + # if there is no entry for this species + if nb_entries == 0: + logger.info(f"No entries found for query: {query}") + return [] + # setting next cursor to the next group retstart += ENTREZ_QUERY_MAX_RESULTS diff --git a/bin/gprofiler_utils.py b/bin/gprofiler_utils.py index 566783e4..de97c926 100755 --- a/bin/gprofiler_utils.py +++ b/bin/gprofiler_utils.py @@ -16,6 +16,11 @@ before_sleep_log, ) +from requests.exceptions import ( + HTTPError, + ConnectionError +) + logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -101,41 +106,49 @@ def request_conversion( "g:Profiler main server appears down, trying with the beta server..." ) - response = requests.post( - url=url, - json={ - "organism": organism, - "query": gene_ids, - "target": target_database - } - ) + server_appears_down = False try: - response.raise_for_status() - except requests.exceptions.HTTPError as err: - if err.response.status_code == 502: - # server appears down - if attempts == 0: - # we only tried with the main server, we try with the beta server - return request_conversion( - gene_ids, - species, - target_database=target_database, - url=GPROFILER_CONVERT_BETA_API_ENDPOINT, - attempts=1, - ) + response = requests.post( + url=url, + json={ + "organism": organism, + "query": gene_ids, + "target": target_database + } + ) + except requests.exceptions.ConnectionError: + server_appears_down = True + else: + try: + response.raise_for_status() + except (HTTPError, ConnectionError) as err: + if err.response.status_code == 502: + server_appears_down = True else: - # both servers appear down, we stop here... - logger.error( - "g:Profiler servers (main and beta) seem to be down... Please retry later... " - "If you have gene ID mappings and / or gene metadata for these datasets, you can provide them " - "directly using the `--gene_id_mapping` and `--gene_metadata` parameters respectively, " - "and by skipping the g:Profiler ID mapping step with `--skip_gprofiler`." - ) - sys.exit(102) - - logger.error(f"Error {err.response.status_code} while converting IDs: {err}") - sys.exit(101) + logger.error(f"Error {err.response.status_code} while converting IDs: {err}") + sys.exit(101) + + + if server_appears_down: + if attempts == 0: + logger.warning("g:Profiler main server appears down, trying with the beta server...") + return request_conversion( + gene_ids, + species, + target_database=target_database, + url=GPROFILER_CONVERT_BETA_API_ENDPOINT, + attempts=1, + ) + else: + # both servers appear down, we stop here... + logger.error( + "g:Profiler servers (main and beta) seem to be down... Please retry later... " + "If you have gene ID mappings and / or gene metadata for these datasets, you can provide them " + "directly using the `--gene_id_mapping` and `--gene_metadata` parameters respectively, " + "and by skipping the g:Profiler ID mapping step with `--skip_gprofiler`." + ) + sys.exit(102) return response.json()["result"] diff --git a/conf/base.config b/conf/base.config index bf07fd33..b09a1842 100644 --- a/conf/base.config +++ b/conf/base.config @@ -28,29 +28,28 @@ process { // See https://www.nextflow.io/docs/latest/config.html#config-process-selectors withLabel:process_single { cpus = { 1 } - memory = { 6.GB * task.attempt } - time = { 4.h * task.attempt } + memory = { 2.GB * task.attempt } + time = { 1.h * task.attempt } } withLabel:process_low { - cpus = { 2 * task.attempt } - memory = { 12.GB * task.attempt } - time = { 4.h * task.attempt } + cpus = { 1 } + memory = { 4.GB * task.attempt } + time = { 1.h * task.attempt } } withLabel:process_medium { - cpus = { 6 * task.attempt } - memory = { 36.GB * task.attempt } - time = { 8.h * task.attempt } + cpus = { 6 * task.attempt } + memory = { 10.GB * task.attempt } + time = { 2.h * task.attempt } + } + withLabel:process_high_cpus { + cpus = { 12 * task.attempt } + memory = { 10.GB * task.attempt } + time = { 2.h * task.attempt } } withLabel:process_high { cpus = { 12 * task.attempt } - memory = { 72.GB * task.attempt } - time = { 16.h * task.attempt } - } - withLabel:process_long { - time = { 20.h * task.attempt } - } - withLabel:process_high_memory { - memory = { 200.GB * task.attempt } + memory = { 20.GB * task.attempt } + time = { 4.h * task.attempt } } withLabel:error_ignore { errorStrategy = 'ignore' diff --git a/conf/modules.config b/conf/modules.config index 58122212..fa356d9e 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -29,6 +29,18 @@ process { ] } + withName: 'GEO_GETACCESSIONS' { + publishDir = [ + path: { "${params.outdir}/geo/accessions/" } + ] + } + + withName: 'GEO_GETDATA' { + publishDir = [ + path: { "${params.outdir}/geo/datasets/" } + ] + } + withName: 'IDMAPPING_GPROFILER' { publishDir = [ path: { "${params.outdir}/idmapping/datasets/" } diff --git a/conf/test.config b/conf/test.config index a81201c3..1778c614 100644 --- a/conf/test.config +++ b/conf/test.config @@ -17,6 +17,6 @@ params { // Input data species = 'beta vulgaris' - eatlas_keywords = "leaf" + keywords = "leaf" outdir = "results/test" } diff --git a/conf/test_eatlas_geo.config b/conf/test_eatlas_geo.config new file mode 100644 index 00000000..5d85872f --- /dev/null +++ b/conf/test_eatlas_geo.config @@ -0,0 +1,21 @@ +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Nextflow config file for running minimal tests +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Defines input files and everything required to run a fast and simple pipeline test. + It tests the different ways to use the pipeline, with small data + + Use as follows: + nextflow run nf-core/stableexpression -profile test_eatlas_geo, --outdir + +---------------------------------------------------------------------------------------- +*/ + +params { + config_profile_name = 'Test dataset custom gene data profile' + config_profile_description = 'Minimal test dataset with custom gene metadata to check pipeline function' + + // Input data + species = 'solanum lycopersicum' + outdir = "results/test_eatlas_geo" +} diff --git a/conf/test_eatlas_only.config b/conf/test_eatlas_only.config index af59596e..9dd7b19a 100644 --- a/conf/test_eatlas_only.config +++ b/conf/test_eatlas_only.config @@ -25,7 +25,7 @@ params { // Input data species = 'solanum tuberosum' - eatlas_keywords = "potato,stress" + keywords = "potato,stress" eatlas_accessions = "E-MTAB-552" outdir = "results/test" } diff --git a/conf/test_ignore_errors.config b/conf/test_ignore_errors.config index 81f3d196..96f1950d 100644 --- a/conf/test_ignore_errors.config +++ b/conf/test_ignore_errors.config @@ -17,7 +17,7 @@ params { // Input data species = 'beta vulgaris' - eatlas_keywords = "leaf" + keywords = "leaf" datasets = "tests/test_data/input_datasets/input.csv" outdir = "results/test_dataset" // quant_norm_target_distrib = "uniform" diff --git a/conf/test_local_and_downloaded.config b/conf/test_local_and_downloaded.config index f90796ec..a5b5fed5 100644 --- a/conf/test_local_and_downloaded.config +++ b/conf/test_local_and_downloaded.config @@ -25,7 +25,7 @@ params { // Input data species = 'solanum tuberosum' - eatlas_keywords = "potato,stress" + keywords = "potato,stress" eatlas_accessions = "E-MTAB-552" datasets = "tests/test_data/input_datasets/input.csv" outdir = "results/test_local_and_downloaded" diff --git a/docs/usage.md b/docs/usage.md index 6966c25d..55fbe1c0 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -28,7 +28,7 @@ The pipeline fetches Expression Atlas accessions for the provided species / keyw nextflow run nf-core/stableexpression \ -profile \ --species \ - --eatlas_keywords + --keywords --outdir ``` @@ -118,7 +118,7 @@ Example usage: > -profile docker \ > --species "Arabidopsis thaliana" \ > --eatlas_accessions "E-MTAB-552,E-GEOD-61690" \ -> --eatlas_keywords "stress,flowering" \ +> --keywords "stress,flowering" \ > --datasets ./datasets.csv \ > --outdir ./results > ``` diff --git a/galaxy/build/static/nf_core_stableexpression.boilerplate.xml b/galaxy/build/static/nf_core_stableexpression.boilerplate.xml index 74e239fc..15dcb8ed 100644 --- a/galaxy/build/static/nf_core_stableexpression.boilerplate.xml +++ b/galaxy/build/static/nf_core_stableexpression.boilerplate.xml @@ -85,7 +85,7 @@ INPUTS - + diff --git a/galaxy/tool/nf_core_stableexpression.xml b/galaxy/tool/nf_core_stableexpression.xml index 177fe8bf..9d1e285d 100644 --- a/galaxy/tool/nf_core_stableexpression.xml +++ b/galaxy/tool/nf_core_stableexpression.xml @@ -57,8 +57,8 @@ VERSION="1.0dev"; echo "$VERSION" #if $input_output_options.skip_fetch_eatlas_accessions --skip_fetch_eatlas_accessions $input_output_options.skip_fetch_eatlas_accessions #end if - #if $expression_atlas_options.eatlas_keywords - --eatlas_keywords "$expression_atlas_options.eatlas_keywords" + #if $expression_atlas_options.keywords + --keywords "$expression_atlas_options.keywords" #end if #if $expression_atlas_options.eatlas_accessions --eatlas_accessions "$expression_atlas_options.eatlas_accessions" @@ -110,7 +110,7 @@ VERSION="1.0dev"; echo "$VERSION"
    - + ([a-zA-Z,]+) @@ -164,7 +164,7 @@ VERSION="1.0dev"; echo "$VERSION" - + diff --git a/modules/local/clean_count_data/main.nf b/modules/local/clean_count_data/main.nf index dbf5c57f..432bb616 100644 --- a/modules/local/clean_count_data/main.nf +++ b/modules/local/clean_count_data/main.nf @@ -1,6 +1,6 @@ process CLEAN_COUNT_DATA { - label 'process_low' + label 'process_single' errorStrategy = { if (task.exitStatus == 101) { diff --git a/modules/local/compute_gene_statistics/global/main.nf b/modules/local/compute_gene_statistics/global/main.nf index 02527b0f..96e28dfe 100644 --- a/modules/local/compute_gene_statistics/global/main.nf +++ b/modules/local/compute_gene_statistics/global/main.nf @@ -1,6 +1,6 @@ process COMPUTE_GLOBAL_GENE_STATISTICS { - label 'process_low' + label 'process_high' errorStrategy = { if (task.exitStatus == 100) { diff --git a/modules/local/compute_gene_statistics/per_platform/main.nf b/modules/local/compute_gene_statistics/per_platform/main.nf index 9aee3dd7..3a734f5b 100644 --- a/modules/local/compute_gene_statistics/per_platform/main.nf +++ b/modules/local/compute_gene_statistics/per_platform/main.nf @@ -1,6 +1,6 @@ process COMPUTE_GENE_STATISTICS_PER_PLATFORM { - label 'process_low' + label 'process_medium' errorStrategy = { if (task.exitStatus == 100) { diff --git a/modules/local/dataset_statistics/main.nf b/modules/local/dataset_statistics/main.nf index 43b9cec2..0d04c175 100644 --- a/modules/local/dataset_statistics/main.nf +++ b/modules/local/dataset_statistics/main.nf @@ -1,6 +1,6 @@ process DATASET_STATISTICS { - label 'process_low' + label 'process_single' tag "${meta.dataset}" diff --git a/modules/local/expressionatlas/getdata/main.nf b/modules/local/expressionatlas/getdata/main.nf index 582ec1ef..81e7b6bc 100644 --- a/modules/local/expressionatlas/getdata/main.nf +++ b/modules/local/expressionatlas/getdata/main.nf @@ -1,6 +1,6 @@ process EXPRESSIONATLAS_GETDATA { - label 'process_low' + label 'process_single' // limiting to 8 threads at a time to avoid 429 errors with the Expression Atlas API server maxForks 8 @@ -53,9 +53,6 @@ process EXPRESSIONATLAS_GETDATA { tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //"'), topic: versions tuple val("${task.process}"), val('ExpressionAtlas'), eval('Rscript -e "cat(as.character(packageVersion(\'ExpressionAtlas\')))"'), topic: versions - when: - task.ext.when == null || task.ext.when - script: """ get_eatlas_data.R --accession $accession diff --git a/modules/local/geo/getaccessions/main.nf b/modules/local/geo/getaccessions/main.nf new file mode 100644 index 00000000..118be7a5 --- /dev/null +++ b/modules/local/geo/getaccessions/main.nf @@ -0,0 +1,60 @@ +process GEO_GETACCESSIONS { + + label 'process_high_cpus' + + conda "${moduleDir}/spec-file.txt" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/ca/caae35ec5dc72367102a616a47b6f1a7b3de9ff272422f2c08895b8bb5f0566c/data': + 'community.wave.seqera.io/library/biopython_nltk_pandas_parallelbar_pruned:5fc501b07f8e0428' }" + + input: + val species + val keywords + val platform + path excluded_accessions_file + + output: + path "accessions.txt", emit: accessions + path "*.metadata.tsv", emit: metadata + path "selected_datasets.keywords.yaml", optional: true, topic: selected_experiment_keywords + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions + tuple val("${task.process}"), val('requests'), eval('python3 -c "import requests; print(requests.__version__)"'), topic: versions + tuple val("${task.process}"), val('nltk'), eval('python3 -c "import nltk; print(nltk.__version__)"'), topic: versions + tuple val("${task.process}"), val('pyyaml'), eval('python3 -c "import yaml; print(yaml.__version__)"'), topic: versions + tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions + tuple val("${task.process}"), val('xmltodict'), eval('python3 -c "import xmltodict; print(xmltodict.__version__)"'), topic: versions + tuple val("${task.process}"), val('biopython'), eval('python3 -c "import Bio; print(Bio.__version__)"'), topic: versions + + script: + def keywords_string = keywords.split(',').collect { it.trim() }.join(' ') + def args = " --species $species" + if ( keywords_string != "" ) { + args += " --keywords $keywords_string" + } + if ( platform != 'none' ) { + args += " --platform $platform" + } + if ( excluded_accessions_file != 'none' ) { + args += " --exclude-accessions-in $excluded_accessions_file" + } + // the folder where nltk will download data needs to be writable (necessary for singularity) + """ + # the Entrez module from biopython automatically stores temp results in /.config + # if this directory is not writable, the script fails + export HOME=/tmp/biopython + mkdir -p /tmp/biopython + + export NLTK_DATA=$PWD + + get_geo_dataset_accessions.py $args + """ + + stub: + """ + touch accessions.txt \\ + all_experiments.metadata.tsv \\ + filtered_experiments.metadata.tsv \\ + filtered_experiments.keywords.yaml + """ + +} diff --git a/modules/local/geo/getaccessions/spec-file.txt b/modules/local/geo/getaccessions/spec-file.txt new file mode 100644 index 00000000..ede7a99d --- /dev/null +++ b/modules/local/geo/getaccessions/spec-file.txt @@ -0,0 +1,74 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://repo.anaconda.com/pkgs/main/linux-64/_libgcc_mutex-0.1-main.conda#c3473ff8bdb3d124ed5ff11ec380d6f9 +https://repo.anaconda.com/pkgs/main/linux-64/_openmp_mutex-5.1-1_gnu.conda#71d281e9c2192cb3fa425655a8defb85 +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_4.conda#f406dcbb2e7bef90d793e50e79a2882b +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_4.conda#8a4ab7ff06e4db0be22485332666da0f +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_4.conda#53e876bc2d2648319e94c33c57b9ec74 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_2.conda#dfc5aae7b043d9f56ba99514d5e60625 +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-34_h59b9bed_openblas.conda#064c22bac20fecf2a99838f9b979374c +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-34_he106b2a_openblas.conda#148b531b5457ad666ed76ceb4c766505 +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-34_h7ac8fdf_openblas.conda#f05a31377b4d9a8d8740f47d1e70b70e +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_4.conda#3c376af8888c386b9d3d1c2701e2f3ab +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_4.conda#28771437ffcd9f3417c66012dc49a3be +https://repo.anaconda.com/pkgs/main/linux-64/bzip2-1.0.8-h5eee18b_6.conda#f21a3ff51c1b271977f53ce956a69297 +https://repo.anaconda.com/pkgs/main/linux-64/libstdcxx-ng-11.2.0-h1234567_1.conda#57623d10a70e09e1d048c2b2b6f4e2dd +https://repo.anaconda.com/pkgs/main/linux-64/expat-2.7.1-h6a678d5_0.conda#269942a9f3f943e2e5d8a2516a861f7c +https://repo.anaconda.com/pkgs/main/linux-64/ld_impl_linux-64-2.40-h12ee557_0.conda#ee672b5f635340734f58d618b7bca024 +https://repo.anaconda.com/pkgs/main/linux-64/libffi-3.4.4-h6a678d5_1.conda#70646cc713f0c43926cfdcfe9b695fe0 +https://repo.anaconda.com/pkgs/main/linux-64/libmpdec-4.0.0-h5eee18b_0.conda#feb10f42b1a7b523acbf85461be41a3e +https://repo.anaconda.com/pkgs/main/linux-64/libuuid-1.41.5-h5eee18b_0.conda#4a6a2354414c9080327274aa514e5299 +https://repo.anaconda.com/pkgs/main/linux-64/ncurses-6.5-h7934f7d_0.conda#0abfc090299da4bb031b84c64309757b +https://repo.anaconda.com/pkgs/main/linux-64/ca-certificates-2025.7.15-h06a4308_0.conda#a65eaddc4f9529b9c908f544ca50e7e0 +https://repo.anaconda.com/pkgs/main/linux-64/openssl-3.0.17-h5eee18b_0.conda#c032152f4080dd61875d5047641c8bf2 +https://repo.anaconda.com/pkgs/main/linux-64/python_abi-3.13-0_cp313.conda#d4009c49dd2b54ffded7f1365b5f6505 +https://repo.anaconda.com/pkgs/main/linux-64/readline-8.3-hc2a1206_0.conda#8578e006d4ef5cb98a6cda232b3490f6 +https://repo.anaconda.com/pkgs/main/linux-64/zlib-1.2.13-h5eee18b_1.conda#92e42d8310108b0a440fb2e60b2b2a25 +https://repo.anaconda.com/pkgs/main/linux-64/sqlite-3.50.2-hb25bd0a_1.conda#6ac08aa6b5f14911039aa04b2b2c3350 +https://repo.anaconda.com/pkgs/main/linux-64/pthread-stubs-0.3-h0ce48e5_1.conda#973a642312d2a28927aaf5b477c67250 +https://repo.anaconda.com/pkgs/main/linux-64/xorg-libxau-1.0.12-h9b100fa_0.conda#a8005a9f6eb903e113cd5363e8a11459 +https://repo.anaconda.com/pkgs/main/linux-64/xorg-libxdmcp-1.1.5-h9b100fa_0.conda#c284a09ddfba81d9c4e740110f09ea06 +https://repo.anaconda.com/pkgs/main/linux-64/libxcb-1.17.0-h9b100fa_0.conda#fdf0d380fa3809a301e2dbc0d5183883 +https://repo.anaconda.com/pkgs/main/linux-64/xorg-xorgproto-2024.1-h5eee18b_1.conda#412a0d97a7a51d23326e57226189da92 +https://repo.anaconda.com/pkgs/main/linux-64/xorg-libx11-1.8.12-h9b100fa_1.conda#6298b27afae6f49f03765b2a03df2fcb +https://repo.anaconda.com/pkgs/main/linux-64/tk-8.6.15-h54e0aa7_0.conda#1fa91e0c4fc9c9435eda3f1a25a676fd +https://repo.anaconda.com/pkgs/main/noarch/tzdata-2025b-h04d1e81_0.conda#1d027393db3427ab22a02aa44a56f143 +https://repo.anaconda.com/pkgs/main/linux-64/xz-5.6.4-h5eee18b_1.conda#3581505fa450962d631bd82b8616350e +https://repo.anaconda.com/pkgs/main/linux-64/python-3.13.5-h4612cfd_100_cp313.conda#1adf42b71c42a4a540eae2c0026f02c3 +https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.2-py313hf6604e3_2.conda#67d27f74a90f5f0336035203f91a0abc +https://conda.anaconda.org/conda-forge/linux-64/biopython-1.85-py313h07c4f96_2.conda#376f132b855fa7879f361c8c523c0768 +https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py313h7033f15_4.conda#bc8624c405856b1d047dd0a81829b08c +https://conda.anaconda.org/conda-forge/noarch/certifi-2025.8.3-pyhd8ed1ab_0.conda#11f59985f49df4620890f3e746ed7102 +https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda#12c566707c80111f9799308d9e265aef +https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py313hfab6e84_0.conda#ce6386a5892ef686d6d680c345c40ad1 +https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.3-pyhd8ed1ab_0.conda#7e7d5ef1b9ed630e4a1c358d6bc62284 +https://conda.anaconda.org/conda-forge/noarch/click-8.2.1-pyh707e725_0.conda#94b550b8d3a614dbd326af798c7dfb40 +https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda#962b9857ee8e7018c22f2776ffa0b2d7 +https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda#0a802cb9888dd14eeefc611f05c40b6e +https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda#8e6923fc12f1fe8f8c4e5c9f343256ac +https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda#164fc43f0b53b6e3a7bc7dce5e4f1dc9 +https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda#39a4f67be3286c86d696df570b1201b7 +https://repo.anaconda.com/pkgs/main/linux-64/setuptools-78.1.1-py313h06a4308_0.conda#8f8e1c1e3af9d2d371aaa0ee8316ae7c +https://conda.anaconda.org/conda-forge/noarch/joblib-1.5.2-pyhd8ed1ab_0.conda#4e717929cfa0d49cef92d911e31d0e90 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_4.conda#3baf8976c96134738bba224e9ef6b1e5 +https://conda.anaconda.org/conda-forge/noarch/nltk-3.9.1-pyhd8ed1ab_1.conda#85fd21c82d46f871d3820c17270e575d +https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda#3339e3b65d58accf4ca4fb8748ab16b3 +https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 +https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 +https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 +https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.2-py313h08cd8bf_0.conda#5f4cc42e08d6d862b7b919a3c8959e0b +https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda#9efbfdc37242619130ea42b1cc4ed861 +https://conda.anaconda.org/conda-forge/noarch/parallelbar-2.5-pyhd8ed1ab_0.conda#984636bb8e6681a56b71fc09a911c3b3 +https://repo.anaconda.com/pkgs/main/linux-64/wheel-0.45.1-py313h06a4308_0.conda#29057e876eedce0e37c2388c138a19f9 +https://repo.anaconda.com/pkgs/main/noarch/pip-25.2-pyhc872135_0.conda#b829d36091ab08d18cafe8994ac6e02b +https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda#461219d1a5bd61342293efa2c0c90eac +https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda#a77f85f77be52ff59391544bfe73390a +https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py313h8060acc_2.conda#50992ba61a8a1f8c2d346168ae1c86df +https://conda.anaconda.org/conda-forge/linux-64/regex-2025.7.34-py313h07c4f96_1.conda#bad6ae3c034586b998bd8901ca76915a +https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py313h07c4f96_3.conda#0720da5e63f3c93647350cc217fdf2bc +https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda#436c165519e140cb08d246a4472a9d6a +https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda#db0c6b99149880c8ba515cf4abe93ee4 +https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.2-pyhd8ed1ab_0.conda#5d99943f2ae3cc69e1ada12ce9d4d701 +https://conda.anaconda.org/conda-forge/noarch/xmltodict-0.14.2-pyhd8ed1ab_1.conda#96ef17b8734b174d35346da0762f0137 diff --git a/modules/local/geo/getdata/main.nf b/modules/local/geo/getdata/main.nf new file mode 100644 index 00000000..4e35fe11 --- /dev/null +++ b/modules/local/geo/getdata/main.nf @@ -0,0 +1,70 @@ +process GEO_GETDATA { + + label 'process_single' + + // limiting to 8 threads at a time to avoid 429 errors with the Expression Atlas API server + maxForks 8 + + tag "$accession" + + errorStrategy = { + if (task.exitStatus == 100) { + // ignoring accessions that cannot be retrieved from GEO + log.warn("Could not retrieve data for accession ${accession}. This could be a transient network issue or a permission error.") + return 'ignore' + } else if (task.exitStatus == 101) { + log.warn("GEO dataset with accession ${accession} contains multiple files.") + return 'ignore' + } else if (task.exitStatus == 110) { + log.warn("GEO dataset for accession ${accession} does not seem normalised.") + return 'ignore' + } else if (task.exitStatus == 111) { + log.warn("GEO dataset for accession ${accession} seems normalised but not log-transformed.") + return 'ignore' + } else if (task.exitStatus == 112) { + log.warn("GEO dataset for accession ${accession} are of unclear origin. Could not infer normalisation state.") + return 'ignore' + } else if (task.exitStatus == 137) { // override default behaviour to sleep some time before retry + // in case of OOM errors, we wait a bit and try again (2 retries) + if ( task.attempt in <= 2) { + sleep(Math.pow(2, task.attempt) * 2000 as long) + return 'retry' + } else { + log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") + return 'ignore' + } + } else { + return 'terminate' + } + } + + conda "${moduleDir}/spec-file.txt" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/4c/4cb08d96e62942e7b6288abf2cfd30e813521a022459700e610325a3a7c0b1c8/data': + 'community.wave.seqera.io/library/bioconductor-geoquery_r-base_r-dplyr_r-optparse:fcd002470b7d6809' }" + + input: + val accession + val species + + output: + path "*.design.csv", optional: true, emit: design + path "*.counts.csv", optional: true, emit: counts + tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //"'), topic: versions + tuple val("${task.process}"), val('GEOquery'), eval('Rscript -e "cat(as.character(packageVersion(\'GEOquery\')))"'), topic: versions + tuple val("${task.process}"), val('dplyr'), eval('Rscript -e "cat(as.character(packageVersion(\'dplyr\')))"'), topic: versions + + script: + """ + download_geo_data.R \\ + --accession $accession \\ + --species $species + """ + + stub: + """ + touch acc.microarray.normalised.counts.csv + touch acc.design.csv + """ + +} diff --git a/modules/local/geo/getdata/spec-file.txt b/modules/local/geo/getdata/spec-file.txt new file mode 100644 index 00000000..8bfecd20 --- /dev/null +++ b/modules/local/geo/getdata/spec-file.txt @@ -0,0 +1,201 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_4.conda#3baf8976c96134738bba224e9ef6b1e5 +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d +https://conda.anaconda.org/conda-forge/noarch/_r-mutex-1.0.1-anacondar_1.tar.bz2#19f9db5f4f1b7f5ef5f6d67207f25f38 +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_4.conda#f406dcbb2e7bef90d793e50e79a2882b +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_4.conda#28771437ffcd9f3417c66012dc49a3be +https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62ee74e96c5ebb0af99386de58cf9553 +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda#0be7c6e070c19105f966d3758448d018 +https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda#4211416ecba1866fab0c6470986c22d6 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 +https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc +https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda#0b367fad34931cb79e0d6b7e5c06bb1c +https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda#74784ee3d225fc3dca89edb635b4e5cc +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda#ffffb341206dd0dab0c36053c048d621 +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 +https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 +https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 +https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a +https://conda.anaconda.org/conda-forge/linux-64/python-3.13.7-h2b335a9_100_cp313.conda#724dcf9960e933838247971da07fe5cf +https://conda.anaconda.org/conda-forge/noarch/argcomplete-3.6.2-pyhd8ed1ab_0.conda#eb9d4263271ca287d2e0cf5a86da2d3a +https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-5.14.0-he073ed8_2.conda#0dedbff35a50868200993a2ccf051390 +https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.34-h087de78_2.conda#79592e1be84fccb8a117d9e7b9d01753 +https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.44-h4bf12b8_1.conda#e45cfedc8ca5630e02c106ea36d2c5c6 +https://conda.anaconda.org/conda-forge/linux-64/bwidget-1.10.1-ha770c72_1.conda#983b92277d78c0d0ec498e460caa0e6d +https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.50-h421ea60_1.conda#7af8e91b0deb5f8e25d1a595dea79614 +https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.13.3-h48d6fc4_1.conda#3c255be50a506c50765a93a6644f32fe +https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.13.3-ha770c72_1.conda#51f5be229d83ecd401fb369ab96ae669 +https://conda.anaconda.org/conda-forge/linux-64/freetype-2.13.3-ha770c72_1.conda#9ccd736d31e0c6e41f54e704e5312811 +https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.15.0-h7e30c49_1.conda#8f5b0b297b59e1ac160ad4beec99dbee +https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2#0c96522c6bdaed4b1566d11387caaf45 +https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2#34893075a5c9e55cdafac56607368fc6 +https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2#4d59c254e01d9cde7957100457e2d5fb +https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda#49023d73832ef61042f6a237cb2687e7 +https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29 +https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_4.conda#3c376af8888c386b9d3d1c2701e2f3ab +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_4.conda#2d34729cbc1da0ec988e57b13b712067 +https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 +https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda#915f5995e94f60e9a4826e0b0920ee88 +https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.45-hc749103_0.conda#b90bece58b4c2bf25969b70f3be42d25 +https://conda.anaconda.org/conda-forge/linux-64/libglib-2.84.3-hf39c6af_0.conda#467f23819b1ea2b89c3fc94d65082301 +https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda#b3c17d95b5a10c6e64a21fa17573e70e +https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb9d3cd8_0.conda#f6ebe2cb3f82ba6c057dde5d9debe4f7 +https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb9d3cd8_0.conda#8035c64cb77ed555e3f150b7b3972480 +https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda#92ed62436b625154323d40d5f2f11dd7 +https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_1.conda#c01af13bdc553d1a8fbfff6e8db075f0 +https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda#fb901ff28063514abb6046c9ec2c4a45 +https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda#1c74ff8c35dcadf952a16f752ca5aa49 +https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.12-h4f16b4b_0.conda#db038ce880f100acc74dba10302b5630 +https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.6-hb9d3cd8_0.conda#febbab7d15033c913d53c7a2c102309d +https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda#96d57aba173e878a2089d5638016dc5e +https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda#09262e66b19567aff4f592fb53b28760 +https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda#b38117a3c920364aff79f870c984b4a3 +https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda#c277e0a4d549b03ac1e9d6cbbe3d017b +https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda#3f43953b7d3fb3aaa1d0d0723d91e368 +https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.5-hb9d3cd8_0.conda#f7f0d6cc2dc986d42ac2689ec88192be +https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda#172bf1cd1ff8629f2b1179945ed45055 +https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda#b499ce4b026493a13774bcf0f4c33849 +https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda#eecce068c7e4eddeb169591baac20ac4 +https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda#6432cb5d4ac0046c3ac0a8a0f95842f9 +https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.14.1-h332b0f4_0.conda#45f6713cb00f124af300342512219182 +https://conda.anaconda.org/conda-forge/linux-64/curl-8.14.1-h332b0f4_0.conda#60279087a10b4ab59a70daa838894e4b +https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-15.1.0-h4c094af_104.conda#05eec361e8eca1ad47bad0f8b97a9d67 +https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-15.1.0-h97b714f_4.conda#9577e03ec70b7986ab78a3f057af0df8 +https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-15.1.0-h4393ad2_4.conda#bd50f28da1e011caf83ebfe967dbcc94 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_4.conda#8a4ab7ff06e4db0be22485332666da0f +https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-15.1.0-h3b9cdf2_4.conda#82f37031ba4df0e97a222646ddcfd673 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_4.conda#53e876bc2d2648319e94c33c57b9ec74 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_2.conda#dfc5aae7b043d9f56ba99514d5e60625 +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-34_h59b9bed_openblas.conda#064c22bac20fecf2a99838f9b979374c +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-34_he106b2a_openblas.conda#148b531b5457ad666ed76ceb4c766505 +https://conda.anaconda.org/conda-forge/linux-64/gsl-2.7-he838d99_0.tar.bz2#fec079ba39c9cca093bf4c00001825de +https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-15.1.0-h4c094af_104.conda#608049d7d920f3c559197d4c5445d243 +https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-15.1.0-h6a1bac1_4.conda#f880f89a51a8f93ecdc3b82c4627dc99 +https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.24-h86f0d12_0.conda#64f0c503da58ec25ebd359e4d990afa8 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-15.1.0-h69a702a_4.conda#b1a97c0f2c4f1bb2b8872a21fc7e17a7 +https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.0-hb9d3cd8_0.conda#9fa334557db9f63da6c9285fd2a48638 +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-34_h7ac8fdf_openblas.conda#f05a31377b4d9a8d8740f47d1e70b70e +https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda#9344155d33912347b37f0ae6c410a835 +https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda#aea31d2e5b1091feca96fcfe945c3cf9 +https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.0-h8261f1e_6.conda#b6093922931b535a7ba566b6f384fbe6 +https://conda.anaconda.org/conda-forge/linux-64/make-4.4.1-hb9d3cd8_2.conda#33405d2a66b1411db9f7242c8b97c9e7 +https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.10-h36c2ea0_0.tar.bz2#ac7bc6a654f8f41b352b38f4051135f8 +https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-hecca717_2.conda#2cd94587f3a401ae05e03a6caf09539d +https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-11.4.5-h15599e2_0.conda#1276ae4aa3832a449fcb4253c30da4bc +https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.4-hadf4263_0.conda#79f71230c069a287efe3a8614069ddf1 +https://conda.anaconda.org/conda-forge/linux-64/sed-4.9-h6688a6e_0.conda#171afc5f7ca0408bbccbcb69ade85f92 +https://conda.anaconda.org/conda-forge/linux-64/tktable-2.10-h8d826fa_7.conda#3ac51142c19ba95ae0fadefa333c9afb +https://conda.anaconda.org/conda-forge/linux-64/xorg-libxt-1.3.1-hb9d3cd8_0.conda#279b0de5f6ba95457190a1c459a64e31 +https://conda.anaconda.org/conda-forge/linux-64/r-base-4.4.3-h85845a0_2.conda#d1573e5f701f21560e1fc6b206f0dc55 +https://conda.anaconda.org/bioconda/noarch/bioconductor-biocgenerics-0.52.0-r44hdfd78af_3.tar.bz2#8a9defade51c2c2a6b90a4474dcdfdfc +https://conda.anaconda.org/bioconda/linux-64/bioconductor-biobase-2.66.0-r44h3df3fcb_0.tar.bz2#56f651b4dbe8625ba510c7c3233da8a9 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-s4vectors-0.44.0-r44h3df3fcb_2.tar.bz2#13bdbde9c9496802b7974d006f22fe11 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-iranges-2.40.0-r44h3df3fcb_2.tar.bz2#f2e8f02a2987ccaf39a22c36fa0e9fa8 +https://conda.anaconda.org/conda-forge/linux-64/oniguruma-6.9.10-hb9d3cd8_0.conda#6ce853cb231f18576d2db5c2d4cb473e +https://conda.anaconda.org/conda-forge/linux-64/jq-1.8.1-h73b1eb8_0.conda#2714e43bfc035f7ef26796632aa1b523 +https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda#a77f85f77be52ff59391544bfe73390a +https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py313h8060acc_2.conda#50992ba61a8a1f8c2d346168ae1c86df +https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e +https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda#b0dd904de08b7db706167240bf37b164 +https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.13.3-pyha770c72_0.conda#146402bf0f11cbeb8f781fa4309a95d3 +https://conda.anaconda.org/conda-forge/noarch/xmltodict-0.14.2-pyhd8ed1ab_1.conda#96ef17b8734b174d35346da0762f0137 +https://conda.anaconda.org/conda-forge/noarch/yq-3.4.3-pyhe01879c_2.conda#18cefe7c50c1228da474ea0e95a8e646 +https://conda.anaconda.org/bioconda/noarch/bioconductor-data-packages-20250625-hdfd78af_0.tar.bz2#34d7066b99d7e6769305dcebf0a9de87 +https://conda.anaconda.org/bioconda/noarch/bioconductor-genomeinfodbdata-1.2.13-r44hdfd78af_0.tar.bz2#06d453df3bc59956a3ffac7674652f44 +https://conda.anaconda.org/conda-forge/linux-64/r-curl-7.0.0-r44h10955f1_0.conda#7f9ea9206c1eba58b56cdb49894c1a10 +https://conda.anaconda.org/conda-forge/linux-64/r-jsonlite-2.0.0-r44h2b5f3a1_0.conda#741243137a52f978739eff83126dc2bb +https://conda.anaconda.org/conda-forge/linux-64/r-mime-0.13-r44h2b5f3a1_0.conda#58856b0f45ffe6477a5a92cc2fc0843c +https://conda.anaconda.org/conda-forge/linux-64/r-sys-3.4.3-r44h2b5f3a1_0.conda#7771befc8f294d762f12a48dffa1650a +https://conda.anaconda.org/conda-forge/linux-64/r-askpass-1.2.1-r44h2b5f3a1_0.conda#6e2597e8d6c7e8c2d7dec6d50103d958 +https://conda.anaconda.org/conda-forge/linux-64/r-openssl-2.3.3-r44he8289e2_0.conda#216e57ec49b5079a1a5387f1482cad0f +https://conda.anaconda.org/conda-forge/noarch/r-r6-2.6.1-r44hc72bb7e_0.conda#08d1985cbe6bd96a818e127de51f9905 +https://conda.anaconda.org/conda-forge/noarch/r-httr-1.4.7-r44hc72bb7e_1.conda#9dd48155c67d87a2adc68ddb7c3ab508 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-ucsc.utils-1.2.0-r44h9ee0642_1.tar.bz2#1653b8e9eb24949cc045e21ad21927b6 +https://conda.anaconda.org/bioconda/noarch/bioconductor-genomeinfodb-1.42.0-r44hdfd78af_2.tar.bz2#52f21aeff5a62bf53a37a4ad4d197e06 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-zlibbioc-1.52.0-r44h3df3fcb_2.tar.bz2#1f08126acbb2d9a8835aba0b1a7bf9e2 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-xvector-0.46.0-r44h15a9599_2.tar.bz2#1ce6efc6cb59e8ce10ec4cb722ece013 +https://conda.anaconda.org/conda-forge/noarch/r-crayon-1.5.3-r44hc72bb7e_1.conda#89626d77a94256b8304bb8fc33d3364c +https://conda.anaconda.org/bioconda/linux-64/bioconductor-biostrings-2.74.0-r44h3df3fcb_1.tar.bz2#964a5ef8fe0d0e9a599efab3fa7c3a71 +https://conda.anaconda.org/conda-forge/linux-64/r-png-0.1_8-r44h21f035c_2.conda#7726f2ccf765e2e70168df48920084d1 +https://conda.anaconda.org/bioconda/noarch/bioconductor-keggrest-1.46.0-r44hdfd78af_0.tar.bz2#dd1a6a8509dc43cec9a22754eec1bef3 +https://conda.anaconda.org/conda-forge/noarch/r-dbi-1.2.3-r44hc72bb7e_1.conda#15f0ce8bf5c00f734af73165feb59e13 +https://conda.anaconda.org/conda-forge/linux-64/r-bit-4.6.0-r44h2b5f3a1_0.conda#02e9bed2800ee2cc9e7506e808e6ef06 +https://conda.anaconda.org/conda-forge/linux-64/r-bit64-4.6.0_1-r44h2b5f3a1_0.conda#8875d4a17c3252891859c834c3925707 +https://conda.anaconda.org/conda-forge/linux-64/r-rlang-1.1.6-r44h93ab643_0.conda#b154e08c49d92f0f59b9236f189a8264 +https://conda.anaconda.org/conda-forge/linux-64/r-cli-3.6.5-r44h93ab643_0.conda#f1c2722622bb979e389c132212de9220 +https://conda.anaconda.org/conda-forge/linux-64/r-glue-1.8.0-r44h2b5f3a1_0.conda#990ecdd6246dfcce8cea4eb5a97ca735 +https://conda.anaconda.org/conda-forge/noarch/r-lifecycle-1.0.4-r44hc72bb7e_1.conda#66d0ba7c05d0abfa11d126ba8e2907aa +https://conda.anaconda.org/conda-forge/linux-64/r-vctrs-0.6.5-r44h0d4f4ea_1.conda#399bc7872bdb40b5531d887858253e76 +https://conda.anaconda.org/conda-forge/noarch/r-blob-1.2.4-r44hc72bb7e_2.conda#4f9161cb110a1e0d95c7631294fbba40 +https://conda.anaconda.org/conda-forge/noarch/r-cpp11-0.5.2-r44h785f33e_1.conda#c8f41b1d8dbbc1b057d282e59cbc46ca +https://conda.anaconda.org/conda-forge/linux-64/r-fastmap-1.2.0-r44ha18555a_1.conda#314840b54bb3397ea0dd118bab7bcb1e +https://conda.anaconda.org/conda-forge/linux-64/r-cachem-1.1.0-r44hb1dbf0f_1.conda#a8ac6cdc444baf323c90c47789f62387 +https://conda.anaconda.org/conda-forge/noarch/r-memoise-2.0.1-r44hc72bb7e_3.conda#3b5d996b00a781c3a08a05687ce55075 +https://conda.anaconda.org/conda-forge/noarch/r-pkgconfig-2.0.3-r44hc72bb7e_4.conda#17d36e686fb918637ca43fc4c036549e +https://conda.anaconda.org/conda-forge/noarch/r-plogr-0.2.0-r44hc72bb7e_1006.conda#4ff9218577c4dcf961a80afdaa851927 +https://conda.anaconda.org/conda-forge/linux-64/r-rsqlite-2.4.3-r44h3697838_0.conda#e2b405b74d9e946818fdb1674e66d53c +https://conda.anaconda.org/bioconda/noarch/bioconductor-annotationdbi-1.68.0-r44hdfd78af_0.tar.bz2#cdec31da7826d82f22cdebc22c8755d2 +https://conda.anaconda.org/conda-forge/linux-64/r-ellipsis-0.3.2-r44hb1dbf0f_3.conda#a96cb6b4b61efd45ffe47019af5eb879 +https://conda.anaconda.org/conda-forge/noarch/r-generics-0.1.4-r44hc72bb7e_0.conda#c02ed249dc33336dfa20ca3eeaf3d1ee +https://conda.anaconda.org/conda-forge/linux-64/r-magrittr-2.0.3-r44hb1dbf0f_3.conda#a53562b6400cbbf0323fb97881ba61b3 +https://conda.anaconda.org/conda-forge/linux-64/r-fansi-1.0.6-r44hb1dbf0f_1.conda#c4c0d4b82b54899c61c6f3e09b1bcc5c +https://conda.anaconda.org/conda-forge/linux-64/r-utf8-1.2.6-r44h2b5f3a1_0.conda#e463d3779b87ad5615816f1c3cde1135 +https://conda.anaconda.org/conda-forge/noarch/r-pillar-1.11.0-r44hc72bb7e_0.conda#6fc3eef9e2b83886004b85758b33d61c +https://conda.anaconda.org/conda-forge/linux-64/r-tibble-3.3.0-r44h2b5f3a1_0.conda#e61406c01509e1a942d9b01de165b454 +https://conda.anaconda.org/conda-forge/noarch/r-withr-3.0.2-r44hc72bb7e_0.conda#7c7e6e8f6fc8d0fd3baf24e8a5ed8ff5 +https://conda.anaconda.org/conda-forge/noarch/r-tidyselect-1.2.1-r44hc72bb7e_1.conda#aa5f953d9b0709ee347bb4e4dd5acfea +https://conda.anaconda.org/conda-forge/linux-64/r-dplyr-1.1.4-r44h0d4f4ea_1.conda#1c060647efac55dce8530efc27b39825 +https://conda.anaconda.org/conda-forge/linux-64/r-purrr-1.1.0-r44h54b55ab_0.conda#c092fa236b23d0b345dbae05cb5cee5f +https://conda.anaconda.org/conda-forge/linux-64/r-stringi-1.8.7-r44h3c328a7_0.conda#f040df1163b8069796dafbf17ccf5990 +https://conda.anaconda.org/conda-forge/noarch/r-stringr-1.5.1-r44h785f33e_1.conda#f1fdeed70529cb6e74277d758b5304fd +https://conda.anaconda.org/conda-forge/linux-64/r-tidyr-1.3.1-r44h0d4f4ea_1.conda#28992fad4ad3081d55f9d7bcd1c7a1ef +https://conda.anaconda.org/conda-forge/noarch/r-dbplyr-2.5.0-r44hc72bb7e_1.conda#14bca827a234b44dd594c345cb9380aa +https://conda.anaconda.org/conda-forge/linux-64/r-filelock-1.0.3-r44hb1dbf0f_1.conda#a1db32be1b2d013336f4c408ae2fd94a +https://conda.anaconda.org/bioconda/noarch/bioconductor-biocfilecache-2.14.0-r44hdfd78af_0.tar.bz2#3f79b6f2157a77fce7eca6cd95082212 +https://conda.anaconda.org/conda-forge/linux-64/r-digest-0.6.37-r44h0d4f4ea_0.conda#a02d79cfe9ed0e17ca2984fad70121ff +https://conda.anaconda.org/conda-forge/linux-64/r-rappdirs-0.3.3-r44hb1dbf0f_3.conda#91e4bf38b98bbb03e6babe4537b840b7 +https://conda.anaconda.org/conda-forge/noarch/r-httr2-1.2.1-r44hc72bb7e_0.conda#44f7c9b3563348fe4f7c2ca7a15daf2f +https://conda.anaconda.org/conda-forge/noarch/r-hms-1.1.3-r44hc72bb7e_2.conda#e3f892da67e8364c23c449565eb7970b +https://conda.anaconda.org/conda-forge/noarch/r-assertthat-0.2.1-r44hc72bb7e_5.conda#9e9eee147ae329aaf3ec1cd3a1a7027c +https://conda.anaconda.org/conda-forge/noarch/r-prettyunits-1.2.0-r44hc72bb7e_1.conda#9e8e45220a8e13eb137c2ff752b7b941 +https://conda.anaconda.org/conda-forge/noarch/r-progress-1.2.3-r44hc72bb7e_1.conda#a14cff20682eb2f1a94ed9fb0646d73c +https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.8-h04c0eec_1.conda#10bcbd05e1c1c9d652fccb42b776a9fa +https://conda.anaconda.org/conda-forge/linux-64/r-xml2-1.4.0-r44hc6fd541_0.conda#a1a0f84c9ffbe227236069ff24a562c0 +https://conda.anaconda.org/bioconda/noarch/bioconductor-biomart-2.62.0-r44hdfd78af_0.tar.bz2#39cfd13060df28086f6c8e4e35ba35d8 +https://conda.anaconda.org/conda-forge/linux-64/r-matrixstats-1.5.0-r44h2b5f3a1_0.conda#ef9118c2585cea360e5e780b374a2e29 +https://conda.anaconda.org/bioconda/noarch/bioconductor-matrixgenerics-1.18.0-r44hdfd78af_0.tar.bz2#d1b86fcb6d7e4d3c9fe67817c739b5a7 +https://conda.anaconda.org/conda-forge/noarch/r-abind-1.4_5-r44hc72bb7e_1006.conda#bb3b3bb6a65a4c572ed072ca52c98a2b +https://conda.anaconda.org/conda-forge/linux-64/r-lattice-0.22_7-r44h2b5f3a1_0.conda#3d5d499e979c3e2e8314e5af04653ece +https://conda.anaconda.org/conda-forge/linux-64/r-matrix-1.7_4-r44h0e4624f_0.conda#b47f779332393213ebb99909739a65b8 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-s4arrays-1.6.0-r44h3df3fcb_1.tar.bz2#a6774527b21da1eb7a99b3839301ab57 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-sparsearray-1.6.0-r44h3df3fcb_1.tar.bz2#143242d9cf4199b8f26c2552b61d2049 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-delayedarray-0.32.0-r44h3df3fcb_1.tar.bz2#2a69a0cd9896594a301067804e65b8d0 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-genomicranges-1.58.0-r44h3df3fcb_2.tar.bz2#2200bf0109f17b793a1e0140f74e0d1a +https://conda.anaconda.org/conda-forge/linux-64/r-statmod-1.5.0-r44ha36c22a_2.conda#c430837afa5aa965f3dc841499ad4813 +https://conda.anaconda.org/bioconda/linux-64/bioconductor-limma-3.62.1-r44h15a9599_1.tar.bz2#f98d28949c8fbfcfa8e7073512d4bbee +https://conda.anaconda.org/bioconda/noarch/bioconductor-summarizedexperiment-1.36.0-r44hdfd78af_0.tar.bz2#144e795fdb25d213899e249bcb538bc4 +https://conda.anaconda.org/conda-forge/linux-64/r-data.table-1.17.8-r44h1c8cec4_0.conda#3e38239b6017a49fd35d789a74e13607 +https://conda.anaconda.org/conda-forge/noarch/r-r.methodss3-1.8.2-r44hc72bb7e_3.conda#77a8638e6efd89803446295bf52b81b8 +https://conda.anaconda.org/conda-forge/noarch/r-r.oo-1.27.1-r44hc72bb7e_0.conda#4b4237f0386dbb8761882cf54c43864f +https://conda.anaconda.org/conda-forge/noarch/r-r.utils-2.13.0-r44hc72bb7e_0.conda#0b97b7fb7400db6e236ed80f4e454154 +https://conda.anaconda.org/conda-forge/noarch/r-clipr-0.8.0-r44hc72bb7e_3.conda#a1361a4e31db3a567f1f4792281f66a6 +https://conda.anaconda.org/conda-forge/linux-64/r-tzdb-0.5.0-r44h3697838_1.conda#d9f598a55e3e347d34e035bcebca0cf6 +https://conda.anaconda.org/conda-forge/linux-64/r-vroom-1.6.5-r44h0d4f4ea_1.conda#38ab4b98e4e6d5fd67fc686e8b616fc6 +https://conda.anaconda.org/conda-forge/linux-64/r-readr-2.1.5-r44h0d4f4ea_1.conda#cf7a9a09e3825c416769dca906692a9a +https://conda.anaconda.org/conda-forge/linux-64/r-xml-3.99_0.17-r44h5bae778_2.conda#bdbd8d1e692e2b2d05a261dc7cad3a6f +https://conda.anaconda.org/conda-forge/noarch/r-rentrez-1.2.4-r44h785f33e_0.conda#95c3f6a7d23f959c8f3a8a0cb1b5d273 +https://conda.anaconda.org/conda-forge/noarch/r-selectr-0.4_2-r44hc72bb7e_4.conda#b3b0ff7cd9ae293a27126643d052cd55 +https://conda.anaconda.org/conda-forge/noarch/r-rvest-1.0.5-r44hc72bb7e_0.conda#e652da30224aa33c5806c217f584912b +https://conda.anaconda.org/bioconda/noarch/bioconductor-geoquery-2.74.0-r44hdfd78af_0.tar.bz2#edef0f2edf5e269df73ccc92ea9a5d17 +https://conda.anaconda.org/conda-forge/noarch/pip-25.2-pyh145f28c_0.conda#e7ab34d5a93e0819b62563c78635d937 +https://conda.anaconda.org/conda-forge/noarch/r-getopt-1.20.4-r44ha770c72_1.conda#dc45a69329ba168c23002d2ebd774e0c +https://conda.anaconda.org/conda-forge/noarch/r-optparse-1.7.5-r44hc72bb7e_1.conda#dcc48b3a7acea00d133eb775e0d1c7e8 diff --git a/modules/local/idmapping/gprofiler/main.nf b/modules/local/idmapping/gprofiler/main.nf index d837189e..9acf9861 100644 --- a/modules/local/idmapping/gprofiler/main.nf +++ b/modules/local/idmapping/gprofiler/main.nf @@ -1,6 +1,6 @@ process IDMAPPING_GPROFILER { - label 'process_low' + label 'process_single' tag "${meta.dataset}" diff --git a/modules/local/merge_counts/main.nf b/modules/local/merge_counts/main.nf index 8bfb4ad9..e529faac 100644 --- a/modules/local/merge_counts/main.nf +++ b/modules/local/merge_counts/main.nf @@ -1,6 +1,6 @@ process MERGE_COUNTS { - label 'process_low' + label 'process_high' conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? diff --git a/modules/local/normalisation/deseq2/main.nf b/modules/local/normalisation/deseq2/main.nf index e5e0932c..126e6c97 100644 --- a/modules/local/normalisation/deseq2/main.nf +++ b/modules/local/normalisation/deseq2/main.nf @@ -1,6 +1,6 @@ process NORMALISATION_DESEQ2 { - label 'process_low' + label 'process_single' tag "${meta.dataset}" diff --git a/modules/local/normalisation/edger/main.nf b/modules/local/normalisation/edger/main.nf index 77bec625..6150f402 100644 --- a/modules/local/normalisation/edger/main.nf +++ b/modules/local/normalisation/edger/main.nf @@ -1,6 +1,6 @@ process NORMALISATION_EDGER { - label 'process_low' + label 'process_single' tag "${meta.dataset}" diff --git a/modules/local/quantile_normalisation/main.nf b/modules/local/quantile_normalisation/main.nf index d659ec05..93cd111d 100644 --- a/modules/local/quantile_normalisation/main.nf +++ b/modules/local/quantile_normalisation/main.nf @@ -1,6 +1,6 @@ process QUANTILE_NORMALISATION { - label 'process_low' + label 'process_single' tag "${meta.dataset}" diff --git a/nextflow.config b/nextflow.config index f806afb0..fc2fd361 100644 --- a/nextflow.config +++ b/nextflow.config @@ -12,30 +12,40 @@ params { // Mandatory inputs species = null + // general options + keywords = "" + platform = null + accessions_only = false + download_only = false + // Local datasets datasets = null - // statistics - normalisation_method = 'deseq2' - quantile_normalisation_target_distribution = 'uniform' - nb_top_gene_candidates = 1000 - ks_pvalue_threshold = 0 - - // ID mapping - gene_metadata = null - gene_id_mapping_file = null - skip_gprofiler = false - // Expression atlas skip_fetch_eatlas_accessions = false - eatlas_keywords = "" eatlas_accessions = "" - eatlas_platform = null - accessions_only = false exclude_eatlas_accessions = "" eatlas_accessions_file = null exclude_eatlas_accessions_file = null + // GEO + skip_fetch_geo_accessions = false + geo_accessions = "" + exclude_geo_accessions = "" + geo_accessions_file = null + exclude_geo_accessions_file = null + + // ID mapping + gene_metadata = null + gene_id_mapping_file = null + skip_gprofiler = false + + // statistics + normalisation_method = 'deseq2' + quantile_normalisation_target_distribution = 'uniform' + nb_top_gene_candidates = 1000 + ks_pvalue_threshold = 0 + // Boilerplate options outdir = null publish_dir_mode = 'copy' @@ -204,6 +214,7 @@ profiles { test_one_accession_low_gene_count { includeConfig 'conf/test_one_accession_low_gene_count.config' } test_local_and_downloaded { includeConfig 'conf/test_local_and_downloaded.config' } test_one_rnaseq_one_microarray { includeConfig 'conf/test_one_rnaseq_one_microarray.config' } + test_eatlas_geo { includeConfig 'conf/test_eatlas_geo.config' } } // Load nf-core custom profiles from different Institutions diff --git a/nextflow_schema.json b/nextflow_schema.json index 718b2d6b..1e497504 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -36,11 +36,32 @@ "help_text": "Path to CSV file containing information about the input count datasets and their related experimental design. The dataset file should be a comma-separated file with 3 columns, and a header row. See [usage docs](https://nf-co.re/stableexpression/usage#samplesheet-input) for more information. Before running the pipeline, and for each user count dataset, you will need to create a design file with information about the samples in your experiment. Use this parameter to specify its location. Combine with --skip_fetch_eatlas_accessions if you only want to analyse your own count datasets.", "fa_icon": "fas fa-file-csv" }, - "skip_fetch_eatlas_accessions": { + "keywords": { + "type": "string", + "description": "Keywords used for selecting specific Expression Atlas / GEO accessions", + "default": "", + "fa_icon": "fas fa-highlighter", + "pattern": "([a-zA-Z,]+)", + "help_text": "Keywords (separated by commas) to use when retrieving specific experiments from Expression Atlas and / or GEO datasets. The pipeline will select all Expression Atlas experiments / GEO datasets that contain the provided keywords in their description of in one of the condition names. Example: `--keywords 'stress,flowering'`. This parameter is unused if both --skip_fetch_eatlas_accessions and --skip_fetch_geo_accessions are set." + }, + "platform": { + "type": "string", + "enum": ["rnaseq", "microarray"], + "description": "Only download from this platform", + "fa_icon": "fas fa-id-card", + "help_text": "By default, data from all platform are downloaded. If this parameter is specified, a filter is applied to get data from only one specific type of platform. This filter is only used while fetching appropriate Expression atlas / GEO accessions. It will not filter accessions provided directly by the user." + }, + "accessions_only": { "type": "boolean", - "fa_icon": "fas fa-cloud-arrow-down", - "description": "Skip fetching Expression Atlas accessions", - "help_text": "Expression Atlas accessions are automatically fetched by default. Set this parameter to use your own count datasets only." + "description": "Only get accessions from Expression Atlas / GEO and exit.", + "fa_icon": "fas fa-id-card", + "help_text": "Use this option if you only want to get Expression Atlas accessions and skip the rest of the pipeline." + }, + "download_only": { + "type": "boolean", + "description": "Only get accessions from Expression Atlas / GEO and download the selected datasets.", + "fa_icon": "fas fa-id-card", + "help_text": "Use this option if you only want to get Expression Atlas / GEO accessions, download the selected data, and skip the rest of the pipeline." }, "email": { "type": "string", @@ -61,41 +82,33 @@ "title": "Expression Atlas options", "type": "object", "fa_icon": "fas fa-book-atlas", - "description": "Options for fetching datasets from Expression Atlas.", + "description": "Options for fetching experiment data from Expression Atlas.", "properties": { - "eatlas_keywords": { - "type": "string", - "description": "Expression Atlas keywords", - "fa_icon": "fas fa-highlighter", - "pattern": "([a-zA-Z,]+)", - "help_text": "Keywords (separated by commas) to use when retrieving specific experiments from Expression Atlas. The pipeline will select all Expression Atlas experiments that contain the provided keywords in their description of in one of the condition names. Example: `--eatlas_keywords 'stress,flowering'`. This parameter is unused if --skip_fetch_eatlas_accessions is set." + "skip_fetch_eatlas_accessions": { + "type": "boolean", + "fa_icon": "fas fa-cloud-arrow-down", + "description": "Skip fetching Expression Atlas accessions", + "help_text": "Expression Atlas accessions are automatically fetched by default. Set this parameter to skip this step." }, "eatlas_accessions": { "type": "string", "pattern": "([A-Z0-9-]+,?)+", - "description": "Expression Atlas accessions", + "description": "Expression Atlas accession(s) to include", "fa_icon": "fas fa-id-card", "help_text": "Provide directly in command line Expression Atlas accession(s) (separated by commas) that you want to download. Example: `--eatlas_accessions E-MTAB-552,E-GEOD-61690`. Combine with --skip_fetch_eatlas_accessions if you want only these accessions to be used." }, - "eatlas_platform": { - "type": "string", - "enum": ["rnaseq", "microarray"], - "description": "Only download Expression Atlas experiments from this platform", - "fa_icon": "fas fa-id-card", - "help_text": "By default, data from all platform are downloaded. If this parameter is specified, a filter is applied to get data from only one specific type of platform. This filter is only used while fetching appropriate Expression atlas accessions. It will not filter accessions provided with --eatlas_accessions or --eatlas_accessions_file." - }, "eatlas_accessions_file": { "type": "string", "format": "file-path", "exists": true, - "description": "File with Expression Atlas accessions", + "description": "File containing Expression Atlas accession(s) to download", "fa_icon": "fas fa-id-card", - "help_text": "File containing Expression Atlas accession(s) that you want to download. One accession per line.Example: `--eatlas_accessions_file included_accessions.txt`. Combine with --skip_fetch_eatlas_accessions if you want only these accessions to be used." + "help_text": "File containing Expression Atlas accession(s) that you want to download. One accession per line. Example: `--eatlas_accessions_file included_accessions.txt`. Combine with --skip_fetch_eatlas_accessions if you want only these accessions to be used." }, "exclude_eatlas_accessions": { "type": "string", "pattern": "([A-Z0-9-]+,?)+", - "description": "Expression Atlas accession to exclude", + "description": "Expression Atlas accession(s) to exclude", "fa_icon": "fas fa-id-card", "help_text": "Provide directly in command line Expression Atlas accessions (separated by commas) that you want to exclude. Example: `--exclude_eatlas_accessions E-MTAB-552,E-GEOD-61690`" }, @@ -103,15 +116,53 @@ "type": "string", "format": "file-path", "exists": true, - "description": "File with Expression Atlas accessions to exclude", + "description": "File containing Expression Atlas accession(s) to exclude", "fa_icon": "fas fa-id-card", "help_text": "File containing Expression Atlas accession(s) that you want to exclude. One accession per line. Example: `--exclude_eatlas_accessions_file excluded_accessions.txt`." - }, - "accessions_only": { + } + } + }, + "geo_dataset_options": { + "title": "GEO dataset options", + "type": "object", + "fa_icon": "fas fa-book-atlas", + "description": "Options for fetching datasets from NCBI GEO.", + "properties": { + "skip_fetch_geo_accessions": { "type": "boolean", - "description": "Only get accessions from Expression Atlas and exit.", + "fa_icon": "fas fa-cloud-arrow-down", + "description": "Skip fetching GEO accessions", + "help_text": "GEO accessions are automatically fetched by default. Set this parameter to skip this step." + }, + "geo_accessions": { + "type": "string", + "pattern": "([A-Z0-9-]+,?)+", + "description": "GEO accession(s) to include", "fa_icon": "fas fa-id-card", - "help_text": "Use this option if you only want to get Expression Atlas accessions and skip the rest of the pipeline." + "help_text": "Provide directly in command line GEO series accession(s) (separated by commas) that you want to download. Example: `--geo_accessions GSE8165,GSE8161`. Combine with --skip_fetch_geo_accessions if you want only these accessions to be used." + }, + "geo_accessions_file": { + "type": "string", + "format": "file-path", + "exists": true, + "description": "File containing GEO accession(s) to download", + "fa_icon": "fas fa-id-card", + "help_text": "File containing GEO series accession(s) that you want to download. One accession per line. Example: `--geo_accessions_file included_accessions.txt`. Combine with --skip_fetch_geo_accessions if you want only these accessions to be used." + }, + "exclude_geo_accessions": { + "type": "string", + "pattern": "([A-Z0-9-]+,?)+", + "description": "GEO accession(s) to exclude", + "fa_icon": "fas fa-id-card", + "help_text": "Provide directly in command line GEO series accessions (separated by commas) that you want to exclude. Example: `--exclude_geo_accessions GSE8165,GSE8161`" + }, + "exclude_geo_accessions_file": { + "type": "string", + "format": "file-path", + "exists": true, + "description": "File containing GEO accession(s) to exclude", + "fa_icon": "fas fa-id-card", + "help_text": "File containing GEO series accession(s) that you want to exclude. One accession per line. Example: `--exclude_geo_accessions_file excluded_accessions.txt`." } } }, diff --git a/subworkflows/local/expressionatlas_fetchdata/main.nf b/subworkflows/local/expressionatlas_fetchdata/main.nf index 9c1ef0eb..d3f4786a 100644 --- a/subworkflows/local/expressionatlas_fetchdata/main.nf +++ b/subworkflows/local/expressionatlas_fetchdata/main.nf @@ -1,5 +1,8 @@ include { EXPRESSIONATLAS_GETACCESSIONS } from '../../../modules/local/expressionatlas/getaccessions' include { EXPRESSIONATLAS_GETDATA } from '../../../modules/local/expressionatlas/getdata' +include { addDatasetIdToMetadata } from '../utils_nfcore_stableexpression_pipeline' +include { groupFilesByDatasetId } from '../utils_nfcore_stableexpression_pipeline' +include { augmentToMetadata } from '../utils_nfcore_stableexpression_pipeline' /* ======================================================================================== @@ -27,15 +30,15 @@ workflow EXPRESSIONATLAS_FETCHDATA { .set { ch_input_accessions } // fetching Expression Atlas accessions if applicable - if ( !params.skip_fetch_eatlas_accessions || params.eatlas_keywords ) { + if ( !params.skip_fetch_eatlas_accessions || params.keywords ) { // getting Expression Atlas accessions given a species name and keywords // keywords can be an empty string - def eatlas_platform = params.eatlas_platform?: 'none' + def platform = params.platform?: 'none' EXPRESSIONATLAS_GETACCESSIONS( ch_species, - params.eatlas_keywords, - eatlas_platform + params.keywords, + platform ) EXPRESSIONATLAS_GETACCESSIONS.out.accessions @@ -67,6 +70,7 @@ workflow EXPRESSIONATLAS_FETCHDATA { .combine ( ch_excluded_accessions ) .filter { accession, excluded_accessions -> !(accession in excluded_accessions) } .map { accession, excluded_accessions -> accession } + .view() .set { ch_accessions } if ( !params.accessions_only ) { @@ -75,83 +79,19 @@ workflow EXPRESSIONATLAS_FETCHDATA { EXPRESSIONATLAS_GETDATA( ch_accessions ) // adding dataset id (accession + data_type) in the file meta - ch_etlas_design = addDatasetIdToMetadata( EXPRESSIONATLAS_GETDATA.out.design.flatten() ) - ch_eatlas_counts = addDatasetIdToMetadata( EXPRESSIONATLAS_GETDATA.out.counts.flatten() ) + ch_design = addDatasetIdToMetadata( EXPRESSIONATLAS_GETDATA.out.design.flatten() ) + ch_counts = addDatasetIdToMetadata( EXPRESSIONATLAS_GETDATA.out.counts.flatten() ) // adding design files to the meta of their respective count files - ch_eatlas_datasets = groupFilesByDatasetId( ch_etlas_design, ch_eatlas_counts ) + ch_datasets = groupFilesByDatasetId( ch_design, ch_counts ) // adding normalisation state in the meta - augmentToMetadata( ch_eatlas_datasets ) + augmentToMetadata( ch_datasets ) } emit: - downloaded_datasets = ch_eatlas_datasets + downloaded_datasets = ch_datasets + accessions = ch_accessions } - - - -/* -======================================================================================== - FUNCTIONS -======================================================================================== -*/ - - -// -// Get Expression Atlas Batch ID (accession + data_type) from file stem -// -def addDatasetIdToMetadata( ch_files ) { - return ch_files - .map { - file -> - def meta = [dataset: file.getSimpleName()] - [meta, file] - } -} - -// -// Groups design and data files by accession and data_type -// Design and count files have necessarily the same dataset ID (same file stem) -// -def groupFilesByDatasetId(ch_design, ch_counts) { - return ch_design - .concat( ch_counts ) // puts counts at the end of the resulting channel - .groupTuple() // groups by dataset ID; design files are necessarily BEFORE count files - .filter { - it.get(1).size() == 2 // only groups with two files - } - .filter { // only groups with first file as design file and second one as count fileWARN: java.net.ConnectException: Connexion refusée - meta, files -> - files.get(0).name.endsWith('.design.csv') && !files.get(1).name.endsWith('.design.csv') - } - .map { // putting design file in meta - meta, files -> - def new_meta = meta + [design: files[0]] - [new_meta, files[1]] - } -} - -def getNthPartFromEnd(String s, int n) { - def tokens = s.tokenize('.') - return tokens[tokens.size() - n] -} - -// -// Add normalised: true / false in meta -// -def augmentToMetadata( ch_files ) { - return ch_files - .map { - meta, file -> - if ( getNthPartFromEnd(file.name, 3) == 'raw' ) { - meta.normalised = false - } else { - meta.normalised = true - } - meta.platform = getNthPartFromEnd(file.name, 4) - [meta, file] - } -} diff --git a/subworkflows/local/geo_fetchdata/main.nf b/subworkflows/local/geo_fetchdata/main.nf new file mode 100644 index 00000000..b09d3f22 --- /dev/null +++ b/subworkflows/local/geo_fetchdata/main.nf @@ -0,0 +1,106 @@ +include { GEO_GETACCESSIONS } from '../../../modules/local/geo/getaccessions' +include { GEO_GETDATA } from '../../../modules/local/geo/getdata' +include { addDatasetIdToMetadata } from '../utils_nfcore_stableexpression_pipeline' +include { groupFilesByDatasetId } from '../utils_nfcore_stableexpression_pipeline' +include { augmentToMetadata } from '../utils_nfcore_stableexpression_pipeline' + +/* +======================================================================================== + SUBWORKFLOW TO DOWNLOAD GEO ACCESSIONS AND DATASETS +======================================================================================== +*/ + +workflow GEO_FETCHDATA { + + take: + ch_species + ch_excluded_accessions + + main: + + ch_datasets = Channel.empty() + ch_fetched_accessions = Channel.empty() + + ch_geo_accessions_file = params.geo_accessions_file ? Channel.fromPath(params.geo_accessions_file, checkIfExists: true) : Channel.empty() + + Channel.fromList( params.geo_accessions.tokenize(',') ) + .mix( ch_geo_accessions_file.splitText() ) + .unique() + .map { it -> it.trim() } + .set { ch_input_accessions } + + // fetching GEO accessions if applicable + if ( !params.skip_fetch_geo_accessions || params.keywords ) { + + ch_excluded_accessions + .collectFile( + name: 'excluded_geo_accessions.txt', + sort: true, + newLine: true + ) + .set { ch_excluded_accessions_file } + + // getting GEO accessions given a species name and keywords + // keywords can be an empty string + def platform = params.platform?: 'none' + GEO_GETACCESSIONS( + ch_species, + params.keywords, + platform, + ch_excluded_accessions_file + ) + + GEO_GETACCESSIONS.out.accessions + .splitText() + .set { ch_fetched_accessions } + + } + + ch_exclude_geo_accessions_file = params.exclude_geo_accessions_file ? Channel.fromPath(params.exclude_geo_accessions_file, checkIfExists: true) : Channel.empty() + + // getting accessions to exclude and preparing in the right format + Channel.fromList( params.exclude_geo_accessions.tokenize(',') ) + .mix( ch_exclude_geo_accessions_file.splitText() ) + .unique() + .map { it -> it.trim() } + .toList() + .map { lst -> [lst] } // list of lists : mandatory when combining in the next step + .set { ch_excluded_accessions } + + // appending to accessions provided by the user + // ensures that no accessions is present twice (provided by the user and fetched from GEO) + // removing excluded accessions + ch_input_accessions + .mix( ch_fetched_accessions ) + .unique() + .map { it -> it.trim() } + .filter { it.startsWith('GSE') } + .combine ( ch_excluded_accessions ) + .filter { accession, excluded_accessions -> !(accession in excluded_accessions) } + .map { accession, excluded_accessions -> accession } + .set { ch_accessions } + + if ( !params.accessions_only ) { + + // Downloading GEO datasets for each accession in ch_accessions + GEO_GETDATA( + ch_accessions, + ch_species + ) + + // adding dataset id (accession + data_type) in the file meta + ch_design = addDatasetIdToMetadata( GEO_GETDATA.out.design.flatten() ) + ch_counts = addDatasetIdToMetadata( GEO_GETDATA.out.counts.flatten() ) + + // adding design files to the meta of their respective count files + ch_datasets = groupFilesByDatasetId( ch_design, ch_counts ) + + // adding normalisation state in the meta + augmentToMetadata( ch_datasets ) + + } + + emit: + downloaded_datasets = ch_datasets + +} diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index 1e66b144..1a828fd5 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -292,3 +292,68 @@ def customSoftwareVersionsToYAML(versions) { +/* +======================================================================================== + FUNCTIONS FOR FORMATTING DATA FETCHED FROM EXPRESSION ATLAS / GEO +======================================================================================== +*/ + +// +// Get Expression Atlas Batch ID (accession + data_type) from file stem +// +def addDatasetIdToMetadata( ch_files ) { + return ch_files + .map { + file -> + def meta = [dataset: file.getSimpleName()] + [meta, file] + } +} + +// +// Groups design and data files by accession and data_type +// Design and count files have necessarily the same dataset ID (same file stem) +// +def groupFilesByDatasetId(ch_design, ch_counts) { + return ch_design + .concat( ch_counts ) // puts counts at the end of the resulting channel + .groupTuple() // groups by dataset ID; design files are necessarily BEFORE count files + .filter { + it.get(1).size() == 2 // only groups with two files + } + .filter { // only groups with first file as design file and second one as count fileWARN: java.net.ConnectException: Connexion refusée + meta, files -> + files.get(0).name.endsWith('.design.csv') && !files.get(1).name.endsWith('.design.csv') + } + .map { // putting design file in meta + meta, files -> + def new_meta = meta + [design: files[0]] + [new_meta, files[1]] + } +} + +def getNthPartFromEnd(String s, int n) { + def tokens = s.tokenize('.') + return tokens[tokens.size() - n] +} + +// +// Add normalised: true / false in meta +// +def augmentToMetadata( ch_files ) { + return ch_files + .map { + meta, file -> + if ( getNthPartFromEnd(file.name, 3) == 'raw' ) { + meta.normalised = false + } else { + meta.normalised = true + } + meta.platform = getNthPartFromEnd(file.name, 4) + [meta, file] + } +} + + + + diff --git a/tests/modules/local/geo/getaccessions/main.nf.test b/tests/modules/local/geo/getaccessions/main.nf.test new file mode 100644 index 00000000..7f82ea5d --- /dev/null +++ b/tests/modules/local/geo/getaccessions/main.nf.test @@ -0,0 +1,28 @@ +nextflow_process { + + name "Test Process GEO_GETACCESSIONS" + script "modules/local/geo/getaccessions/main.nf" + process "GEO_GETACCESSIONS" + tag "geo_getaccession" + + test("Beta vulgaris") { + + when { + process { + """ + input[0] = "beta_vulgaris" + input[1] = "" + input[2] = "none" + input[3] = file( '$projectDir/tests/test_data/geo/get_accessions/exclude_no_accessions.txt', checkIfExists: true ) + """ + } + } + + then { + assert process.success + assert snapshot(process.out).match() + } + + } + +} diff --git a/tests/modules/local/geo/getaccessions/main.nf.test.snap b/tests/modules/local/geo/getaccessions/main.nf.test.snap new file mode 100644 index 00000000..8e176166 --- /dev/null +++ b/tests/modules/local/geo/getaccessions/main.nf.test.snap @@ -0,0 +1,87 @@ +{ + "Beta vulgaris": { + "content": [ + { + "0": [ + "accessions.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ], + "1": [ + [ + "filtered_datasets.metadata.tsv:md5,3aab7ebb4309c0839514fb3115466569", + "rejected_datasets.metadata.tsv:md5,a39a681edb1a293ccd687b7fa84290a9", + "selected_datasets.metadata.tsv:md5,3aab7ebb4309c0839514fb3115466569", + "species_datasets.metadata.tsv:md5,360fddb142ccd3a44aac82ec262ccc87" + ] + ], + "2": [ + "selected_datasets.keywords.yaml:md5,2eff480907cde020adb64a6da3e1b007" + ], + "3": [ + [ + "GEO_GETACCESSIONS", + "python", + "3.13.7" + ] + ], + "4": [ + [ + "GEO_GETACCESSIONS", + "requests", + "2.32.5" + ] + ], + "5": [ + [ + "GEO_GETACCESSIONS", + "nltk", + "3.9.1" + ] + ], + "6": [ + [ + "GEO_GETACCESSIONS", + "pyyaml", + "6.0.2" + ] + ], + "7": [ + [ + "GEO_GETACCESSIONS", + "pandas", + "2.3.2" + ] + ], + "8": [ + [ + "GEO_GETACCESSIONS", + "xmltodict", + "0.14.2" + ] + ], + "9": [ + [ + "GEO_GETACCESSIONS", + "biopython", + "1.85" + ] + ], + "accessions": [ + "accessions.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ], + "metadata": [ + [ + "filtered_datasets.metadata.tsv:md5,3aab7ebb4309c0839514fb3115466569", + "rejected_datasets.metadata.tsv:md5,a39a681edb1a293ccd687b7fa84290a9", + "selected_datasets.metadata.tsv:md5,3aab7ebb4309c0839514fb3115466569", + "species_datasets.metadata.tsv:md5,360fddb142ccd3a44aac82ec262ccc87" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.6" + }, + "timestamp": "2025-09-11T08:57:04.392847619" + } +} \ No newline at end of file diff --git a/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test b/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test index 529a339a..68b8c220 100644 --- a/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test +++ b/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test @@ -13,13 +13,13 @@ nextflow_workflow { """ species = 'solanum tuberosum' eatlas_accessions = "E-MTAB-552,E-GEOD-61690 ,E-PROT-138" - eatlas_keywords = "potato,stress" + keywords = "potato,stress" skip_fetch_eatlas_accessions = false accessions_only = false input[0] = Channel.value( species.split(' ').join('_') ) input[1] = eatlas_accessions - input[2] = eatlas_keywords + input[2] = keywords input[3] = fetch_eatlas_accessions """ } @@ -41,13 +41,13 @@ nextflow_workflow { """ species = 'solanum tuberosum' eatlas_accessions = "E-MTAB-552,E-GEOD-61690 ,E-PROT-138" - eatlas_keywords = "potato,stress" + keywords = "potato,stress" skip_fetch_eatlas_accessions = false accessions_only = true input[0] = Channel.value( species.split(' ').join('_') ) input[1] = eatlas_accessions - input[2] = eatlas_keywords + input[2] = keywords input[3] = fetch_eatlas_accessions """ } @@ -73,7 +73,7 @@ nextflow_workflow { input[0] = Channel.value( species.split(' ').join('_') ) input[1] = eatlas_accessions - input[2] = eatlas_keywords + input[2] = keywords input[3] = fetch_eatlas_accessions """ } diff --git a/tests/test_data/geo/get_accessions/exclude_no_accessions.txt b/tests/test_data/geo/get_accessions/exclude_no_accessions.txt new file mode 100644 index 00000000..e69de29b diff --git a/tests/workflows/stableexpression.nf.test b/tests/workflows/stableexpression.nf.test index 11ab6ce9..9354c5b0 100644 --- a/tests/workflows/stableexpression.nf.test +++ b/tests/workflows/stableexpression.nf.test @@ -67,7 +67,7 @@ nextflow_workflow { when { params { species = "beta vulgaris" - eatlas_keywords = "potato,stress" + keywords = "potato,stress" } workflow { """ @@ -123,7 +123,7 @@ nextflow_workflow { params { species = "solanum tuberosum" eatlas_accessions = "E-MTAB-552,E-GEOD-61690" - eatlas_keywords = "phloem" + keywords = "phloem" } workflow { """ diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 689f7391..140cec0a 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -5,6 +5,7 @@ */ include { EXPRESSIONATLAS_FETCHDATA } from '../subworkflows/local/expressionatlas_fetchdata' +include { GEO_FETCHDATA } from '../subworkflows/local/geo_fetchdata' include { IDMAPPING } from '../subworkflows/local/idmapping' include { EXPRESSION_NORMALISATION } from '../subworkflows/local/expression_normalisation' include { DATA_CLEANSING } from '../subworkflows/local/data_cleansing' @@ -40,11 +41,24 @@ workflow STABLEEXPRESSION { EXPRESSIONATLAS_FETCHDATA( ch_species ) - if ( !params.accessions_only ) { + // getting accessions to exclude from GEO + EXPRESSIONATLAS_FETCHDATA.out.accessions + .filter { accession -> accession.startsWith("E-GEOD-") } + .map { accession -> accession.replace("E-GEOD-", "GSE")} + .view() + .set { ch_excluded_geo_accessions } + + GEO_FETCHDATA ( + ch_species, + ch_excluded_geo_accessions + ) + + if ( !params.accessions_only && !params.download_only ) { // putting all datasets together (local datasets + Expression Atlas datasets) ch_input_datasets .concat( EXPRESSIONATLAS_FETCHDATA.out.downloaded_datasets ) + .concat( GEO_FETCHDATA.out.downloaded_datasets ) .set { ch_datasets } // ----------------------------------------------------------------- From deb965e43619ebb302316b4db30ab6be415f791c Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Thu, 11 Sep 2025 12:15:44 +0200 Subject: [PATCH 053/258] now mandatory to give design file for each coutn dataset --- assets/schema_datasets.json | 2 +- bin/normalise_with_deseq2.R | 25 +++++++-------- bin/normalise_with_edger.R | 28 +++++++--------- modules/local/merge/designs/spec-file.txt | 39 +++++++++++++++++++++++ 4 files changed, 62 insertions(+), 32 deletions(-) create mode 100644 modules/local/merge/designs/spec-file.txt diff --git a/assets/schema_datasets.json b/assets/schema_datasets.json index 5995a1c2..3f43549d 100644 --- a/assets/schema_datasets.json +++ b/assets/schema_datasets.json @@ -35,6 +35,6 @@ "meta": ["normalised"] } }, - "required": ["counts", "platform", "normalised"] + "required": ["counts", "design", "platform", "normalised"] } } diff --git a/bin/normalise_with_deseq2.R b/bin/normalise_with_deseq2.R index 20a7db2c..54c6670a 100755 --- a/bin/normalise_with_deseq2.R +++ b/bin/normalise_with_deseq2.R @@ -99,18 +99,10 @@ get_normalised_cpm_counts <- function(count_file, design_file) { # we do not consider these columns count_matrix <- remove_all_zero_columns(count_matrix) - if ( is.null(design_file) ) { - # faking a design table - design_data <- data.frame( - sample = colnames(count_matrix), - condition = rep("A", ncol(count_matrix)) - ) - } else { - # getting design data - design_data <- read.csv(design_file) - # removing extra samples in design table - design_data <- design_data[design_data$sample %in% colnames(count_matrix), ] - } + # getting design data + design_data <- read.csv(design_file) + # removing extra samples in design table + design_data <- design_data[design_data$sample %in% colnames(count_matrix), ] # check if the column names of count_matrix match the sample names check_samples(count_matrix, design_data) @@ -155,7 +147,7 @@ get_normalised_cpm_counts <- function(count_file, design_file) { export_data <- function(cpm_counts, filename) { filename <- sub("\\.csv$", ".cpm.csv", filename) - cat(paste('Exporting normalised counts per million to:', filename, "\n")) + message(paste('Exporting normalised counts per million to:', filename)) write.table(cpm_counts, filename, sep = ',', row.names = TRUE, col.names = NA, quote = FALSE) } @@ -167,7 +159,12 @@ export_data <- function(cpm_counts, filename) { args <- get_args() -cat(paste("Normalising counts in", args$count_file, "\n")) +if ( is.null(args$design_file) ) { + message("A design dataframe must be provided.") + quit(save = "no", status = 1) +} + +message(paste("Normalising counts in", args$count_file)) cpm_counts <- get_normalised_cpm_counts(args$count_file, args$design_file) export_data(cpm_counts, basename(args$count_file)) diff --git a/bin/normalise_with_edger.R b/bin/normalise_with_edger.R index e9d02cad..84b4b24f 100755 --- a/bin/normalise_with_edger.R +++ b/bin/normalise_with_edger.R @@ -75,7 +75,7 @@ get_cpm_counts <- function(dge) { get_normalised_cpm_counts <- function(count_file, design_file) { - print(paste('Normalizing counts in:', count_file)) + message(paste('Normalizing counts in:', count_file)) count_data <- read.csv(args$count_file, row.names = 1) @@ -84,21 +84,10 @@ get_normalised_cpm_counts <- function(count_file, design_file) { # we do not consider these columns count_matrix <- remove_all_zero_columns(count_matrix) - if ( is.null(design_file) ) { - - # faking a design table - design_data <- data.frame( - sample = colnames(count_matrix), - condition = rep("A", ncol(count_matrix)) - ) - - } else { - - # getting design data - design_data <- read.csv(design_file) - # removing extra samples in design table - design_data <- design_data[design_data$sample %in% colnames(count_matrix), ] - } + # getting design data + design_data <- read.csv(design_file) + # removing extra samples in design table + design_data <- design_data[design_data$sample %in% colnames(count_matrix), ] # check if the column names of count_matrix match the sample names check_samples(count_matrix, design_data) @@ -132,7 +121,7 @@ get_normalised_cpm_counts <- function(count_file, design_file) { export_data <- function(cpm_counts, filename) { filename <- sub("\\.csv$", ".cpm.csv", filename) - print(paste('Exporting normalised counts per million to:', filename)) + message(paste('Exporting normalised counts per million to:', filename)) write.table(cpm_counts, filename, sep = ',', row.names = TRUE, col.names = NA, quote = FALSE) } @@ -144,6 +133,11 @@ export_data <- function(cpm_counts, filename) { args <- get_args() +if ( is.null(args$design_file) ) { + message("A design dataframe must be provided.") + quit(save = "no", status = 1) +} + cpm_counts <- get_normalised_cpm_counts(args$count_file, args$design_file) export_data(cpm_counts, basename(args$count_file)) diff --git a/modules/local/merge/designs/spec-file.txt b/modules/local/merge/designs/spec-file.txt new file mode 100644 index 00000000..51228f95 --- /dev/null +++ b/modules/local/merge/designs/spec-file.txt @@ -0,0 +1,39 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_5.conda#dcd5ff1940cd38f6df777cac86819d60 +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_5.conda#264fbfba7fb20acf3b29cde153e345ce +https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda#74784ee3d225fc3dca89edb635b4e5cc +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda#0be7c6e070c19105f966d3758448d018 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_5.conda#fbd4008644add05032b6764807ee2cba +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_5.conda#0c91408b3dec0b97e8a3c694845bd63b +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_2.conda#dfc5aae7b043d9f56ba99514d5e60625 +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-35_h4a7cf45_openblas.conda#6da7e852c812a84096b68158574398d0 +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-35_h0358290_openblas.conda#8aa3389d36791ecd31602a247b1f3641 +https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda#4211416ecba1866fab0c6470986c22d6 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-35_h47877c9_openblas.conda#aa0b36b71d44f74686f13b9bfabec891 +https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc +https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda#0b367fad34931cb79e0d6b7e5c06bb1c +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_5.conda#4e02a49aaa9d5190cb630fa43528fbe6 +https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.1-he9a06e4_0.conda#af930c65e9a79a3423d6d36e265cef65 +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda#ffffb341206dd0dab0c36053c048d621 +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 +https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 +https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 +https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a +https://conda.anaconda.org/conda-forge/linux-64/python-3.13.7-h2b335a9_100_cp313.conda#724dcf9960e933838247971da07fe5cf +https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.3-py313hf6604e3_0.conda#3122d20dc438287e125fb5acff1df170 +https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda#3339e3b65d58accf4ca4fb8748ab16b3 +https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 +https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 +https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 +https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.2-py313h08cd8bf_0.conda#5f4cc42e08d6d862b7b919a3c8959e0b +https://conda.anaconda.org/conda-forge/noarch/pip-25.2-pyh145f28c_0.conda#e7ab34d5a93e0819b62563c78635d937 From 63938ee1ab45fec503e0fc6c7e6241a1de1ef8d6 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sat, 13 Sep 2025 01:31:32 +0200 Subject: [PATCH 054/258] refactoring and first functional version of the normfinder script --- bin/compute_final_gene_statistics.py | 6 +- bin/download_geo_data.R | 14 +- bin/gprofiler_utils.py | 1 - bin/merge_designs.py | 65 ++++ bin/normfinder.py | 318 ++++++++++++++++++ .../{merge_counts => merge/counts}/main.nf | 0 .../counts}/spec-file.txt | 0 modules/local/merge/designs/main.nf | 24 ++ .../local/expressionatlas_fetchdata/main.nf | 1 - .../local/merge_compute_stats/main.nf | 12 +- .../merge_compute_stats_per_platform/main.nf | 2 +- .../global/main.nf.test | 33 ++ .../global}/main.nf.test.snap | 28 +- .../local/gene_statistics/main.nf.test | 38 --- .../compute_gene_statistics/input/design.csv | 28 ++ .../input/gene_counts.csv | 0 .../input/ks_stats.csv | 0 .../input/mapping1.csv | 0 .../input/mapping2.csv | 0 .../input/mapping3.csv | 0 .../input/metadata1.csv | 0 .../input/metadata2.csv | 0 .../input/microarray_stats_all_genes.csv | 8 + .../input/rnaseq_stats_all_genes.csv | 8 + .../normfinder/all_counts.normalised.parquet | Bin 0 -> 2553 bytes .../normfinder/all_counts.normfinder.csv | 6 + .../normfinder/convert_to_parquet.py | 4 + tests/test_data/normfinder/design.csv | 7 + tests/test_data/normfinder/normfinder.R | 298 ++++++++++++++++ workflows/stableexpression.nf | 1 - 30 files changed, 834 insertions(+), 68 deletions(-) create mode 100755 bin/merge_designs.py create mode 100755 bin/normfinder.py rename modules/local/{merge_counts => merge/counts}/main.nf (100%) rename modules/local/{merge_counts => merge/counts}/spec-file.txt (100%) create mode 100644 modules/local/merge/designs/main.nf create mode 100644 tests/modules/local/compute_gene_statistics/global/main.nf.test rename tests/modules/local/{gene_statistics => compute_gene_statistics/global}/main.nf.test.snap (51%) delete mode 100644 tests/modules/local/gene_statistics/main.nf.test create mode 100644 tests/test_data/compute_gene_statistics/input/design.csv rename tests/test_data/{gene_statistics => compute_gene_statistics}/input/gene_counts.csv (100%) rename tests/test_data/{gene_statistics => compute_gene_statistics}/input/ks_stats.csv (100%) rename tests/test_data/{gene_statistics => compute_gene_statistics}/input/mapping1.csv (100%) rename tests/test_data/{gene_statistics => compute_gene_statistics}/input/mapping2.csv (100%) rename tests/test_data/{gene_statistics => compute_gene_statistics}/input/mapping3.csv (100%) rename tests/test_data/{gene_statistics => compute_gene_statistics}/input/metadata1.csv (100%) rename tests/test_data/{gene_statistics => compute_gene_statistics}/input/metadata2.csv (100%) create mode 100644 tests/test_data/compute_gene_statistics/input/microarray_stats_all_genes.csv create mode 100644 tests/test_data/compute_gene_statistics/input/rnaseq_stats_all_genes.csv create mode 100644 tests/test_data/normfinder/all_counts.normalised.parquet create mode 100644 tests/test_data/normfinder/all_counts.normfinder.csv create mode 100644 tests/test_data/normfinder/convert_to_parquet.py create mode 100644 tests/test_data/normfinder/design.csv create mode 100644 tests/test_data/normfinder/normfinder.R diff --git a/bin/compute_final_gene_statistics.py b/bin/compute_final_gene_statistics.py index 49e782bc..aad31c79 100755 --- a/bin/compute_final_gene_statistics.py +++ b/bin/compute_final_gene_statistics.py @@ -114,7 +114,11 @@ def parse_args(): help="Metadata file", ) parser.add_argument( - "--mappings", type=str, dest="mapping_files", required=True, help="Mapping file" + "--mappings", + type=str, + dest="mapping_files", + required=True, + help="Mapping file" ) parser.add_argument( "--nb-top-stable-genes", diff --git a/bin/download_geo_data.R b/bin/download_geo_data.R index 0b5cb337..e9036655 100755 --- a/bin/download_geo_data.R +++ b/bin/download_geo_data.R @@ -84,11 +84,11 @@ build_design_dataframe <- function(df, accession) { mutate(group_num = cur_group_id()) %>% ungroup() %>% mutate( - group = paste0("G", group_num), + condition = paste0("G", group_num), batch = accession ) %>% - select(sample, group, batch) %>% - arrange(group) + select(sample, condition, batch) %>% + arrange(condition) return(design_df) } @@ -139,13 +139,13 @@ check_microarray_normalisation <- function(df) { message("Normalized, log2 scale (e.g. RMA, quantile)") } else if (all_integers) { message("Raw probe intensities (unnormalized CEL-like data)") - quit(save = "no", status = 110) + #quit(save = "no", status = 110) } else if (value_range[2] > 1000) { message("Normalized but not log-transformed (e.g. MAS5, raw intensities)") - quit(save = "no", status = 111) + #quit(save = "no", status = 111) } else { message("Unclear data origin, check GEO metadata") - quit(save = "no", status = 112) + #quit(save = "no", status = 112) } } @@ -221,7 +221,7 @@ export_metadata <- function(design_df, batch_id) { df <- design_df %>% mutate(sample = new_sample_names ) %>% - select(sample, group, batch) + select(sample, condition, batch) outfilename <- paste0(batch_id, '.design.csv') message(paste('Exporting design data to file', outfilename)) diff --git a/bin/gprofiler_utils.py b/bin/gprofiler_utils.py index de97c926..05d1fefd 100755 --- a/bin/gprofiler_utils.py +++ b/bin/gprofiler_utils.py @@ -129,7 +129,6 @@ def request_conversion( logger.error(f"Error {err.response.status_code} while converting IDs: {err}") sys.exit(101) - if server_appears_down: if attempts == 0: logger.warning("g:Profiler main server appears down, trying with the beta server...") diff --git a/bin/merge_designs.py b/bin/merge_designs.py new file mode 100755 index 00000000..1bb124a1 --- /dev/null +++ b/bin/merge_designs.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 + +# Written by Olivier Coen. Released under the MIT license. + +import argparse +import pandas as pd +from pathlib import Path +import logging + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +OUTFILENAME = "whole_design.csv" + + +##################################################### +##################################################### +# FUNCTIONS +##################################################### +##################################################### + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Merge count datasets" + ) + parser.add_argument( + "--designs", type=str, dest="design_files", required=True, help="Design files" + ) + return parser.parse_args() + + +def merge_designs(design_files): + dfs = [pd.read_csv(file) for file in design_files] + return pd.concat(dfs, ignore_index=True) + + +##################################################### +# EXPORT +##################################################### + + +def export_data(design_df: pd.DataFrame ): + logger.info(f"Exporting normalised counts to: {OUTFILENAME}") + design_df.to_csv(OUTFILENAME, index=False, header=True) + + +##################################################### +##################################################### +# MAIN +##################################################### +##################################################### + + +def main(): + args = parse_args() + design_files = [Path(file) for file in args.design_files.split(" ")] + + # putting all designs into a single dataframe + design_df = merge_designs(design_files) + export_data(design_df) + + +if __name__ == "__main__": + main() diff --git a/bin/normfinder.py b/bin/normfinder.py new file mode 100755 index 00000000..1270ce8c --- /dev/null +++ b/bin/normfinder.py @@ -0,0 +1,318 @@ +import polars as pl +import sys +from tqdm import tqdm +from statistics import mean + +ENSEMBL_GENE_ID_COLNAME = "ensembl_gene_id" + + +count_file = sys.argv[1] +design_file = sys.argv[2] + +count_lf = pl.scan_parquet(count_file) +design_df = pl.read_csv(design_file) + +design_df = ( + design_df + .with_columns(pl.concat_str([pl.col('batch'), pl.col('condition')], separator="_").alias("group")) + .select("sample", "group") +) + +group_to_sample_df = ( + design_df + .group_by("group", maintain_order=True) # maintain order is better for repeatability and testing + .agg("sample") +) + +group_to_samples_dict = { + d["group"]: d["sample"] + for d in group_to_sample_df.to_dicts() +} +del group_to_sample_df + +groups = list(group_to_samples_dict.keys()) +n_groups = len(groups) + +genes = count_lf.select(ENSEMBL_GENE_ID_COLNAME).collect().to_series().to_list() +k = len(genes) +renaming_dict = { f"column_{i}": gene for i, gene in enumerate(genes) } + +if k <= 2: + raise ValueError("Too few genes") + +def get_overall_mean_for_group(df_with_means_over_samples): + return df_with_means_over_samples.mean().item() + + +def get_means_over_samples(df): + return ( + df + .with_columns( + mean_over_samples_for_gene=pl.concat_list(pl.all()).list.drop_nulls().list.mean() + ) + .select("mean_over_samples_for_gene") + ) + + +def compute_minvar(df, target_gene) -> float: + return ( + df + .select((pl.col(target_gene) - pl.col(col)).alias(col) for col in genes if col != target_gene) # makes all pairwise differences with other genes + .var(ddof=1) # computes variance + .select( + ( pl.concat_list(pl.all()).list.drop_nulls().list.min() / 4 ).alias("min") # get min of variances and divides by 4 + ) + .item() + ) + + +def get_unbiased_intragroup_variance(df, means_over_samples_df, group_overall_mean, samples): + + # TODO: see if it's correct + # if only one sample in the group, there's no variance + if len(samples) == 1: + data = { gene: [0] for gene in genes } + return pl.DataFrame(data) + + # lf is a lazyframe with a column being the gene ids (ensembl_gene_id) + # and other columns being the samples + # the current chunk corresponds to only one group + # means_over_samples_df is a single column dataframe containing the means across each row (ie for each gene across samples) + ng = len(samples) + + means_over_samples = means_over_samples_df.to_series().rename("mean_over_samples_for_gene") + + mean_over_genes = df.mean().transpose().to_series().rename("mean_over_genes_for_sample") + + sample_variance_df = ( + df + .hstack([means_over_samples]) # adding column containing means over all samples in this group (for each gene) + .select([ + (pl.col(c) - pl.col("mean_over_samples_for_gene")).alias(c) # y_igj - mean(y_ig*) + for c in samples + ]) + .transpose(include_header=True, column_names=genes) # columns are now genes + .hstack([mean_over_genes]) # adding column containing means over all genes (for each sample) + .select([ + ((pl.col(c) - pl.col("mean_over_genes_for_sample") + group_overall_mean) ** 2).alias(c) # r_igj ^2 = (y_igj - mean(y_ig*) -mean(y_*gj) + mean(y_*g*) ) ^ 2 + for c in genes + ]) + .transpose(include_header=True, column_names=samples) + .with_columns( + sample_variance=pl.concat_list(samples).list.drop_nulls().list.sum() / ((ng - 1) * (1 - 2 / k)) # sum over j (samples) of r_igj ^2 terms + ) + .select("sample_variance") + .transpose() + .rename(renaming_dict) + ) + + # sum of all sample variances for all genes + sample_variance_sum_over_genes = sample_variance_df.select(pl.sum_horizontal(pl.all())).item() # sum of all s_ij² over all genes + + intra_var_df = ( + sample_variance_df + .select([ + ( pl.col(c) - sample_variance_sum_over_genes / (k * (k -1)) ).alias(c) + for c in genes + ]) + ) + + # if some values are negative, we need a special process + genes_with_negative_values = ( + intra_var_df + .select(col for col in genes if (intra_var_df[col] < 0).all()) # intra_var_df has only one row but it is a dataframe + .columns + ) + + minvar = {} + if genes_with_negative_values: + transposed_df = df.transpose(include_header=True, column_names=genes).select(genes) + for gene in genes_with_negative_values: + minvar[gene] = compute_minvar(transposed_df, gene) + + return ( + intra_var_df + .with_columns([ + pl.lit(val).alias(col) for col, val in minvar.items() + ]) + ) + + +def adjust_for_nb_of_samples_in_groups(unbiased_intragroup_variance_df, n_samples_list): + return ( + unbiased_intragroup_variance_df + .with_columns( + n_samples=pl.Series(n_samples_list) + ) + .select([ + (pl.col(c) / pl.col("n_samples")).alias(c) + for c in genes + ]) + ) + + +def get_unbiased_intergroup_variance(gene_means_in_groups_df, dataset_overall_mean): + mean_over_genes = gene_means_in_groups_df.mean().transpose().to_series().rename("mean_over_genes_for_group") + + return ( + gene_means_in_groups_df + .with_columns( + mean_over_groups_for_gene=pl.concat_list(pl.all()).list.drop_nulls().list.mean() + ) + .select([ + (pl.col(c) - pl.col("mean_over_groups_for_gene")).alias(c) + for c in gene_means_in_groups_df.columns + ]) + .transpose(column_names=genes) + .hstack([mean_over_genes]) + .select([ + (pl.col(c) - pl.col("mean_over_genes_for_group") + dataset_overall_mean).alias(c) + for c in genes + ]) + .select([ (pl.col(c) ** 2).alias(c) for c in genes ]) # square to get variance + ) + + +def compute_gamma_factor(diff_df, vardiff_df): + first_term = ( + diff_df + .with_columns( + sum_of_squares=pl.concat_list(pl.all()).list.drop_nulls().list.sum() # sum over columns + ) + .select("sum_of_squares") + .sum() # sum over rows + .select( + ( pl.col("sum_of_squares") / ((n_groups - 1) * (k - 1)) ).alias("normalised_sum_of_squares") + + ) + .item() + ) + + second_term = ( + vardiff_df + .with_columns( + sum=pl.concat_list(pl.all()).list.drop_nulls().list.sum() # sum over columns + ) + .select("sum") + .sum() # sum over rows + .select( + (pl.col("sum") / (n_groups* k)).alias("normalised_sum") + + ) + .item() + ) + + return max(first_term - second_term, 0) + + +def apply_gamma_factor(gamma, diff_df, vardiff_df): + difnew = diff_df * gamma / (gamma + vardiff_df) + varnew = vardiff_df + gamma * vardiff_df / (gamma + vardiff_df) + return difnew, varnew + + +def get_stability_values(unbiased_intragroup_variance_dfs, means_over_samples_dfs, group_overall_means): + + dataset_overall_mean = mean(group_overall_means) + + # putting together all intragroup variances + unbiased_intragroup_variance_df = pl.concat(unbiased_intragroup_variance_dfs) + + group_mean_variance_df = adjust_for_nb_of_samples_in_groups(unbiased_intragroup_variance_df, n_samples_list) + + # putting together all means over samples for each group + gene_means_in_groups_df = pl.concat(means_over_samples_dfs, how="horizontal") + # adding mean over genes for each group (no need to compute it again) + # gene_means_in_groups_df = pl.concat([gene_means_in_groups_df, group_overall_mean_df]) + + intergroup_variance_df = get_unbiased_intergroup_variance(gene_means_in_groups_df, dataset_overall_mean) + + gamma = compute_gamma_factor(intergroup_variance_df, group_mean_variance_df) + + shrunk_intergroup_variance_df, shrunk_group_mean_variance_df = apply_gamma_factor(gamma, intergroup_variance_df, group_mean_variance_df) + + return ( + ( + shrunk_intergroup_variance_df.select([pl.col(c).abs() for c in genes]) + + shrunk_group_mean_variance_df.select([pl.col(c).sqrt() for c in genes]) + ) + .mean() + ) + + +unbiased_intragroup_variance_dfs = [] +means_over_samples_dfs = [] +group_overall_means = [] +n_samples_list = [] +cpt=0 +for group, samples in tqdm(group_to_samples_dict.items()): + cpt+=1 + if cpt > 30: + break + chunk_df = count_lf.select(samples).collect() + means_over_samples_df = get_means_over_samples(chunk_df) + group_overall_mean = get_overall_mean_for_group(means_over_samples_df) + unbiased_intragroup_variance_df = get_unbiased_intragroup_variance(chunk_df, means_over_samples_df, group_overall_mean, samples) + # storing intragroup values for each gene in this group + unbiased_intragroup_variance_dfs.append(unbiased_intragroup_variance_df) + # storing means over samples in this group for each gene + means_over_samples_df = means_over_samples_df.rename({"mean_over_samples_for_gene": group}) + means_over_samples_dfs.append(means_over_samples_df) + # storing overall mean of expression in this group, for all genes and samples + group_overall_means.append(group_overall_mean) + # storing nb of samples in this group + n_samples_list.append(len(samples)) + + + + + + +stab = get_stability_values(unbiased_intragroup_variance_dfs, means_over_samples_dfs, group_overall_means) +#print(stab) + + + + + +""" +import time +s_squared = {} +overall_s_squared_factor = {} +for group, samples in tqdm(batch_condition_to_sample_dict.items()): + overall_group_mean = batch_condition_mean_dict[group] + nb_samples = len(samples) + s_squared[group] = {} + + for gene in tqdm(genes): + sample_ys = { + k: v + for k, v in lf.filter(pl.col(ENSEMBL_GENE_ID_COLNAME) == gene).select(samples).collect().to_dicts()[0].items() + if v is not None + } + if not sample_ys: + s_squared[group][gene] = None + continue + mean_over_samples_for_gene = mean(sample_ys.values()) + sample_r_squares = [] + for sample in sample_ys: + r_square = sample_ys[sample] - mean_over_samples_for_gene - sample_mean_dict[sample] + overall_group_mean + sample_r_squares.append(r_square) + factor = nb_samples - 1 if nb_samples > 1 else 1 + s_squared[group][gene] = sum(sample_r_squares) / ( factor * (1 - 2 / nb_genes)) + + # sum of s squared for all genes + overall_s_squared_factor[group] = sum(s_squared[group].values()) + +sigma_squared = {} +for group, samples in batch_condition_to_sample_dict.items(): + sigma_squared[group] = {} + s_squared_factor = overall_s_squared_factor[group] + for gene in genes: + if gene not in s_squared[group]: + sigma_squared[group][gene] = None + continue + sigma_squared[group][gene] = s_squared[group][gene] - s_squared_factor / nb_genes * (nb_genes - 1) +""" + + diff --git a/modules/local/merge_counts/main.nf b/modules/local/merge/counts/main.nf similarity index 100% rename from modules/local/merge_counts/main.nf rename to modules/local/merge/counts/main.nf diff --git a/modules/local/merge_counts/spec-file.txt b/modules/local/merge/counts/spec-file.txt similarity index 100% rename from modules/local/merge_counts/spec-file.txt rename to modules/local/merge/counts/spec-file.txt diff --git a/modules/local/merge/designs/main.nf b/modules/local/merge/designs/main.nf new file mode 100644 index 00000000..be55ff5b --- /dev/null +++ b/modules/local/merge/designs/main.nf @@ -0,0 +1,24 @@ +process MERGE_DESIGNS { + + label 'process_high' + + conda "${moduleDir}/spec-file.txt" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/f1/f1c30725ef181337de8749d5b54eacb1a8e1f97ac5e43fe15ec34a61789a7320/data': + 'community.wave.seqera.io/library/pandas:2.3.2--baef3004955c4a32' }" + + input: + path design_files, stageAs: "?/*" + + output: + path 'whole_design.csv', emit: design + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions + tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions + + script: + """ + merge_designs.py \\ + --designs "$design_files" + """ + +} diff --git a/subworkflows/local/expressionatlas_fetchdata/main.nf b/subworkflows/local/expressionatlas_fetchdata/main.nf index d3f4786a..88eb73af 100644 --- a/subworkflows/local/expressionatlas_fetchdata/main.nf +++ b/subworkflows/local/expressionatlas_fetchdata/main.nf @@ -70,7 +70,6 @@ workflow EXPRESSIONATLAS_FETCHDATA { .combine ( ch_excluded_accessions ) .filter { accession, excluded_accessions -> !(accession in excluded_accessions) } .map { accession, excluded_accessions -> accession } - .view() .set { ch_accessions } if ( !params.accessions_only ) { diff --git a/subworkflows/local/merge_compute_stats/main.nf b/subworkflows/local/merge_compute_stats/main.nf index 7aa37555..7823f893 100644 --- a/subworkflows/local/merge_compute_stats/main.nf +++ b/subworkflows/local/merge_compute_stats/main.nf @@ -1,4 +1,5 @@ -include { MERGE_COUNTS } from '../../../modules/local/merge_counts' +include { MERGE_COUNTS } from '../../../modules/local/merge/counts' +include { MERGE_DESIGNS } from '../../../modules/local/merge/designs' include { COMPUTE_GLOBAL_GENE_STATISTICS } from '../../../modules/local/compute_gene_statistics/global' include { MERGE_COMPUTE_STATS_PER_PLATFORM as MERGE_COMPUTE_STATS_MICROARRAY } from '../merge_compute_stats_per_platform' @@ -40,6 +41,15 @@ workflow MERGE_COMPUTE_STATS { MERGE_COUNTS( ch_all_counts.collect() ) + // ----------------------------------------------------------------- + // MERGE ALL DESIGNS IN A SINGLE TABLE + // ----------------------------------------------------------------- + ch_normalised_counts + .map { meta, file -> meta.design } + .set { ch_designs } + + MERGE_DESIGNS( ch_designs.collect() ) + // ----------------------------------------------------------------- // GENE STATISTICS // ----------------------------------------------------------------- diff --git a/subworkflows/local/merge_compute_stats_per_platform/main.nf b/subworkflows/local/merge_compute_stats_per_platform/main.nf index fcbe1711..85910cfa 100644 --- a/subworkflows/local/merge_compute_stats_per_platform/main.nf +++ b/subworkflows/local/merge_compute_stats_per_platform/main.nf @@ -1,4 +1,4 @@ -include { MERGE_COUNTS } from '../../../modules/local/merge_counts' +include { MERGE_COUNTS } from '../../../modules/local/merge/counts' include { COMPUTE_GENE_STATISTICS_PER_PLATFORM } from '../../../modules/local/compute_gene_statistics/per_platform' /* diff --git a/tests/modules/local/compute_gene_statistics/global/main.nf.test b/tests/modules/local/compute_gene_statistics/global/main.nf.test new file mode 100644 index 00000000..31a1fbfd --- /dev/null +++ b/tests/modules/local/compute_gene_statistics/global/main.nf.test @@ -0,0 +1,33 @@ +nextflow_process { + + name "Test Process COMPUTE_GLOBAL_GENE_STATISTICS" + script "modules/local/compute_gene_statistics/global/main.nf" + process "COMPUTE_GLOBAL_GENE_STATISTICS" + tag "genestats" + tag "module" + + test("Should run without failures") { + + when { + + process { + """ + input[0] = Channel.fromPath( '$projectDir/tests/test_data/merge_data/output/all_counts.parquet', checkIfExists: true) + input[1] = Channel.fromPath( '$projectDir/tests/test_data/compute_gene_statistics/input/*_stats_all_genes.csv', checkIfExists: true).collect() + input[2] = Channel.fromPath( '$projectDir/tests/test_data/compute_gene_statistics/input/metadata*.csv', checkIfExists: true ).collect() + input[3] = Channel.fromPath( '$projectDir/tests/test_data/compute_gene_statistics/input/mapping*.csv', checkIfExists: true ).collect() + input[4] = 1000 + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + +} diff --git a/tests/modules/local/gene_statistics/main.nf.test.snap b/tests/modules/local/compute_gene_statistics/global/main.nf.test.snap similarity index 51% rename from tests/modules/local/gene_statistics/main.nf.test.snap rename to tests/modules/local/compute_gene_statistics/global/main.nf.test.snap index ce43fa5d..24489c98 100644 --- a/tests/modules/local/gene_statistics/main.nf.test.snap +++ b/tests/modules/local/compute_gene_statistics/global/main.nf.test.snap @@ -3,49 +3,43 @@ "content": [ { "0": [ - "top_stable_genes_summary.csv:md5,ad6f15d03a4465551e689a7ef79a9490" + "top_stable_genes_summary.csv:md5,625915d58bdbeb59174ca3efbd74c509" ], "1": [ - "stats_all_genes.csv:md5,7e62d9c35f65c30059dd415c67bf258c" + "stats_all_genes.csv:md5,8b18e7b9360d9a24c10a3ceca1fed802" ], "2": [ - "all_counts_filtered.parquet:md5,c1d7f14d04ab280b9ff875599127091d" + "top_stable_genes_transposed_counts_filtered.csv:md5,049ac577898d74c820004ed83761f52f" ], "3": [ - "top_stable_genes_transposed_counts_filtered.csv:md5,6662ad1c9bbccdce113d7e574bf7e45c" - ], - "4": [ [ - "GENE_STATISTICS", + "COMPUTE_GLOBAL_GENE_STATISTICS", "python", "3.12.8" ] ], - "5": [ + "4": [ [ - "GENE_STATISTICS", + "COMPUTE_GLOBAL_GENE_STATISTICS", "polars", "1.17.1" ] ], - "all_counts": [ - "all_counts_filtered.parquet:md5,c1d7f14d04ab280b9ff875599127091d" - ], "all_statistics": [ - "stats_all_genes.csv:md5,7e62d9c35f65c30059dd415c67bf258c" + "stats_all_genes.csv:md5,8b18e7b9360d9a24c10a3ceca1fed802" ], "top_stable_genes_summary": [ - "top_stable_genes_summary.csv:md5,ad6f15d03a4465551e689a7ef79a9490" + "top_stable_genes_summary.csv:md5,625915d58bdbeb59174ca3efbd74c509" ], "top_stable_genes_transposed_counts": [ - "top_stable_genes_transposed_counts_filtered.csv:md5,6662ad1c9bbccdce113d7e574bf7e45c" + "top_stable_genes_transposed_counts_filtered.csv:md5,049ac577898d74c820004ed83761f52f" ] } ], "meta": { "nf-test": "0.9.2", - "nextflow": "25.04.0" + "nextflow": "25.04.6" }, - "timestamp": "2025-05-11T19:30:08.395767893" + "timestamp": "2025-09-11T11:20:17.827642173" } } \ No newline at end of file diff --git a/tests/modules/local/gene_statistics/main.nf.test b/tests/modules/local/gene_statistics/main.nf.test deleted file mode 100644 index e3fd5226..00000000 --- a/tests/modules/local/gene_statistics/main.nf.test +++ /dev/null @@ -1,38 +0,0 @@ -nextflow_process { - - name "Test Process GENE_STATISTICS" - script "modules/local/gene_statistics/main.nf" - process "GENE_STATISTICS" - tag "genestats" - tag "module" - - test("Should run without failures") { - - when { - - process { - """ - ch_counts = Channel.fromPath( '$projectDir/tests/test_data/merge_data/output/all_counts.parquet', checkIfExists: true) - ch_metadata = Channel.fromPath( '$projectDir/tests/test_data/gene_statistics/input/metadata*.csv', checkIfExists: true ).collect() - ch_mapping = Channel.fromPath( '$projectDir/tests/test_data/gene_statistics/input/mapping*.csv', checkIfExists: true ).collect() - ch_ks_stat_file = Channel.fromPath( '$projectDir/tests/test_data/gene_statistics/input/ks_stats.csv', checkIfExists: true ) - input[0] = ch_counts - input[1] = ch_metadata - input[2] = ch_mapping - input[3] = 3 - input[4] = ch_ks_stat_file - input[5] = 0 - """ - } - } - - then { - assertAll( - { assert process.success }, - { assert snapshot(process.out).match() } - ) - } - - } - -} diff --git a/tests/test_data/compute_gene_statistics/input/design.csv b/tests/test_data/compute_gene_statistics/input/design.csv new file mode 100644 index 00000000..d3e8694c --- /dev/null +++ b/tests/test_data/compute_gene_statistics/input/design.csv @@ -0,0 +1,28 @@ +sample,condition,batch +ARR029909,g1,A +ARR029910,g1,A +ARR029911,g1,A +ARR029912,g2,A +ARR029913,g2,A +ARR029914,g2,A +ARR029915,g3,A +ARR029916,g3,A +ARR029917,g3,A +URR029909,g1,B +URR029910,g1,B +URR029911,g1,B +URR029912,g2,B +URR029913,g2,B +URR029914,g2,B +URR029915,g3,B +URR029916,g3,B +URR029917,g3,B +ERR029909,g1,C +ERR029910,g1,C +ERR029911,g1,C +ERR029912,g2,C +ERR029913,g2,C +ERR029914,g2,C +ERR029915,g3,C +ERR029916,g3,C +ERR029917,g3,C diff --git a/tests/test_data/gene_statistics/input/gene_counts.csv b/tests/test_data/compute_gene_statistics/input/gene_counts.csv similarity index 100% rename from tests/test_data/gene_statistics/input/gene_counts.csv rename to tests/test_data/compute_gene_statistics/input/gene_counts.csv diff --git a/tests/test_data/gene_statistics/input/ks_stats.csv b/tests/test_data/compute_gene_statistics/input/ks_stats.csv similarity index 100% rename from tests/test_data/gene_statistics/input/ks_stats.csv rename to tests/test_data/compute_gene_statistics/input/ks_stats.csv diff --git a/tests/test_data/gene_statistics/input/mapping1.csv b/tests/test_data/compute_gene_statistics/input/mapping1.csv similarity index 100% rename from tests/test_data/gene_statistics/input/mapping1.csv rename to tests/test_data/compute_gene_statistics/input/mapping1.csv diff --git a/tests/test_data/gene_statistics/input/mapping2.csv b/tests/test_data/compute_gene_statistics/input/mapping2.csv similarity index 100% rename from tests/test_data/gene_statistics/input/mapping2.csv rename to tests/test_data/compute_gene_statistics/input/mapping2.csv diff --git a/tests/test_data/gene_statistics/input/mapping3.csv b/tests/test_data/compute_gene_statistics/input/mapping3.csv similarity index 100% rename from tests/test_data/gene_statistics/input/mapping3.csv rename to tests/test_data/compute_gene_statistics/input/mapping3.csv diff --git a/tests/test_data/gene_statistics/input/metadata1.csv b/tests/test_data/compute_gene_statistics/input/metadata1.csv similarity index 100% rename from tests/test_data/gene_statistics/input/metadata1.csv rename to tests/test_data/compute_gene_statistics/input/metadata1.csv diff --git a/tests/test_data/gene_statistics/input/metadata2.csv b/tests/test_data/compute_gene_statistics/input/metadata2.csv similarity index 100% rename from tests/test_data/gene_statistics/input/metadata2.csv rename to tests/test_data/compute_gene_statistics/input/metadata2.csv diff --git a/tests/test_data/compute_gene_statistics/input/microarray_stats_all_genes.csv b/tests/test_data/compute_gene_statistics/input/microarray_stats_all_genes.csv new file mode 100644 index 00000000..acb56a11 --- /dev/null +++ b/tests/test_data/compute_gene_statistics/input/microarray_stats_all_genes.csv @@ -0,0 +1,8 @@ +ensembl_gene_id,microarray_mean,microarray_standard_deviation,microarray_median,microarray_median_absolute_deviation,microarray_variation_coefficient,microarray_total_nb_nulls,microarray_nb_nulls_valid_samples,microarray_stability_score,microarray_expression_level_quantile_interval +AT1G34790,0.6041722385984585,0.2965945020346847,0.8210634736950527,0.07852066041592076,0.49091051042450434,678,643,1.4392880915454482,71 +AT5G35550,0.04211885958141837,0.017403154131542625,0.04081717758449555,0.00889668425147598,0.41319148487155133,678,643,1.3615690659924953,0 +AT5G23260,0.3265572056851324,0.12636844695328353,0.2977133397782717,0.09861099799987358,0.3869718528738528,678,643,1.3353494339947967,35 +AT5G23261,0.05948100952172446,0.0268768665570047,0.049569984365840696,0.021228253513649518,0.4518562608993441,678,643,1.400233842020288,1 +AT1G34790,0.5791984846868644,0.16532007773816776,0.5865184277282238,0.13319224137108376,0.28542905775650596,70,35,0.337051476635562,68 +AT5G35550,0.4069181057633956,0.2662419700433056,0.26506770843115524,0.13156965253473574,0.6542888268484007,678,643,1.6026664079693447,46 +AT5G23260,0.12079194562039748,0.060559689529495545,0.10818687095210754,0.0368400391021249,0.5013553612242599,678,643,1.449732942345204,7 diff --git a/tests/test_data/compute_gene_statistics/input/rnaseq_stats_all_genes.csv b/tests/test_data/compute_gene_statistics/input/rnaseq_stats_all_genes.csv new file mode 100644 index 00000000..789ddd31 --- /dev/null +++ b/tests/test_data/compute_gene_statistics/input/rnaseq_stats_all_genes.csv @@ -0,0 +1,8 @@ +ensembl_gene_id,rnaseq_mean,rnaseq_standard_deviation,rnaseq_median,rnaseq_median_absolute_deviation,rnaseq_variation_coefficient,rnaseq_total_nb_nulls,rnaseq_nb_nulls_valid_samples,rnaseq_stability_score,rnaseq_expression_level_quantile_interval +AT1G34790,0.029004004004004002,0.061217504567865136,0.0,0.0,2.110657016852365,345,336,3.0544772415714663,0 +AT5G35550,0.2921254587921254,0.028005675342417956,0.28128128128128127,0.025025025025025016,0.09586865676896245,356,347,1.070587757892558,41 +AT5G23260,0.051621388830691145,0.04715133948046024,0.04154154154154154,0.027027027027027035,0.9134070304677029,322,313,1.7926205136137703,3 +AT5G23261,0.06000444889333778,0.0796183056079376,0.030030030030030026,0.030030030030030026,1.3268733748303374,356,347,2.301592475953933,5 +AT1G34790,0.027638749860972082,0.019581626793675158,0.025525525525525526,0.014014014014014014,0.7084845332069752,356,347,1.6832036343305707,0 +AT5G35550,0.07687920478618152,0.05023977809403856,0.06906906906906907,0.03603603603603604,0.6534898251583997,322,313,1.532703308304467,8 +AT5G23260,0.05421550582840906,0.0785887308235655,0.0,0.0,1.4495618849761762,303,294,2.2754045816053896,4 diff --git a/tests/test_data/normfinder/all_counts.normalised.parquet b/tests/test_data/normfinder/all_counts.normalised.parquet new file mode 100644 index 0000000000000000000000000000000000000000..ba20a3e5c40973c0ae709bb6c5af977f87c87041 GIT binary patch literal 2553 zcmb_eO=uHA6n>jO;wIKwopDzq6eLSQQJcRuHkO^*AAhPQHC9^$e`;D4Yo%3N#X~_6 z!6JeRR*HBK5fLxq%|j0!ic}COBI3b|#e){{CO+Ae7XsLs%v8 z5nm_)HSTOSrg;TOmI`zMxbQ|*-0rKbK!SQ4e3iPkBzI~k*MH>X!M@z7+`;2VNsy}a zg+kvehClUZDWp0;vYZMel^<&YjHXO2AH_|)7wXre`bQS$-Ekaxoe87yL@Hr=pgJ%$ z9Ig$_C`8r6;L|nNo?1cuRG|+QW7@^J7p(nv4Gjei35SOJmIfH*{G0IKkW83fsJ?dl z<)nY(5oJwLL&T8&_YFMqnq@^JmGw+TkO?*ytWIT?o zhz2fc)l=TFqKc?g@mMiF`h<#wsxQuIflH)3s9 z@2t1gyh${h8!@Q?-S{F_LjJv+wS|W5&Q75J0HeGUR%BZdp8-ZJ4ZXAV=tMjj_riTY zUkgqn;*x$sF}|xdBb;RgefvW6jRksiT`tIP05|e2NFqC53ypOqTCmE!X0kP4vmhb) z?AE6!qfCeBIdTGD0rU`^c9DllKA!Ou7wO) z`DsDc(Xo5qrlFo=x&Fi26YmBw`kEMHE%;~k=Wt>q!$Z6mQ#%+NVp|x)Ylf{&V}5v; z=eIG|mw&DYuUlF$%{l}ogY}vP_?UP?)?=)W<1g?@3*08cmS((USkpF!K)kkaJ{*FF zvGgE<>T#-zHT7`+QsLl(k-&`?pi}@4WC$ERMXtd$AS& z@b?1u7k$mJW)5xZJlhlLJk!-b(%T*BJloxwJl)OMX3%`;fs=;^hxChhw$1;+3jC}~ H<450bBy0aQ literal 0 HcmV?d00001 diff --git a/tests/test_data/normfinder/all_counts.normfinder.csv b/tests/test_data/normfinder/all_counts.normfinder.csv new file mode 100644 index 00000000..63bf33ec --- /dev/null +++ b/tests/test_data/normfinder/all_counts.normfinder.csv @@ -0,0 +1,6 @@ +ensembl_gene_id,S1,S2,S3,S4,S5,S6 +GAPDH,23.1,23.5,20.2,20.7,24.9,25.2 +TT1,21.5,26.5,25.6,28.2,21.5,26.5 +TT2,22.5,27.5,25.9,6.2,25.5,30.5 +TT3,28.5,25.5,25.6,27.2,21.5,26.5 +TT4,22.5,22.5,21.6,22.2,21.5,23.5 diff --git a/tests/test_data/normfinder/convert_to_parquet.py b/tests/test_data/normfinder/convert_to_parquet.py new file mode 100644 index 00000000..433171dd --- /dev/null +++ b/tests/test_data/normfinder/convert_to_parquet.py @@ -0,0 +1,4 @@ +import polars as pl + +df = pl.read_csv("/home/olivier/repositories/nf-core-stableexpression/tests/all_counts.normfinder.csv") +df.write_parquet("/home/olivier/repositories/nf-core-stableexpression/tests/all_counts.normalised.parquet") diff --git a/tests/test_data/normfinder/design.csv b/tests/test_data/normfinder/design.csv new file mode 100644 index 00000000..221601a5 --- /dev/null +++ b/tests/test_data/normfinder/design.csv @@ -0,0 +1,7 @@ +sample,condition,batch +S1,control,A +S2,treated,A +S3,control,A +S4,treated,A +S5,control,A +S6,treated,A diff --git a/tests/test_data/normfinder/normfinder.R b/tests/test_data/normfinder/normfinder.R new file mode 100644 index 00000000..c120d305 --- /dev/null +++ b/tests/test_data/normfinder/normfinder.R @@ -0,0 +1,298 @@ +library(optparse) +library(dplyr) +library(tidyr) + + +get_args <- function() { + option_list <- list( + make_option("--data", type = "character") + ) + + args <- parse_args(OptionParser( + option_list = option_list, + description = "Normfinder" + )) + return(args) +} + + +normfinder<-function(data, group = TRUE, ctVal=FALSE, pStabLim=0.3, sample = "sample", gene = "gene", groups = "group", cq = "cq"){ + + # Group & sample ID + sample_group <- unique(data[,c(sample, groups)]) + + tmp <- data.frame(sample = as.character(data[, sample]), + gene = as.character(data[, gene]), + cq = as.numeric(data[, cq])) + tmp <- tmp %>% + dplyr::group_by(sample, gene) %>% + dplyr::summarise(cq=mean(cq, na.rm=T)) %>% + tidyr::spread(sample, cq) + + ntotal<-length(sample_group[,1]) + + if (group == TRUE){ + ngenes <- length(tmp$gene) # number of genes + genenames <- as.character(tmp$gene) + grId <- factor(sample_group[,2]) + } else { + ngenes <- length(tmp$gene) # number of genes + genenames <- as.character(tmp$gene) + grId <- rep(1,ntotal) + } + + tmp <- data.matrix(tmp[,sample_group[,1]]) + + if (!ctVal){tmp<-log2(tmp)} + + + groupnames <- levels(grId) + ngr <- length(levels(grId)) + + # Number of samples in each group: + nsamples <- rep(0,ngr) + for (group in 1:ngr){nsamples[group] <- sum(grId==groupnames[group])} + + + + MakeStab <- function(da){ + + ngenes <- dim(da)[1] + # Sample averages + sampleavg <- apply(da,2,mean) + # Gene averages within group + genegroupavg <- matrix(0,ngenes,ngr) + + for (group in 1:ngr){ + genegroupavg[,group] <- apply(da[,grId==groupnames[group]],1,mean)} + + # Group averages + groupavg=rep(0,ngr) + for (group in 1:ngr){groupavg[group] <- mean(da[,grId==groupnames[group]])} + + # Variances + GGvar=matrix(0,ngenes,ngr) + for (group in 1:ngr){ + grset <- (grId==groupnames[group]) + a=rep(0,ngenes) + for (gene in 1:ngenes){ + a[gene] <- sum((da[gene,grset]-genegroupavg[gene,group]- + sampleavg[grset]+groupavg[group])^2)/(nsamples[group]-1) + } + GGvar[,group] <- (a-sum(a)/(ngenes*ngenes-ngenes))/(1-2/ngenes) + } + + print("GGvar") + print(GGvar) + + # + # Change possible negative values + genegroupMinvar <- matrix(0, ngenes, ngr) + for (group in 1:ngr){ + grset <- (grId == groupnames[group]) + z <- da[,grset] + for (gene in 1:ngenes){ + varpair <- rep(0,ngenes) + for (gene1 in 1:ngenes){varpair[gene1] <- var(z[gene,] - z[gene1,])} + genegroupMinvar[gene,group] <- min(varpair[-gene])/4 + } + } + # + # Final variances + GGvar <- ifelse(GGvar < 0, genegroupMinvar, GGvar) + print("GGvar") + print(GGvar) + # + # Old stability measure for each gene is calculated: + # + dif <- genegroupavg + difgeneavg <- apply(dif, 1, mean) + difgroupavg <- apply(dif, 2, mean) + difavg <- mean(dif) + for (gene in 1:ngenes){ + for (group in 1:ngr){ + dif[gene,group] <- dif[gene, group] - difgeneavg[gene] - difgroupavg[group] + difavg + } + } + # + nsampMatrix <- matrix(rep(nsamples,ngenes),ngenes,ngr,byrow=T) + vardif <- GGvar/nsampMatrix + gamma <- sum(dif * dif) / ((ngr-1) * (ngenes-1)) -sum (vardif) / (ngenes*ngr) + gamma <- ifelse(gamma<0,0,gamma) + # + difnew <- dif * gamma / (gamma+vardif) + varnew <- vardif + gamma * vardif / (gamma+vardif) + Ostab0 <- abs(difnew) + sqrt(varnew) + Ostab <- apply(Ostab0, 1, mean) + + # + # Measure of group differences: + mud <- rep(0,ngenes) + for (gene in 1:ngenes){ + mud[gene] <- 2*max(abs(dif[gene,])) + } + # Common variance: + genevar <- rep(0,ngenes) + for (gene in 1:ngenes){ + genevar[gene] <- sum((nsamples-1) * GGvar[gene,]) / (sum(nsamples)-ngr) + } + Gsd <- sqrt(genevar) + # + # Return results: + # + return(cbind(mud, Gsd, Ostab, rep(gamma,ngenes), GGvar,dif)) + } # End of function MakeStab + # + # + MakeComb2 <- function(g1, g2, res){ + gam <- res[1,4] + d1 <- res[g1,(4 + ngr + 1):(4 + ngr + ngr)]; d2 <- res[g2, (4 + ngr + 1):(4+ngr+ngr)] + s1 <- res[g1, (4+1):(4+ngr)]; s2 <- res[g2, (4+1):(4+ngr)] + rho <- abs(gam * d1 / (gam + s1 / nsamples) + gam * d2 / (gam + s2 / nsamples)) * sqrt(ngenes / (ngenes-2)) / 2 + rho <- rho + sqrt(s1 / nsamples + gam * s1 / (nsamples*gam+s1) + s2 / nsamples + gam * s2 / (nsamples*gam+s2))/2 + return(mean(rho)) + } + # + # + MakeStabOne <- function(da){ + ngenes <- dim(da)[1] + # Sample averages + sampleavg <- apply(da, 2, mean) + # Gene averages + geneavg <- apply(da, 1, mean) + totalavg <- mean(da) + # + # Variances + genevar0 <- rep(0, ngenes) + for (gene in 1:ngenes){ + genevar0[gene] <- sum((tmp[gene,] - geneavg[gene] - sampleavg + totalavg)^2) / ((ntotal-1) * (1-2/ngenes)) + } + genevar <- genevar0 - sum(genevar0) / (ngenes*ngenes-ngenes) + # + # Change possible negative values + geneMinvar <- rep(0,ngenes) + z <- da + for (gene in 1:ngenes){ + varpair <- rep(0, ngenes) + for (gene1 in 1:ngenes){varpair[gene1] <- var(z[gene,] - z[gene1,])} + geneMinvar[gene] <- min(varpair[-gene]) / 4 + } + # Final variances + genevar = ifelse(genevar<0, geneMinvar, genevar) + # + return(genevar) + } + # End of function MakeStabOne + + #### Main function #### + if (ngr>1){ # More than one group. + # + res <- MakeStab(tmp) + # + gcand <- c(1:ngenes)[res[,3] < pStabLim] + ncand <- length(gcand) + if (ncand<4){ + if (ngenes>3){ + li <- sort(res[,3])[4] + gcand <- c(1:ngenes)[res[,3]<=li] + ncand <- length(gcand) + } else { + gcand <- c(1:ngenes) + ncand <- length(gcand) + } + } + # + vv2 <- c() + # + for (g1 in 1:(ncand-1)){ + for (g2 in (g1+1):ncand){ + qmeas <- MakeComb2(gcand[g1], gcand[g2], res) + vv2 <- rbind(vv2, c(gcand[g1], gcand[g2], qmeas)) + }} + # + ord <- order(res[,3]) + FinalRes <- list(Ordered <- data.frame("GroupDif" = round(res[ord,1],3), + "GroupSD" = round(res[ord,2],3), + "Stability" = round(res[ord,3],3), + row.names = genenames[ord]), + UnOrdered <- data.frame("GroupDif" = round(res[,1],3), + "GroupSD" = round(res[,2],3), + "Stability" = round(res[,3],3), + "IGroupSD" = round(sqrt(res[,(4+1):(4+ngr)]),3), + "IGroupDif" = round(res[,(4+ngr+1):(4+ngr+ngr)],3), + row.names = genenames), + PairOfGenes <- data.frame("Gene1" = genenames[vv2[,1]], + "Gene2" = genenames[vv2[,2]], + "Stability" = round(vv2[,3],3))) + # + return(FinalRes) + # + } else { # End of more than one group: next is for one group only. + # + # + sigma <- sqrt(MakeStabOne(tmp)) + # + siglim <- (min(sigma)+0.1) + gcand <- c(1:ngenes)[sigma=2) & (ngenes>3)){ + # + vv2=c() + # + for (g1 in 1:(ncand-1)){ + for (g2 in (g1+1):ncand){ + dat1 <- rbind(tmp[-c(gcand[g1], gcand[g2]),], + apply(tmp[c(gcand[g1], gcand[g2]),], 2, mean)) + qmeas <- sqrt(MakeStabOne(dat1)) + vv2 <- rbind(vv2, c(gcand[g1], gcand[g2], qmeas[ngenes-1])) + }} + ord <- order(sigma) + FinalRes <- list(Ordered <- data.frame("GroupSD" = round(sigma[ord],3), + row.names = genenames[ord]), + PairOfGenes <- data.frame("Gene1" = genenames[vv2[,1]], + "Gene2" = genenames[vv2[,2]], + "GroupSD" = round(vv2[,3],3))) + } else { # No combined genes to consider + ord <- order(sigma) + FinalRes <- list(Ordered <- data.frame("GroupSD" = round(sigma[ord],3), + row.names = genenames[ord])) + } # End ncand<2 or ngenes<=3 + # + return(FinalRes) + # + } # End one group only + +} ##### + +# Read the counts file +counts <- read.csv("all_counts.normfinder.csv") + +# Build design (conditions per sample) +design <- data.frame( + sample = c("S1","S2","S3","S4","S5","S6"), + group = c("control","treated","control","treated","control","treated") +) + +# Convert counts wide → long +library(tidyr) +library(dplyr) + +data <- counts %>% + tidyr::pivot_longer( + cols = -ensembl_gene_id, + names_to = "sample", + values_to = "cq" + ) %>% + dplyr::rename(gene = ensembl_gene_id) %>% + dplyr::left_join(design, by = "sample") + +# Inspect +#print(data) + +data <- as.data.frame(data) + + +res = normfinder(data, ctVal=TRUE) +print("res") +print(res) diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 140cec0a..0811e122 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -45,7 +45,6 @@ workflow STABLEEXPRESSION { EXPRESSIONATLAS_FETCHDATA.out.accessions .filter { accession -> accession.startsWith("E-GEOD-") } .map { accession -> accession.replace("E-GEOD-", "GSE")} - .view() .set { ch_excluded_geo_accessions } GEO_FETCHDATA ( From 495adb97227a8908420da1e1fdae4fd13e7f1abc Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sat, 13 Sep 2025 19:01:05 +0200 Subject: [PATCH 055/258] finalised and tested the normfinder module --- bin/normfinder.py | 651 +++++++++++------- modules/local/normfinder/main.nf | 31 + modules/local/normfinder/spec-file.txt | 43 ++ tests/modules/local/normfinder/main.nf.test | 44 ++ .../local/normfinder/main.nf.test.snap | 64 ++ .../normfinder/all_counts.normfinder.csv | 6 - .../normfinder/convert_to_parquet.py | 4 - .../all_counts.normalised.parquet | Bin 0 -> 6708 bytes .../normfinder/small_normalised/design.csv | 12 + .../all_counts.normalised.parquet | Bin .../normfinder/{ => very_small_cq}/design.csv | 0 .../{ => very_small_cq}/normfinder.R | 0 12 files changed, 586 insertions(+), 269 deletions(-) create mode 100644 modules/local/normfinder/main.nf create mode 100644 modules/local/normfinder/spec-file.txt create mode 100644 tests/modules/local/normfinder/main.nf.test create mode 100644 tests/modules/local/normfinder/main.nf.test.snap delete mode 100644 tests/test_data/normfinder/all_counts.normfinder.csv delete mode 100644 tests/test_data/normfinder/convert_to_parquet.py create mode 100644 tests/test_data/normfinder/small_normalised/all_counts.normalised.parquet create mode 100644 tests/test_data/normfinder/small_normalised/design.csv rename tests/test_data/normfinder/{ => very_small_cq}/all_counts.normalised.parquet (100%) rename tests/test_data/normfinder/{ => very_small_cq}/design.csv (100%) rename tests/test_data/normfinder/{ => very_small_cq}/normfinder.R (100%) diff --git a/bin/normfinder.py b/bin/normfinder.py index 1270ce8c..4255667f 100755 --- a/bin/normfinder.py +++ b/bin/normfinder.py @@ -1,318 +1,451 @@ +#!/usr/bin/env python3 + +# Written by Olivier Coen. Released under the MIT license. + import polars as pl import sys +import argparse +from pathlib import Path from tqdm import tqdm +from dataclasses import dataclass, field +from typing import ClassVar from statistics import mean -ENSEMBL_GENE_ID_COLNAME = "ensembl_gene_id" +import numpy as np +from numba import njit, prange +import logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) -count_file = sys.argv[1] -design_file = sys.argv[2] +STABILITY_OUTFILENAME = "stabilities.csv" -count_lf = pl.scan_parquet(count_file) -design_df = pl.read_csv(design_file) -design_df = ( - design_df - .with_columns(pl.concat_str([pl.col('batch'), pl.col('condition')], separator="_").alias("group")) - .select("sample", "group") -) +############################################################################ +# POLARS EXTENSIONS +############################################################################ -group_to_sample_df = ( - design_df - .group_by("group", maintain_order=True) # maintain order is better for repeatability and testing - .agg("sample") -) +@pl.api.register_expr_namespace("row") +class StatsExtension: + def __init__(self, expr: pl.Expr): + self._expr = expr -group_to_samples_dict = { - d["group"]: d["sample"] - for d in group_to_sample_df.to_dicts() -} -del group_to_sample_df + def not_null_values(self): + return ( + self._expr + .list + .eval(pl.element().drop_nulls().drop_nans()) + .list + ) -groups = list(group_to_samples_dict.keys()) -n_groups = len(groups) + def mean(self) -> pl.Expr: + """Mean over non nulls values in row""" + return self.not_null_values().mean() + + def sum(self) -> pl.Expr: + """Median over non nulls values in row""" + return self.not_null_values().sum() + + def min(self) -> pl.Expr: + """Median over non nulls values in row""" + return self.not_null_values().min() + + +############################################################################ +# NUMBA-ACCELERATED FUNCTIONS +############################################################################ + +@njit(parallel=True) +def compute_minvars(z: np.ndarray, target_idx: np.ndarray) -> np.ndarray: + """ + z: (ngenes, nsamples) array + target_idx: 1D array of indices (int64) for which to compute minvar + returns: 1D array of length len(target_idx) + """ + ngenes, nsamples = z.shape + + # should not happen as it is controlled before, but just in case + if nsamples < 2: + raise ValueError("Number of samples must be at least 2") + + minvars = np.empty(len(target_idx), dtype=np.float64) + for k in prange(len(target_idx)): + i = target_idx[k] + # checking if counts for this gene are all nans + nb_valid_counts = (~np.isnan(z[i, :])).sum() + if nb_valid_counts < 1: + minvars[k] = np.nan + continue # skip this gene + # computing variances of pairwise differences + minv = 1e18 + for j in prange(ngenes): + if i == j: + continue + diffs = z[i, :] - z[j, :] + mean = np.sum(diffs) / nsamples # scalar + var = np.sum((diffs - mean) ** 2) / (nsamples - 1) # scalar + if np.isnan(var): + continue # skip + if var < minv: + minv = var + minvars[k] = minv / 4.0 if minv < 1e18 else np.inf + return minvars + + +##################################################### +# NORMFINDER CLASS +##################################################### + +@dataclass +class NormFinder: + + ENSEMBL_GENE_ID_COLNAME: ClassVar[str] = "ensembl_gene_id" + + count_lf: pl.LazyFrame + design_df: pl.DataFrame + + genes: list[str] = field(init=False) + + group_to_samples_dict: dict[str, list] = field(init=False) + + n_groups: int = field(init=False) + n_genes: int = field(init=False) + + def __post_init__(self): + + # format_design + self.design_df = ( + self.design_df + .with_columns(pl.concat_str([pl.col('batch'), pl.col('condition')], separator="_").alias("group")) + .select("sample", "group") + ) -genes = count_lf.select(ENSEMBL_GENE_ID_COLNAME).collect().to_series().to_list() -k = len(genes) -renaming_dict = { f"column_{i}": gene for i, gene in enumerate(genes) } + # make dict associating a group to the list of its samples + group_to_sample_df = ( + self.design_df + .group_by("group", maintain_order=True) # maintain order is better for repeatability and testing + .agg("sample") + ) -if k <= 2: - raise ValueError("Too few genes") + self.group_to_samples_dict = { d["group"]: d["sample"] for d in group_to_sample_df.to_dicts() } -def get_overall_mean_for_group(df_with_means_over_samples): - return df_with_means_over_samples.mean().item() + groups = list(self.group_to_samples_dict.keys()) + self.n_groups = len(groups) + self.genes = self.count_lf.select(self.ENSEMBL_GENE_ID_COLNAME).collect().to_series().to_list() + self.n_genes = len(self.genes) -def get_means_over_samples(df): - return ( - df - .with_columns( - mean_over_samples_for_gene=pl.concat_list(pl.all()).list.drop_nulls().list.mean() - ) - .select("mean_over_samples_for_gene") - ) + if self.n_genes <= 2: + logger.error("Too few genes") + sys.exit(100) -def compute_minvar(df, target_gene) -> float: - return ( - df - .select((pl.col(target_gene) - pl.col(col)).alias(col) for col in genes if col != target_gene) # makes all pairwise differences with other genes - .var(ddof=1) # computes variance - .select( - ( pl.concat_list(pl.all()).list.drop_nulls().list.min() / 4 ).alias("min") # get min of variances and divides by 4 - ) - .item() - ) + @staticmethod + def get_overall_mean_for_group(df_with_means_over_samples: pl.DataFrame) -> float: + return df_with_means_over_samples.mean().item() -def get_unbiased_intragroup_variance(df, means_over_samples_df, group_overall_mean, samples): - - # TODO: see if it's correct - # if only one sample in the group, there's no variance - if len(samples) == 1: - data = { gene: [0] for gene in genes } - return pl.DataFrame(data) - - # lf is a lazyframe with a column being the gene ids (ensembl_gene_id) - # and other columns being the samples - # the current chunk corresponds to only one group - # means_over_samples_df is a single column dataframe containing the means across each row (ie for each gene across samples) - ng = len(samples) - - means_over_samples = means_over_samples_df.to_series().rename("mean_over_samples_for_gene") - - mean_over_genes = df.mean().transpose().to_series().rename("mean_over_genes_for_sample") - - sample_variance_df = ( - df - .hstack([means_over_samples]) # adding column containing means over all samples in this group (for each gene) - .select([ - (pl.col(c) - pl.col("mean_over_samples_for_gene")).alias(c) # y_igj - mean(y_ig*) - for c in samples - ]) - .transpose(include_header=True, column_names=genes) # columns are now genes - .hstack([mean_over_genes]) # adding column containing means over all genes (for each sample) - .select([ - ((pl.col(c) - pl.col("mean_over_genes_for_sample") + group_overall_mean) ** 2).alias(c) # r_igj ^2 = (y_igj - mean(y_ig*) -mean(y_*gj) + mean(y_*g*) ) ^ 2 - for c in genes - ]) - .transpose(include_header=True, column_names=samples) - .with_columns( - sample_variance=pl.concat_list(samples).list.drop_nulls().list.sum() / ((ng - 1) * (1 - 2 / k)) # sum over j (samples) of r_igj ^2 terms + @staticmethod + def get_means_over_samples(df: pl.DataFrame) -> pl.DataFrame: + return ( + df + .with_columns( + mean_over_samples_for_gene=pl.concat_list(pl.all()).row.mean() + ) + .select("mean_over_samples_for_gene") ) - .select("sample_variance") - .transpose() - .rename(renaming_dict) - ) - # sum of all sample variances for all genes - sample_variance_sum_over_genes = sample_variance_df.select(pl.sum_horizontal(pl.all())).item() # sum of all s_ij² over all genes - intra_var_df = ( - sample_variance_df - .select([ - ( pl.col(c) - sample_variance_sum_over_genes / (k * (k -1)) ).alias(c) - for c in genes - ]) - ) - - # if some values are negative, we need a special process - genes_with_negative_values = ( - intra_var_df - .select(col for col in genes if (intra_var_df[col] < 0).all()) # intra_var_df has only one row but it is a dataframe - .columns - ) + def correct_negative_values(self, intra_var_df: pl.DataFrame, group_count_df: pl.DataFrame) -> pl.DataFrame: + genes_with_negative_values = ( + intra_var_df + .select(col for col in self.genes if + (intra_var_df[col] < 0).all()) # intra_var_df has only one row but it is a dataframe + .columns + ) - minvar = {} - if genes_with_negative_values: - transposed_df = df.transpose(include_header=True, column_names=genes).select(genes) - for gene in genes_with_negative_values: - minvar[gene] = compute_minvar(transposed_df, gene) - - return ( - intra_var_df - .with_columns([ - pl.lit(val).alias(col) for col, val in minvar.items() - ]) - ) + # getting indexes of genes for which we must compute minvar + indexes_of_genes_with_negative_values = [i for i, gene in enumerate(self.genes) if gene in genes_with_negative_values] + #transposed_df = group_count_df.transpose(include_header=True, column_names=self.genes).select(self.genes) + minvars = compute_minvars( + group_count_df.to_numpy(), + indexes_of_genes_with_negative_values + ) + # associating back minvars to their respective gene + minvar_dict = { gene: minvars[i] for i, gene in enumerate(genes_with_negative_values) } + return ( + intra_var_df + .with_columns([ + pl.lit(val).alias(col) for col, val in minvar_dict.items() + ]) + ) -def adjust_for_nb_of_samples_in_groups(unbiased_intragroup_variance_df, n_samples_list): - return ( - unbiased_intragroup_variance_df - .with_columns( - n_samples=pl.Series(n_samples_list) + def get_unbiased_intragroup_variance_for_group( + self, + group_count_df: pl.DataFrame, + means_over_samples_df: pl.DataFrame, + group_overall_mean: float, + samples: list[str], + ): + + # TODO: see if it's correct + # if only one sample in the group, there's no variance + if len(samples) == 1: + data = { gene: [0] for gene in self.genes } + return pl.DataFrame(data) + + # lf is a lazyframe with a column being the gene ids (ensembl_gene_id) + # and other columns being the samples + # the current chunk corresponds to only one group + # means_over_samples_df is a single column dataframe containing the means across each row (ie for each gene across samples) + ng = len(samples) + + means_over_samples = means_over_samples_df.to_series().rename("mean_over_samples_for_gene") + + mean_over_genes = group_count_df.mean().transpose().to_series().rename("mean_over_genes_for_sample") + + sample_variance_df = ( + group_count_df + .hstack([means_over_samples]) # adding column containing means over all samples in this group (for each gene) + .select([ + (pl.col(c) - pl.col("mean_over_samples_for_gene")).alias(c) # y_igj - mean(y_ig*) + for c in samples + ]) + .transpose(include_header=True, column_names=self.genes) # columns are now genes + .hstack([mean_over_genes]) # adding column containing means over all genes (for each sample) + .select([ + ((pl.col(c) - pl.col("mean_over_genes_for_sample") + group_overall_mean) ** 2).alias(c) # r_igj ^2 = (y_igj - mean(y_ig*) -mean(y_*gj) + mean(y_*g*) ) ^ 2 + for c in self.genes + ]) + .transpose(include_header=True, column_names=samples) + .with_columns( + sample_variance=pl.concat_list(samples).row.sum() / ((ng - 1) * (1 - 2 / self.n_genes)) # sum over j (samples) of r_igj ^2 terms + ) + .select("sample_variance") + .transpose() + .rename( {f"column_{i}": gene for i, gene in enumerate(self.genes)} ) ) - .select([ - (pl.col(c) / pl.col("n_samples")).alias(c) - for c in genes - ]) - ) + # sum of all sample variances for all genes + sample_variance_sum_over_genes = sample_variance_df.select(pl.sum_horizontal(pl.all())).item() # sum of all s_ij² over all genes -def get_unbiased_intergroup_variance(gene_means_in_groups_df, dataset_overall_mean): - mean_over_genes = gene_means_in_groups_df.mean().transpose().to_series().rename("mean_over_genes_for_group") - - return ( - gene_means_in_groups_df - .with_columns( - mean_over_groups_for_gene=pl.concat_list(pl.all()).list.drop_nulls().list.mean() + intra_var_df = ( + sample_variance_df + .select([ + ( pl.col(c) - sample_variance_sum_over_genes / (self.n_genes * (self.n_genes -1)) ).alias(c) + for c in self.genes + ]) + ) + # if some values are negative, we need a special process + corrected_intra_var_df = self.correct_negative_values(intra_var_df, group_count_df) + + return corrected_intra_var_df + + + def get_unbiased_intragroup_variances(self): + + unbiased_intragroup_variance_dfs = [] + means_over_samples_dfs = [] + group_overall_means = [] + + for group, samples in tqdm(self.group_to_samples_dict.items()): + + # sub dataframe corresponding to this group + chunk_df = self.count_lf.select(samples).collect() + # computing means over samples for each gene + means_over_samples_df = self.get_means_over_samples(chunk_df) + # getting overall expression average in the group for all genes + group_overall_mean = self.get_overall_mean_for_group(means_over_samples_df) + + group_unbiased_intragroup_variance_df = self.get_unbiased_intragroup_variance_for_group( + chunk_df, + means_over_samples_df, + group_overall_mean, + samples + ) + + # storing intragroup values for each gene in this group + unbiased_intragroup_variance_dfs.append(group_unbiased_intragroup_variance_df) + # storing means over samples in this group for each gene + means_over_samples_df = means_over_samples_df.rename({"mean_over_samples_for_gene": group}) + means_over_samples_dfs.append(means_over_samples_df) + # storing overall mean of expression in this group, for all genes and samples + group_overall_means.append(group_overall_mean) + + # cast all values to float (to avoid issues when concat) + unbiased_intragroup_variance_dfs = [ + df.select([pl.col(col).cast(pl.Float64) for col in df.columns]) + for df in unbiased_intragroup_variance_dfs + ] + + # before returning: + # concatenate together all intragroup variance data to have a single df for all groups + # stack all means over samples horizontally (becomes a gene * group df ) + # get the mean of group_overall_means to get the overall mean expression value in the count dataframe + + return ( + pl.concat(unbiased_intragroup_variance_dfs), + pl.concat(means_over_samples_dfs, how="horizontal"), + mean(group_overall_means) ) - .select([ - (pl.col(c) - pl.col("mean_over_groups_for_gene")).alias(c) - for c in gene_means_in_groups_df.columns - ]) - .transpose(column_names=genes) - .hstack([mean_over_genes]) - .select([ - (pl.col(c) - pl.col("mean_over_genes_for_group") + dataset_overall_mean).alias(c) - for c in genes - ]) - .select([ (pl.col(c) ** 2).alias(c) for c in genes ]) # square to get variance - ) -def compute_gamma_factor(diff_df, vardiff_df): - first_term = ( - diff_df - .with_columns( - sum_of_squares=pl.concat_list(pl.all()).list.drop_nulls().list.sum() # sum over columns + def adjust_for_nb_of_samples_in_groups(self, unbiased_intragroup_variance_df): + n_samples_list = [ len(samples) for samples in self.group_to_samples_dict.values() ] + return ( + unbiased_intragroup_variance_df + .with_columns( + n_samples=pl.Series(n_samples_list) + ) + .select([ + (pl.col(c) / pl.col("n_samples")).alias(c) + for c in self.genes + ]) ) - .select("sum_of_squares") - .sum() # sum over rows - .select( - ( pl.col("sum_of_squares") / ((n_groups - 1) * (k - 1)) ).alias("normalised_sum_of_squares") + + def get_unbiased_intergroup_variance(self, gene_means_in_groups_df, dataset_overall_mean): + mean_over_genes = gene_means_in_groups_df.mean().transpose().to_series().rename("mean_over_genes_for_group") + + return ( + gene_means_in_groups_df + .with_columns( + mean_over_groups_for_gene=pl.concat_list(pl.all()).row.mean() + ) + .select([ + (pl.col(c) - pl.col("mean_over_groups_for_gene")).alias(c) + for c in gene_means_in_groups_df.columns + ]) + .transpose(column_names=self.genes) + .hstack([mean_over_genes]) + .select([ + (pl.col(c) - pl.col("mean_over_genes_for_group") + dataset_overall_mean).alias(c) + for c in self.genes + ]) + .select([ (pl.col(c) ** 2).alias(c) for c in self.genes ]) # square to get variance ) - .item() - ) - second_term = ( - vardiff_df - .with_columns( - sum=pl.concat_list(pl.all()).list.drop_nulls().list.sum() # sum over columns + + def compute_gamma_factor(self, diff_df, vardiff_df): + logger.info("Computing gamma factor") + first_term = ( + diff_df + .with_columns( + sum_of_squares=pl.concat_list(pl.all()).row.sum() # sum over columns + ) + .select("sum_of_squares") + .sum() # sum over rows + .select( + ( pl.col("sum_of_squares") / ((self.n_groups - 1) * (self.n_genes - 1)) ).alias("normalised_sum_of_squares") + + ) + .item() ) - .select("sum") - .sum() # sum over rows - .select( - (pl.col("sum") / (n_groups* k)).alias("normalised_sum") + second_term = ( + vardiff_df + .with_columns( + sum=pl.concat_list(pl.all()).row.sum() # sum over columns + ) + .select("sum") + .sum() # sum over rows + .select( + (pl.col("sum") / (self.n_groups * self.n_genes)).alias("normalised_sum") + + ) + .item() ) - .item() - ) - return max(first_term - second_term, 0) + return max(first_term - second_term, 0) -def apply_gamma_factor(gamma, diff_df, vardiff_df): - difnew = diff_df * gamma / (gamma + vardiff_df) - varnew = vardiff_df + gamma * vardiff_df / (gamma + vardiff_df) - return difnew, varnew + def apply_gamma_factor(self, gamma, diff_df, vardiff_df): + logger.info("Shrinking intragroup and intergroup variances using gamma factor") + difnew = diff_df * gamma / (gamma + vardiff_df) + varnew = vardiff_df + gamma * vardiff_df / (gamma + vardiff_df) + return difnew, varnew -def get_stability_values(unbiased_intragroup_variance_dfs, means_over_samples_dfs, group_overall_means): + def apply_shrinkage(self, intergroup_variance_df, group_mean_variance_df): + gamma = self.compute_gamma_factor(intergroup_variance_df, group_mean_variance_df) + return self.apply_gamma_factor(gamma, intergroup_variance_df, group_mean_variance_df) - dataset_overall_mean = mean(group_overall_means) - # putting together all intragroup variances - unbiased_intragroup_variance_df = pl.concat(unbiased_intragroup_variance_dfs) + def compute_stability_values(self): - group_mean_variance_df = adjust_for_nb_of_samples_in_groups(unbiased_intragroup_variance_df, n_samples_list) + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # UNBIASED INTRAGROUP VARIANCE + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + logger.info("Computing intragroup variances") + intragroup_variance_df, gene_means_in_groups_df, dataset_overall_mean = self.get_unbiased_intragroup_variances() - # putting together all means over samples for each group - gene_means_in_groups_df = pl.concat(means_over_samples_dfs, how="horizontal") - # adding mean over genes for each group (no need to compute it again) - # gene_means_in_groups_df = pl.concat([gene_means_in_groups_df, group_overall_mean_df]) + logger.info("Adjusting variances by group size") + group_mean_variance_df = self.adjust_for_nb_of_samples_in_groups(intragroup_variance_df) - intergroup_variance_df = get_unbiased_intergroup_variance(gene_means_in_groups_df, dataset_overall_mean) + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # INTERGROUP VARIANCE + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + logger.info("Computing intergroup variances") + intergroup_variance_df = self.get_unbiased_intergroup_variance(gene_means_in_groups_df, dataset_overall_mean) - gamma = compute_gamma_factor(intergroup_variance_df, group_mean_variance_df) + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # STABILITY VALUES + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - shrunk_intergroup_variance_df, shrunk_group_mean_variance_df = apply_gamma_factor(gamma, intergroup_variance_df, group_mean_variance_df) + shrunk_intervar_df, shrunk_gr_mean_var_df = self.apply_shrinkage(intergroup_variance_df, group_mean_variance_df) - return ( - ( - shrunk_intergroup_variance_df.select([pl.col(c).abs() for c in genes]) - + shrunk_group_mean_variance_df.select([pl.col(c).sqrt() for c in genes]) + logger.info("Computing stability values") + return ( + ( + shrunk_intervar_df.select([pl.col(c).abs() for c in self.genes]) + + shrunk_gr_mean_var_df.select([pl.col(c).sqrt() for c in self.genes]) + ) + .mean() ) - .mean() + + +##################################################### +# FUNCTIONS +##################################################### + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Quantile normalise count data for each sample in the dataset" + ) + parser.add_argument( + "--counts", type=Path, dest="count_file", required=True, help="Count file" ) + parser.add_argument( + "--design", type=Path, dest="design_file", required=True, help="Design file" + ) + + return parser.parse_args() + + +def export_stability(stabilities: pl.DataFrame): + """Export stability values to CSV file.""" + logger.info(f"Exporting stability values to: {STABILITY_OUTFILENAME}") + stabilities.write_csv(STABILITY_OUTFILENAME) + + +def main(): + + args = parse_args() + + logger.info(f"Getting counts from {args.count_file}") + count_lf = pl.scan_parquet(args.count_file) + + logger.info(f"Getting design from {args.design_file}") + design_df = pl.read_csv(args.design_file) + nfd = NormFinder(count_lf, design_df) + stabilities = nfd.compute_stability_values() -unbiased_intragroup_variance_dfs = [] -means_over_samples_dfs = [] -group_overall_means = [] -n_samples_list = [] -cpt=0 -for group, samples in tqdm(group_to_samples_dict.items()): - cpt+=1 - if cpt > 30: - break - chunk_df = count_lf.select(samples).collect() - means_over_samples_df = get_means_over_samples(chunk_df) - group_overall_mean = get_overall_mean_for_group(means_over_samples_df) - unbiased_intragroup_variance_df = get_unbiased_intragroup_variance(chunk_df, means_over_samples_df, group_overall_mean, samples) - # storing intragroup values for each gene in this group - unbiased_intragroup_variance_dfs.append(unbiased_intragroup_variance_df) - # storing means over samples in this group for each gene - means_over_samples_df = means_over_samples_df.rename({"mean_over_samples_for_gene": group}) - means_over_samples_dfs.append(means_over_samples_df) - # storing overall mean of expression in this group, for all genes and samples - group_overall_means.append(group_overall_mean) - # storing nb of samples in this group - n_samples_list.append(len(samples)) - - - - - - -stab = get_stability_values(unbiased_intragroup_variance_dfs, means_over_samples_dfs, group_overall_means) -#print(stab) - - - - - -""" -import time -s_squared = {} -overall_s_squared_factor = {} -for group, samples in tqdm(batch_condition_to_sample_dict.items()): - overall_group_mean = batch_condition_mean_dict[group] - nb_samples = len(samples) - s_squared[group] = {} - - for gene in tqdm(genes): - sample_ys = { - k: v - for k, v in lf.filter(pl.col(ENSEMBL_GENE_ID_COLNAME) == gene).select(samples).collect().to_dicts()[0].items() - if v is not None - } - if not sample_ys: - s_squared[group][gene] = None - continue - mean_over_samples_for_gene = mean(sample_ys.values()) - sample_r_squares = [] - for sample in sample_ys: - r_square = sample_ys[sample] - mean_over_samples_for_gene - sample_mean_dict[sample] + overall_group_mean - sample_r_squares.append(r_square) - factor = nb_samples - 1 if nb_samples > 1 else 1 - s_squared[group][gene] = sum(sample_r_squares) / ( factor * (1 - 2 / nb_genes)) - - # sum of s squared for all genes - overall_s_squared_factor[group] = sum(s_squared[group].values()) - -sigma_squared = {} -for group, samples in batch_condition_to_sample_dict.items(): - sigma_squared[group] = {} - s_squared_factor = overall_s_squared_factor[group] - for gene in genes: - if gene not in s_squared[group]: - sigma_squared[group][gene] = None - continue - sigma_squared[group][gene] = s_squared[group][gene] - s_squared_factor / nb_genes * (nb_genes - 1) -""" + logger.info(f"Stability values:\n{stabilities}") + export_stability(stabilities) +if __name__ == "__main__": + main() diff --git a/modules/local/normfinder/main.nf b/modules/local/normfinder/main.nf new file mode 100644 index 00000000..e1a8e8e7 --- /dev/null +++ b/modules/local/normfinder/main.nf @@ -0,0 +1,31 @@ +process NORMFINDER { + + label 'process_single' + + conda "${moduleDir}/spec-file.txt" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0e/0e0445114887dd260f1632afe116b1e81e02e1acc74a86adca55099469b490d9/data': + 'community.wave.seqera.io/library/numba_numpy_polars_tqdm:6923cfab6fc04dec' }" + + input: + path count_file + path design_file + + output: + path('stabilities.csv'), emit: stabilities + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions + tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions + + script: + """ + normfinder.py \ + --counts $count_file \ + --design $design_file + """ + + stub: + """ + touch stabilities.csv + """ + +} diff --git a/modules/local/normfinder/spec-file.txt b/modules/local/normfinder/spec-file.txt new file mode 100644 index 00000000..07a547e8 --- /dev/null +++ b/modules/local/normfinder/spec-file.txt @@ -0,0 +1,43 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_5.conda#dcd5ff1940cd38f6df777cac86819d60 +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_5.conda#264fbfba7fb20acf3b29cde153e345ce +https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda#0be7c6e070c19105f966d3758448d018 +https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda#4211416ecba1866fab0c6470986c22d6 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 +https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc +https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda#0b367fad34931cb79e0d6b7e5c06bb1c +https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.1-he9a06e4_0.conda#af930c65e9a79a3423d6d36e265cef65 +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda#74784ee3d225fc3dca89edb635b4e5cc +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda#ffffb341206dd0dab0c36053c048d621 +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 +https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 +https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 +https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a +https://conda.anaconda.org/conda-forge/linux-64/python-3.13.7-h2b335a9_100_cp313.conda#724dcf9960e933838247971da07fe5cf +https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.7-py313hd8ed1ab_100.conda#c5623ddbd37c5dafa7754a83f97de01e +https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.7-h4df99d1_100.conda#47a123ca8e727d886a2c6d0c71658f8c +https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda#aaa2a381ccc56eac91d63b6c1240312f +https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda#962b9857ee8e7018c22f2776ffa0b2d7 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_5.conda#fbd4008644add05032b6764807ee2cba +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_5.conda#0c91408b3dec0b97e8a3c694845bd63b +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_2.conda#dfc5aae7b043d9f56ba99514d5e60625 +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-35_h4a7cf45_openblas.conda#6da7e852c812a84096b68158574398d0 +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-35_h0358290_openblas.conda#8aa3389d36791ecd31602a247b1f3641 +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-35_h47877c9_openblas.conda#aa0b36b71d44f74686f13b9bfabec891 +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_5.conda#4e02a49aaa9d5190cb630fa43528fbe6 +https://conda.anaconda.org/conda-forge/linux-64/llvmlite-0.44.0-py313hfdae721_2.conda#dd0d7947635c0c524608eab7db55dcc9 +https://conda.anaconda.org/conda-forge/linux-64/numpy-2.2.6-py313h17eae1a_0.conda#7a2d2f9adecd86ed5c29c2115354f615 +https://conda.anaconda.org/conda-forge/linux-64/numba-0.61.2-py313h50b8c88_1.conda#53c79b7cdee329ed4c77cafe27600cdb +https://conda.anaconda.org/conda-forge/noarch/pip-25.2-pyh145f28c_0.conda#e7ab34d5a93e0819b62563c78635d937 +https://conda.anaconda.org/conda-forge/linux-64/polars-default-1.33.1-py39hf521cc8_0.conda#900f486d119d5c83d14c812068a3ecad +https://conda.anaconda.org/conda-forge/linux-64/polars-1.33.1-default_h755bcc6_0.conda#1884a1a6acc457c8e4b59b0f6450e140 +https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda#9efbfdc37242619130ea42b1cc4ed861 diff --git a/tests/modules/local/normfinder/main.nf.test b/tests/modules/local/normfinder/main.nf.test new file mode 100644 index 00000000..b3ee40cb --- /dev/null +++ b/tests/modules/local/normfinder/main.nf.test @@ -0,0 +1,44 @@ +nextflow_process { + + name "Test Process NORMFINDER" + script "modules/local/normfinder/main.nf" + process "NORMFINDER" + tag "normfinder" + + test("Very small dataset - Cq values") { + + when { + process { + """ + input[0] = file( '$projectDir/tests/test_data/normfinder/very_small_cq/all_counts.normalised.parquet', checkIfExists: true) + input[1] = file( '$projectDir/tests/test_data/normfinder/very_small_cq/design.csv', checkIfExists: true) + """ + } + } + + then { + assert process.success + assert snapshot(process.out).match() + } + + } + + test("Small dataset - Real expression values") { + + when { + process { + """ + input[0] = file( '$projectDir/tests/test_data/normfinder/small_normalised/all_counts.normalised.parquet', checkIfExists: true) + input[1] = file( '$projectDir/tests/test_data/normfinder/small_normalised/design.csv', checkIfExists: true) + """ + } + } + + then { + assert process.success + assert snapshot(process.out).match() + } + + } + +} diff --git a/tests/modules/local/normfinder/main.nf.test.snap b/tests/modules/local/normfinder/main.nf.test.snap new file mode 100644 index 00000000..c0fe114b --- /dev/null +++ b/tests/modules/local/normfinder/main.nf.test.snap @@ -0,0 +1,64 @@ +{ + "Small dataset - Real expression values": { + "content": [ + { + "0": [ + "stabilities.csv:md5,c93b8577b3f2073e04881323e711e137" + ], + "1": [ + [ + "NORMFINDER", + "python", + "3.13.7" + ] + ], + "2": [ + [ + "NORMFINDER", + "polars", + "1.33.1" + ] + ], + "stabilities": [ + "stabilities.csv:md5,c93b8577b3f2073e04881323e711e137" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.6" + }, + "timestamp": "2025-09-13T18:08:22.097656317" + }, + "Very small dataset - Cq values": { + "content": [ + { + "0": [ + "stabilities.csv:md5,81863dc328421715ae9580ede5bbc318" + ], + "1": [ + [ + "NORMFINDER", + "python", + "3.13.7" + ] + ], + "2": [ + [ + "NORMFINDER", + "polars", + "1.33.1" + ] + ], + "stabilities": [ + "stabilities.csv:md5,81863dc328421715ae9580ede5bbc318" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.6" + }, + "timestamp": "2025-09-13T17:40:05.554966039" + } +} \ No newline at end of file diff --git a/tests/test_data/normfinder/all_counts.normfinder.csv b/tests/test_data/normfinder/all_counts.normfinder.csv deleted file mode 100644 index 63bf33ec..00000000 --- a/tests/test_data/normfinder/all_counts.normfinder.csv +++ /dev/null @@ -1,6 +0,0 @@ -ensembl_gene_id,S1,S2,S3,S4,S5,S6 -GAPDH,23.1,23.5,20.2,20.7,24.9,25.2 -TT1,21.5,26.5,25.6,28.2,21.5,26.5 -TT2,22.5,27.5,25.9,6.2,25.5,30.5 -TT3,28.5,25.5,25.6,27.2,21.5,26.5 -TT4,22.5,22.5,21.6,22.2,21.5,23.5 diff --git a/tests/test_data/normfinder/convert_to_parquet.py b/tests/test_data/normfinder/convert_to_parquet.py deleted file mode 100644 index 433171dd..00000000 --- a/tests/test_data/normfinder/convert_to_parquet.py +++ /dev/null @@ -1,4 +0,0 @@ -import polars as pl - -df = pl.read_csv("/home/olivier/repositories/nf-core-stableexpression/tests/all_counts.normfinder.csv") -df.write_parquet("/home/olivier/repositories/nf-core-stableexpression/tests/all_counts.normalised.parquet") diff --git a/tests/test_data/normfinder/small_normalised/all_counts.normalised.parquet b/tests/test_data/normfinder/small_normalised/all_counts.normalised.parquet new file mode 100644 index 0000000000000000000000000000000000000000..23f39583a57feebd5185deb598099a3e0de6d7f0 GIT binary patch literal 6708 zcmb_h3sh6b7QKXzVDVwqdnGmaMMTAtgai_8d%O^+CLc`8t~R?&(gQbcs&(h3$q{v>GC`mESDbCV%yUy9eY-d#ZE=FHi1&Ym;p zoLL03Arjh(er8RVSp?9oG(}Oah3;LmmQghVYJjB!diPa_hNxMwTr8FmNwkG^us>GS)LaeK$3N#4ys zR&e`1E^yvW9XR)?=*ay*3m;k}{1JXg3sYx2@!y%KgD1YHu7*Zk1o8Ja3;JKt!S3}I zPm8}e33-FCiFH7A4%hAerXDKu>VCD$Z3nuya3p0TbR6s(6&S{PM@b|KFVCojDbcC1 z3!;2OLX--Hhuq7P<1ELLDX?@Yvcja9b>2KpLvkZV=Ip^V$w#d|8JwD9hNdg=HM2G> zs|H4wTsY@pE$sf8MFivl(bXyshsAAe1=;d3_uN;u!sXHlUd2oUM9mvC;$dMm%v_`TWkOSy9Y+&!z|?v3G+hhusai3cqv_-G zZIRn{nxV<@AJ-CA*Hyxc^Y4CfX~>gGAAHy-ipYt?b|_auuS}G*LN?a*FaqMVfOQ!au|Ad+uRrDYk1P6Frlfc4@VPm z0MXR#ym^}PE1z7k%fK{YDmpjrFhkQ)=_WWi>ISU2bbWBVA;OfLsFa#DT!V7hg#Z!SaxB+e{u%}T!-a<cWq1#x&^@}nTw7*hBys2z9YMQcqJkRUlee#)(mWy>F;Fr@;35bRxuVYVyxiCCzTW+H&Yb(i zEK{e^;kg0a*#zS(_tovj)cZA(pWb6L`W>}rdIpJFjL_4Lwoi6g87EmftO6S~z zySTzTU4%GPxT6}|(C(Kmaz4@ymo}9P1dbix89b~i%(A4r#{7fJ5*G}e(S`0>Mmm_D zc3;W`zux_|uAKYCEE4?KDf8|>wh!61qf3i+AH_L&aA!Q!H26{dL&Y*5h7T1~T|N2$ zQ^Y+)I{-fYlUuOS|F=!2 zcQpW4I|nxd+H87w-~%A5RGvPM%I8&B_Xck2ir42CZrbv!3%x@t1GVC4{Dg3`KF1yBmI?V|OELDkg0x5|ak-Phvxl zgT$oqF%^^c0f|Y$Hx-j|i^QbRnTkmvLt>KPO~oXsA~DI!Slrv-7)d%vOcI2tm>hB> zCI`q=OoASXNr0J($<`t<*$0DI#B^HFM=cP)lu*PJSuMZlxf#cqV)_m@pB4!3PO@6~(MOADM|6=KDfpjD1hfN6zQuM@_6+W8O68?MZ$8I zY)jGq^X|_)Rnm^kBM*8y<fyr7Y#xuW%VqR?l*>tbp4dEZnan|q zy+Mw#*S^UUo9ESxIg7DNIqaJ>Pi&s|8KxRzul2-s=e)%ao9`un(PHe29Cqhmp4dD) zXJ+jLgq`Vy?S3@88@s2o!Luo4vN3WohurDH3z=uX#}r}YmmIQo3@>D!trSy%k3QXS#z=M$Ns@d#2S z#l5&q3Xn5j0aga(Q4diH5`D1_oo)0$G9qrX?@{z8$Dr0Pqe7qlk<*&(y z9yvVNt4BWOFGg}bAF)`&$cJxwkSa!M!nGgfI!G0tp$>~jcKd{5fB&8j)dC|QGK6f5 zkH9crnL1;-k!x`T=2{k{3X}xlol+Lg?Ag+$=kii37}8Xb!;Ku~hQ{*} zyb#M#d`v@Dpd;n|I{4FAHYfw@ry^keAx9wS@%BT1(D@9-`2c-}7}hl;0F)olgHi($ zDe&~9*(vIz*;6N{MoQ--%}$Ppi=L`sDMy>&kCLJnrV7hZTl>l%YS Date: Sat, 13 Sep 2025 19:04:07 +0200 Subject: [PATCH 056/258] reinsert genorm files --- .../compute_m_measure/environment.yml | 6 ++ .../compute_m_measure/main.nf | 27 ++++++ .../cross_join/environment.yml | 6 ++ .../cross_join/main.nf | 26 ++++++ .../expression_ratio/environment.yml | 6 ++ .../expression_ratio/main.nf | 26 ++++++ .../make_chunks/environment.yml | 6 ++ .../make_chunks/main.nf | 26 ++++++ .../ratio_standard_variation/environment.yml | 6 ++ .../ratio_standard_variation/main.nf | 26 ++++++ .../local/pairwise_gene_variation/main.nf | 82 ++++++++++++++++++ .../compute_m_measure/main.nf.test | 29 +++++++ .../compute_m_measure/main.nf.test.snap | 64 ++++++++++++++ .../cross_join/main.nf.test | 29 +++++++ .../cross_join/main.nf.test.snap | 33 +++++++ .../expression_ratio/main.nf.test | 27 ++++++ .../expression_ratio/main.nf.test.snap | 33 +++++++ .../make_chunks/main.nf.test | 27 ++++++ .../make_chunks/main.nf.test.snap | 43 +++++++++ .../ratio_standard_variation/main.nf.test | 27 ++++++ .../main.nf.test.snap | 33 +++++++ .../pairwise_gene_variation/main.nf.test | 49 +++++++++++ .../pairwise_gene_variation/main.nf.test.snap | 19 ++++ .../pairwise_gene_variation/run_genorm.py | 43 +++++++++ .../compute_m_measure/input/std.0.0.parquet | Bin 0 -> 593674 bytes .../compute_m_measure/input/std.0.1.parquet | Bin 0 -> 1354760 bytes .../compute_m_measure/input/std.0.2.parquet | Bin 0 -> 1353997 bytes .../compute_m_measure/input/std.0.3.parquet | Bin 0 -> 451970 bytes .../compute_m_measure/input/std.1.1.parquet | Bin 0 -> 600749 bytes .../compute_m_measure/input/std.1.2.parquet | Bin 0 -> 1367867 bytes .../compute_m_measure/input/std.1.3.parquet | Bin 0 -> 455299 bytes .../compute_m_measure/input/std.2.2.parquet | Bin 0 -> 601884 bytes .../compute_m_measure/input/std.2.3.parquet | Bin 0 -> 455320 bytes .../compute_m_measure/input/std.3.3.parquet | Bin 0 -> 55087 bytes .../cross_join/output/cross_join.0.1.parquet | Bin 0 -> 16244 bytes .../output/ratios.0.1.parquet | Bin 0 -> 1345215 bytes .../make_chunks/input/counts.head.parquet | Bin 0 -> 3732 bytes .../make_chunks/input/counts.parquet | Bin 0 -> 45686 bytes .../make_chunks/output/count_chunk.0.parquet | Bin 0 -> 6433 bytes .../make_chunks/output/count_chunk.1.parquet | Bin 0 -> 6375 bytes .../make_chunks/output/count_chunk.2.parquet | Bin 0 -> 6383 bytes .../output/std.0.1.parquet | Bin 0 -> 639448 bytes .../output/std.0.2.parquet | Bin 0 -> 638182 bytes 43 files changed, 699 insertions(+) create mode 100644 modules/local/pairwise_gene_variation/compute_m_measure/environment.yml create mode 100644 modules/local/pairwise_gene_variation/compute_m_measure/main.nf create mode 100644 modules/local/pairwise_gene_variation/cross_join/environment.yml create mode 100644 modules/local/pairwise_gene_variation/cross_join/main.nf create mode 100644 modules/local/pairwise_gene_variation/expression_ratio/environment.yml create mode 100644 modules/local/pairwise_gene_variation/expression_ratio/main.nf create mode 100644 modules/local/pairwise_gene_variation/make_chunks/environment.yml create mode 100644 modules/local/pairwise_gene_variation/make_chunks/main.nf create mode 100644 modules/local/pairwise_gene_variation/ratio_standard_variation/environment.yml create mode 100644 modules/local/pairwise_gene_variation/ratio_standard_variation/main.nf create mode 100644 subworkflows/local/pairwise_gene_variation/main.nf create mode 100644 tests/modules/local/pairwise_gene_variation/compute_m_measure/main.nf.test create mode 100644 tests/modules/local/pairwise_gene_variation/compute_m_measure/main.nf.test.snap create mode 100644 tests/modules/local/pairwise_gene_variation/cross_join/main.nf.test create mode 100644 tests/modules/local/pairwise_gene_variation/cross_join/main.nf.test.snap create mode 100644 tests/modules/local/pairwise_gene_variation/expression_ratio/main.nf.test create mode 100644 tests/modules/local/pairwise_gene_variation/expression_ratio/main.nf.test.snap create mode 100644 tests/modules/local/pairwise_gene_variation/make_chunks/main.nf.test create mode 100644 tests/modules/local/pairwise_gene_variation/make_chunks/main.nf.test.snap create mode 100644 tests/modules/local/pairwise_gene_variation/ratio_standard_variation/main.nf.test create mode 100644 tests/modules/local/pairwise_gene_variation/ratio_standard_variation/main.nf.test.snap create mode 100644 tests/subworkflows/local/pairwise_gene_variation/main.nf.test create mode 100644 tests/subworkflows/local/pairwise_gene_variation/main.nf.test.snap create mode 100644 tests/subworkflows/local/pairwise_gene_variation/run_genorm.py create mode 100644 tests/test_data/pairwise_gene_variation/compute_m_measure/input/std.0.0.parquet create mode 100644 tests/test_data/pairwise_gene_variation/compute_m_measure/input/std.0.1.parquet create mode 100644 tests/test_data/pairwise_gene_variation/compute_m_measure/input/std.0.2.parquet create mode 100644 tests/test_data/pairwise_gene_variation/compute_m_measure/input/std.0.3.parquet create mode 100644 tests/test_data/pairwise_gene_variation/compute_m_measure/input/std.1.1.parquet create mode 100644 tests/test_data/pairwise_gene_variation/compute_m_measure/input/std.1.2.parquet create mode 100644 tests/test_data/pairwise_gene_variation/compute_m_measure/input/std.1.3.parquet create mode 100644 tests/test_data/pairwise_gene_variation/compute_m_measure/input/std.2.2.parquet create mode 100644 tests/test_data/pairwise_gene_variation/compute_m_measure/input/std.2.3.parquet create mode 100644 tests/test_data/pairwise_gene_variation/compute_m_measure/input/std.3.3.parquet create mode 100644 tests/test_data/pairwise_gene_variation/cross_join/output/cross_join.0.1.parquet create mode 100644 tests/test_data/pairwise_gene_variation/expression_ratio/output/ratios.0.1.parquet create mode 100644 tests/test_data/pairwise_gene_variation/make_chunks/input/counts.head.parquet create mode 100644 tests/test_data/pairwise_gene_variation/make_chunks/input/counts.parquet create mode 100644 tests/test_data/pairwise_gene_variation/make_chunks/output/count_chunk.0.parquet create mode 100644 tests/test_data/pairwise_gene_variation/make_chunks/output/count_chunk.1.parquet create mode 100644 tests/test_data/pairwise_gene_variation/make_chunks/output/count_chunk.2.parquet create mode 100644 tests/test_data/pairwise_gene_variation/ratio_standard_variation/output/std.0.1.parquet create mode 100644 tests/test_data/pairwise_gene_variation/ratio_standard_variation/output/std.0.2.parquet diff --git a/modules/local/pairwise_gene_variation/compute_m_measure/environment.yml b/modules/local/pairwise_gene_variation/compute_m_measure/environment.yml new file mode 100644 index 00000000..a06d640d --- /dev/null +++ b/modules/local/pairwise_gene_variation/compute_m_measure/environment.yml @@ -0,0 +1,6 @@ +name: compute_m_measure +channels: + - conda-forge +dependencies: + - conda-forge::python==3.12.8 + - conda-forge::polars==1.17.1 diff --git a/modules/local/pairwise_gene_variation/compute_m_measure/main.nf b/modules/local/pairwise_gene_variation/compute_m_measure/main.nf new file mode 100644 index 00000000..5e55fd8d --- /dev/null +++ b/modules/local/pairwise_gene_variation/compute_m_measure/main.nf @@ -0,0 +1,27 @@ +process COMPUTE_M_MEASURE { + + // label 'process_medium' + publishDir "${params.outdir}/pairwise_gene_variation/m_measures" + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': + 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" + + input: + path count_file + path files + + output: + path 'm_measures.csv', emit: m_measures + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions + tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions + + + script: + def args = "--task-attempts ${task.attempt}" + """ + compute_m_measures.py --counts $count_file --std-files "$files" $args + """ + +} diff --git a/modules/local/pairwise_gene_variation/cross_join/environment.yml b/modules/local/pairwise_gene_variation/cross_join/environment.yml new file mode 100644 index 00000000..d3991e07 --- /dev/null +++ b/modules/local/pairwise_gene_variation/cross_join/environment.yml @@ -0,0 +1,6 @@ +name: cross_join +channels: + - conda-forge +dependencies: + - conda-forge::python==3.12.8 + - conda-forge::polars==1.17.1 diff --git a/modules/local/pairwise_gene_variation/cross_join/main.nf b/modules/local/pairwise_gene_variation/cross_join/main.nf new file mode 100644 index 00000000..2bc06d19 --- /dev/null +++ b/modules/local/pairwise_gene_variation/cross_join/main.nf @@ -0,0 +1,26 @@ +process CROSS_JOIN { + + // label 'process_medium' + publishDir "${params.outdir}/pairwise_gene_variation/cross_joins" + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': + 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" + + input: + tuple val(meta), path("count_chunk_file_1"), path("count_chunk_file_2") + + output: + path 'cross_join.*.parquet', emit: data + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions + tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions + + + script: + def args = "--task-attempts ${task.attempt}" + """ + make_cross_join.py --file1 count_chunk_file_1 --file2 count_chunk_file_2 --index1 ${meta.index_1} --index2 ${meta.index_2} $args + """ + +} diff --git a/modules/local/pairwise_gene_variation/expression_ratio/environment.yml b/modules/local/pairwise_gene_variation/expression_ratio/environment.yml new file mode 100644 index 00000000..3a094c34 --- /dev/null +++ b/modules/local/pairwise_gene_variation/expression_ratio/environment.yml @@ -0,0 +1,6 @@ +name: expression_ratio +channels: + - conda-forge +dependencies: + - conda-forge::python==3.12.8 + - conda-forge::polars==1.17.1 diff --git a/modules/local/pairwise_gene_variation/expression_ratio/main.nf b/modules/local/pairwise_gene_variation/expression_ratio/main.nf new file mode 100644 index 00000000..be173625 --- /dev/null +++ b/modules/local/pairwise_gene_variation/expression_ratio/main.nf @@ -0,0 +1,26 @@ +process EXPRESSION_RATIO { + + // label 'process_medium' + publishDir "${params.outdir}/pairwise_gene_variation/expression_ratios" + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': + 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" + + input: + path file + + output: + path 'ratios.*.parquet', emit: data + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions + tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions + + + script: + def args = "--task-attempts ${task.attempt}" + """ + make_pairwise_gene_expression_ratio.py --file $file + """ + +} diff --git a/modules/local/pairwise_gene_variation/make_chunks/environment.yml b/modules/local/pairwise_gene_variation/make_chunks/environment.yml new file mode 100644 index 00000000..125bbcea --- /dev/null +++ b/modules/local/pairwise_gene_variation/make_chunks/environment.yml @@ -0,0 +1,6 @@ +name: make_chunks +channels: + - conda-forge +dependencies: + - conda-forge::python==3.12.8 + - conda-forge::polars==1.17.1 diff --git a/modules/local/pairwise_gene_variation/make_chunks/main.nf b/modules/local/pairwise_gene_variation/make_chunks/main.nf new file mode 100644 index 00000000..2b0fb8a1 --- /dev/null +++ b/modules/local/pairwise_gene_variation/make_chunks/main.nf @@ -0,0 +1,26 @@ +process MAKE_CHUNKS { + + // label 'process_medium' + publishDir "${params.outdir}/pairwise_gene_variation/chunks" + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': + 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" + + input: + path count_file + + output: + path 'count_chunk.*.parquet', emit: chunks + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions + tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions + + + script: + def args = "--task-attempts ${task.attempt}" + """ + make_parquet_chunks.py --counts $count_file $args + """ + +} diff --git a/modules/local/pairwise_gene_variation/ratio_standard_variation/environment.yml b/modules/local/pairwise_gene_variation/ratio_standard_variation/environment.yml new file mode 100644 index 00000000..40d9b7d6 --- /dev/null +++ b/modules/local/pairwise_gene_variation/ratio_standard_variation/environment.yml @@ -0,0 +1,6 @@ +name: ratio_standard_variation +channels: + - conda-forge +dependencies: + - conda-forge::python==3.12.8 + - conda-forge::polars==1.17.1 diff --git a/modules/local/pairwise_gene_variation/ratio_standard_variation/main.nf b/modules/local/pairwise_gene_variation/ratio_standard_variation/main.nf new file mode 100644 index 00000000..b25fb235 --- /dev/null +++ b/modules/local/pairwise_gene_variation/ratio_standard_variation/main.nf @@ -0,0 +1,26 @@ +process RATIO_STANDARD_VARIATION { + + // label 'process_medium' + publishDir "${params.outdir}/pairwise_gene_variation/ratio_standard_variations" + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': + 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" + + input: + path file + + output: + path 'std.*.parquet', emit: data + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions + tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions + + + script: + def args = "--task-attempts ${task.attempt}" + """ + get_ratio_standard_variation.py --file $file $args + """ + +} diff --git a/subworkflows/local/pairwise_gene_variation/main.nf b/subworkflows/local/pairwise_gene_variation/main.nf new file mode 100644 index 00000000..c7ae3c4b --- /dev/null +++ b/subworkflows/local/pairwise_gene_variation/main.nf @@ -0,0 +1,82 @@ +// +// Subworkflow with functionality specific to the nf-core/stableexpression pipeline +// + +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + IMPORT MODULES / SUBWORKFLOWS / FUNCTIONS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ + +include { MAKE_CHUNKS } from '../../../modules/local/pairwise_gene_variation/make_chunks/main' +include { CROSS_JOIN } from '../../../modules/local/pairwise_gene_variation/cross_join/main' +include { EXPRESSION_RATIO } from '../../../modules/local/pairwise_gene_variation/expression_ratio/main' +include { RATIO_STANDARD_VARIATION } from '../../../modules/local/pairwise_gene_variation/ratio_standard_variation/main' +include { COMPUTE_M_MEASURE } from '../../../modules/local/pairwise_gene_variation/compute_m_measure/main' + +/* +======================================================================================== + SUBWORKFLOW TO COMPUTE PAIRWISE GENE VARIATION +======================================================================================== +*/ + +workflow PAIRWISE_GENE_VARIATION { + + take: + ch_counts + + + main: + + MAKE_CHUNKS( ch_counts ) + + // we need to flatten to set each chunk file as a separate item in the channel + ch_count_chunks = MAKE_CHUNKS.out.chunks.flatten() + getUniqueFilePairs( ch_count_chunks ) | CROSS_JOIN + + CROSS_JOIN.out.data | EXPRESSION_RATIO + + EXPRESSION_RATIO.out.data | RATIO_STANDARD_VARIATION + + COMPUTE_M_MEASURE( + ch_counts, + RATIO_STANDARD_VARIATION.out.data.collect() + ) + + emit: + m_measures = COMPUTE_M_MEASURE.out.m_measures + +} + + + +/* +======================================================================================== + FUNCTIONS +======================================================================================== +*/ + +// +// Generate channels consisting of unique pairs of files +// Gets +// +def getUniqueFilePairs( ch_count_chunks ) { + + ch_count_chunks_with_indexes = ch_count_chunks + .map { file -> [file.name.tokenize('.')[1], file] } // extract file index + + return ch_count_chunks_with_indexes + .combine( ch_count_chunks_with_indexes ) // full cartesian product with itself + .map { // steps not mandatory but helps to make the filter clearer + index_1, file_1, index_2, file_2 -> + [index_1: index_1, index_2: index_2, file_1: file_1, file_2: file_2] + } + .filter { it -> it.index_1 <= it.index_2 } // keeps only pairs where i <= j + .map { + it -> + def meta = [index_1: it.index_1, index_2: it.index_2] // puts indexes in a meta tuple + [ meta, it.file_1, it.file_2 ] + } +} + + diff --git a/tests/modules/local/pairwise_gene_variation/compute_m_measure/main.nf.test b/tests/modules/local/pairwise_gene_variation/compute_m_measure/main.nf.test new file mode 100644 index 00000000..2684a873 --- /dev/null +++ b/tests/modules/local/pairwise_gene_variation/compute_m_measure/main.nf.test @@ -0,0 +1,29 @@ +nextflow_process { + + name "Test Process COMPUTE_M_MEASURE" + script "modules/local/pairwise_gene_variation/compute_m_measure/main.nf" + process "COMPUTE_M_MEASURE" + tag "m_measure" + tag "module" + + test("Four initial chunk files") { + + when { + process { + """ + ch_count_file = Channel.fromPath( '$projectDir/tests/test_data/pairwise_gene_variation/make_chunks/input/counts.parquet', checkIfExists: true) + ch_ratio_std_files = Channel.fromPath( '$projectDir/tests/test_data/pairwise_gene_variation/compute_m_measure/input/std.*.parquet', checkIfExists: true).collect() + input[0] = ch_count_file + input[1] = ch_ratio_std_files + """ + } + } + + then { + assert process.success + // assert snapshot(process.out).match() + } + + } + +} diff --git a/tests/modules/local/pairwise_gene_variation/compute_m_measure/main.nf.test.snap b/tests/modules/local/pairwise_gene_variation/compute_m_measure/main.nf.test.snap new file mode 100644 index 00000000..64909557 --- /dev/null +++ b/tests/modules/local/pairwise_gene_variation/compute_m_measure/main.nf.test.snap @@ -0,0 +1,64 @@ +{ + "Should run without failures": { + "content": [ + { + "0": [ + "m_measures.csv:md5,996ba9ca255d5d4032e30e04bc062079" + ], + "1": [ + [ + "COMPUTE_M_MEASURE", + "python", + "3.12.8" + ] + ], + "2": [ + [ + "COMPUTE_M_MEASURE", + "polars", + "1.17.1" + ] + ], + "m_measures": [ + "m_measures.csv:md5,996ba9ca255d5d4032e30e04bc062079" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "24.10.3" + }, + "timestamp": "2025-01-16T11:48:03.528113087" + }, + "Four initial chunk files": { + "content": [ + { + "0": [ + "m_measures.csv:md5,8d6b01ed9f1dd7767f9ac370cec263ad" + ], + "1": [ + [ + "COMPUTE_M_MEASURE", + "python", + "3.12.8" + ] + ], + "2": [ + [ + "COMPUTE_M_MEASURE", + "polars", + "1.17.1" + ] + ], + "m_measures": [ + "m_measures.csv:md5,8d6b01ed9f1dd7767f9ac370cec263ad" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "24.10.3" + }, + "timestamp": "2025-01-30T16:16:59.347457078" + } +} \ No newline at end of file diff --git a/tests/modules/local/pairwise_gene_variation/cross_join/main.nf.test b/tests/modules/local/pairwise_gene_variation/cross_join/main.nf.test new file mode 100644 index 00000000..682df77e --- /dev/null +++ b/tests/modules/local/pairwise_gene_variation/cross_join/main.nf.test @@ -0,0 +1,29 @@ +nextflow_process { + + name "Test Process CROSS_JOIN" + script "modules/local/pairwise_gene_variation/cross_join/main.nf" + process "CROSS_JOIN" + tag "cross_join" + tag "module" + + test("Should run without failures") { + + when { + process { + """ + meta = [index_1: 0, index_2: 1] + file_1 = file( '$projectDir/tests/test_data/pairwise_gene_variation/make_chunks/output/count_chunk.0.parquet', checkIfExists: true) + file_2 = file( '$projectDir/tests/test_data/pairwise_gene_variation/make_chunks/output/count_chunk.1.parquet', checkIfExists: true) + input[0] = [meta, file_1, file_2] + """ + } + } + + then { + assert process.success + assert snapshot(process.out).match() + } + + } + +} diff --git a/tests/modules/local/pairwise_gene_variation/cross_join/main.nf.test.snap b/tests/modules/local/pairwise_gene_variation/cross_join/main.nf.test.snap new file mode 100644 index 00000000..4186e075 --- /dev/null +++ b/tests/modules/local/pairwise_gene_variation/cross_join/main.nf.test.snap @@ -0,0 +1,33 @@ +{ + "Should run without failures": { + "content": [ + { + "0": [ + "cross_join.0.1.parquet:md5,ef1d4777f59bf8c52f05fa37d638989f" + ], + "1": [ + [ + "CROSS_JOIN", + "python", + "3.12.8" + ] + ], + "2": [ + [ + "CROSS_JOIN", + "polars", + "1.17.1" + ] + ], + "data": [ + "cross_join.0.1.parquet:md5,ef1d4777f59bf8c52f05fa37d638989f" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "24.10.3" + }, + "timestamp": "2025-01-29T15:29:44.996421136" + } +} \ No newline at end of file diff --git a/tests/modules/local/pairwise_gene_variation/expression_ratio/main.nf.test b/tests/modules/local/pairwise_gene_variation/expression_ratio/main.nf.test new file mode 100644 index 00000000..7c6e86dd --- /dev/null +++ b/tests/modules/local/pairwise_gene_variation/expression_ratio/main.nf.test @@ -0,0 +1,27 @@ +nextflow_process { + + name "Test Process EXPRESSION_RATIO" + script "modules/local/pairwise_gene_variation/expression_ratio/main.nf" + process "EXPRESSION_RATIO" + tag "expression_ratio" + tag "module" + + test("Should run without failures") { + + when { + process { + """ + file = file( '$projectDir/tests/test_data/pairwise_gene_variation/cross_join/output/cross_join.0.1.parquet', checkIfExists: true) + input[0] = file + """ + } + } + + then { + assert process.success + assert snapshot(process.out).match() + } + + } + +} diff --git a/tests/modules/local/pairwise_gene_variation/expression_ratio/main.nf.test.snap b/tests/modules/local/pairwise_gene_variation/expression_ratio/main.nf.test.snap new file mode 100644 index 00000000..8b538b8e --- /dev/null +++ b/tests/modules/local/pairwise_gene_variation/expression_ratio/main.nf.test.snap @@ -0,0 +1,33 @@ +{ + "Should run without failures": { + "content": [ + { + "0": [ + "ratios.0.1.parquet:md5,b0999933fae92eab5ba6e01a17a352df" + ], + "1": [ + [ + "EXPRESSION_RATIO", + "python", + "3.12.8" + ] + ], + "2": [ + [ + "EXPRESSION_RATIO", + "polars", + "1.17.1" + ] + ], + "data": [ + "ratios.0.1.parquet:md5,b0999933fae92eab5ba6e01a17a352df" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "24.10.3" + }, + "timestamp": "2025-01-29T15:30:55.228354841" + } +} \ No newline at end of file diff --git a/tests/modules/local/pairwise_gene_variation/make_chunks/main.nf.test b/tests/modules/local/pairwise_gene_variation/make_chunks/main.nf.test new file mode 100644 index 00000000..64518e60 --- /dev/null +++ b/tests/modules/local/pairwise_gene_variation/make_chunks/main.nf.test @@ -0,0 +1,27 @@ +nextflow_process { + + name "Test Process MAKE_CHUNKS" + script "modules/local/pairwise_gene_variation/make_chunks/main.nf" + process "MAKE_CHUNKS" + tag "make_chunks" + tag "module" + + test("Should run without failures") { + + when { + process { + """ + ch_counts = Channel.fromPath( '$projectDir/tests/test_data/pairwise_gene_variation/make_chunks/input/counts.parquet', checkIfExists: true) + input[0] = ch_counts + """ + } + } + + then { + assert process.success + assert snapshot(process.out).match() + } + + } + +} diff --git a/tests/modules/local/pairwise_gene_variation/make_chunks/main.nf.test.snap b/tests/modules/local/pairwise_gene_variation/make_chunks/main.nf.test.snap new file mode 100644 index 00000000..d089bf1e --- /dev/null +++ b/tests/modules/local/pairwise_gene_variation/make_chunks/main.nf.test.snap @@ -0,0 +1,43 @@ +{ + "Should run without failures": { + "content": [ + { + "0": [ + [ + "count_chunk.0.parquet:md5,77cbba7f85bb646d4843d756c7ba701d", + "count_chunk.1.parquet:md5,33d252ec9988e951a3b9dbe30d6c2e3d", + "count_chunk.2.parquet:md5,1a80a3d72cd2b090d1b67aa561409f7c", + "count_chunk.3.parquet:md5,34890e85d64bbc623477299cff4b3c24" + ] + ], + "1": [ + [ + "MAKE_CHUNKS", + "python", + "3.12.8" + ] + ], + "2": [ + [ + "MAKE_CHUNKS", + "polars", + "1.17.1" + ] + ], + "chunks": [ + [ + "count_chunk.0.parquet:md5,77cbba7f85bb646d4843d756c7ba701d", + "count_chunk.1.parquet:md5,33d252ec9988e951a3b9dbe30d6c2e3d", + "count_chunk.2.parquet:md5,1a80a3d72cd2b090d1b67aa561409f7c", + "count_chunk.3.parquet:md5,34890e85d64bbc623477299cff4b3c24" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "24.10.3" + }, + "timestamp": "2025-01-29T19:24:23.507551112" + } +} \ No newline at end of file diff --git a/tests/modules/local/pairwise_gene_variation/ratio_standard_variation/main.nf.test b/tests/modules/local/pairwise_gene_variation/ratio_standard_variation/main.nf.test new file mode 100644 index 00000000..53a44228 --- /dev/null +++ b/tests/modules/local/pairwise_gene_variation/ratio_standard_variation/main.nf.test @@ -0,0 +1,27 @@ +nextflow_process { + + name "Test Process RATIO_STANDARD_VARIATION" + script "modules/local/pairwise_gene_variation/ratio_standard_variation/main.nf" + process "RATIO_STANDARD_VARIATION" + tag "ratio_std" + tag "module" + + test("Should run without failures") { + + when { + process { + """ + file = file( '$projectDir/tests/test_data/pairwise_gene_variation/expression_ratio/output/ratios.0.1.parquet', checkIfExists: true) + input[0] = file + """ + } + } + + then { + assert process.success + assert snapshot(process.out).match() + } + + } + +} diff --git a/tests/modules/local/pairwise_gene_variation/ratio_standard_variation/main.nf.test.snap b/tests/modules/local/pairwise_gene_variation/ratio_standard_variation/main.nf.test.snap new file mode 100644 index 00000000..3ebf8613 --- /dev/null +++ b/tests/modules/local/pairwise_gene_variation/ratio_standard_variation/main.nf.test.snap @@ -0,0 +1,33 @@ +{ + "Should run without failures": { + "content": [ + { + "0": [ + "std.0.1.parquet:md5,7a1529e9be1f9ca8aecb9146c39ab4f5" + ], + "1": [ + [ + "RATIO_STANDARD_VARIATION", + "python", + "3.12.8" + ] + ], + "2": [ + [ + "RATIO_STANDARD_VARIATION", + "polars", + "1.17.1" + ] + ], + "data": [ + "std.0.1.parquet:md5,7a1529e9be1f9ca8aecb9146c39ab4f5" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "24.10.3" + }, + "timestamp": "2025-01-29T16:12:29.850020618" + } +} \ No newline at end of file diff --git a/tests/subworkflows/local/pairwise_gene_variation/main.nf.test b/tests/subworkflows/local/pairwise_gene_variation/main.nf.test new file mode 100644 index 00000000..db8e783a --- /dev/null +++ b/tests/subworkflows/local/pairwise_gene_variation/main.nf.test @@ -0,0 +1,49 @@ +nextflow_workflow { + + name "Test Workflow PAIRWISE_GENE_VARIATION" + script "subworkflows/local/pairwise_gene_variation/main.nf" + workflow "PAIRWISE_GENE_VARIATION" + tag "subworkflow_pairwise_gene_variation" + tag "subworkflow" + + test("10 genes") { + + tag "subworkflow_pairwise_gene_variation_10_genes" + + when { + workflow { + """ + ch_counts = Channel.fromPath( '$projectDir/tests/test_data/pairwise_gene_variation/make_chunks/input/counts.head.parquet', checkIfExists: true) + input[0] = ch_counts + """ + } + } + + then { + assert workflow.success + assert snapshot(workflow.out).match() + } + + } + + test("1000 genes") { + + tag "subworkflow_pairwise_gene_variation_1000_genes" + + when { + workflow { + """ + ch_counts = Channel.fromPath( '$projectDir/tests/test_data/pairwise_gene_variation/make_chunks/input/counts.parquet', checkIfExists: true) + input[0] = ch_counts + """ + } + } + + then { + assert workflow.success + // assert snapshot(workflow.out).match() + } + + } + +} diff --git a/tests/subworkflows/local/pairwise_gene_variation/main.nf.test.snap b/tests/subworkflows/local/pairwise_gene_variation/main.nf.test.snap new file mode 100644 index 00000000..7507648c --- /dev/null +++ b/tests/subworkflows/local/pairwise_gene_variation/main.nf.test.snap @@ -0,0 +1,19 @@ +{ + "10 genes": { + "content": [ + { + "0": [ + "m_measures.csv:md5,597ef653508ddfc81f1c284daca67fbc" + ], + "m_measures": [ + "m_measures.csv:md5,597ef653508ddfc81f1c284daca67fbc" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "24.10.3" + }, + "timestamp": "2025-01-30T16:23:31.38356288" + } +} \ No newline at end of file diff --git a/tests/subworkflows/local/pairwise_gene_variation/run_genorm.py b/tests/subworkflows/local/pairwise_gene_variation/run_genorm.py new file mode 100644 index 00000000..3d91ae26 --- /dev/null +++ b/tests/subworkflows/local/pairwise_gene_variation/run_genorm.py @@ -0,0 +1,43 @@ +import pandas as pd +import numpy as np +import sys + +file = sys.argv[1] +# Expression data for three control genes. +counts = pd.read_parquet(file) +counts.set_index("ensembl_gene_id", inplace=True) +counts = counts.T.replace(0, 1e-8) + + +def _m_numpy(gene_expression: np.ndarray) -> np.ndarray: + """Internal control gene-stability measure `M`. + + Computes Eq. (4) in Ref. [1]. + + [1]: Vandesompele, Jo, et al. "Accurate normalization of real-time quantitative + RT-PCR data by geometric averaging of multiple internal control genes." Genome + biology 3.7 (2002): 1-12. + """ + + if not (gene_expression > 0).all(): + raise ValueError( + "Expression domain error: not all expression data are strictly positive!" + ) + + a = gene_expression + # Eq. (2): A_{jk}^{(i)} = log_2 (a_{ij} / a_{ik}) + A = np.log2(np.einsum("ij,ik->ijk", a, 1 / a)) + # Eq. (3) + V = np.std(A, axis=0) + # Eq. (4) N.B., Since V_{j=k} is zero, we can simply ignore it since it does not + # contribute to calculation. + n = V.shape[1] + return np.sum(V, axis=1) / (n - 1) + + +def m_measure(gene_expression): + m_values = _m_numpy(gene_expression.to_numpy()) + return pd.Series(m_values, index=gene_expression.columns) + + +print(m_measure(counts).sort_values()) diff --git a/tests/test_data/pairwise_gene_variation/compute_m_measure/input/std.0.0.parquet b/tests/test_data/pairwise_gene_variation/compute_m_measure/input/std.0.0.parquet new file mode 100644 index 0000000000000000000000000000000000000000..3421c12920244fc0389a6e63dab3b45da7572389 GIT binary patch literal 593674 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 z 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�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ξ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

    -
    - + ([a-zA-Z,]+) - + + + + + + +
    +
    + + ([A-Z0-9-]+,?)+ - - + + ([A-Z0-9-]+,?)+ - - + +
    +
    + + + ([A-Z0-9-]+,?)+ + + + + ([A-Z0-9-]+,?)+ + +
    @@ -129,73 +146,26 @@ VERSION="1.0dev"; echo "$VERSION"
    - + - + + + +
    +
    + + + + + + + +
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @misc{nf-core/stableexpression, - author = {Coen, Olivier}, - year = {2025}, + author = {}, + year = {}, title = {nf-core/stableexpression}, publisher = {GitHub}, journal = {GitHub repository}, - url = {https://github.com/OlivierCoen/stableexpression}, + url = {https://github.com/nf-core/stableexpression}, } From 0f267a5e8183292430c069214c7eeb5f6abfb5b6 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Fri, 24 Oct 2025 08:29:15 +0200 Subject: [PATCH 077/258] replace terminate by ignore in clean count data --- modules/local/clean_count_data/main.nf | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/local/clean_count_data/main.nf b/modules/local/clean_count_data/main.nf index 449adb5e..d99a474b 100644 --- a/modules/local/clean_count_data/main.nf +++ b/modules/local/clean_count_data/main.nf @@ -4,12 +4,14 @@ process CLEAN_COUNT_DATA { errorStrategy { if (task.exitStatus == 101) { - log.error( + /* + log.warning( "No more valid sample after checking p-value of Kolmogorow-Smirnoff test against target distribution! " + "You can try a more flexible approach by setting again the value of the ks_pvalue_threshold parameter. " + "Provide a negative value to disable this filter." ) - return 'terminate' + */ + return 'ignore' } } From 1370b0836fbcc5f5c949645bae9e7cfd58b893c6 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Fri, 24 Oct 2025 08:29:27 +0200 Subject: [PATCH 078/258] add local config --- nextflow.config | 1 + 1 file changed, 1 insertion(+) diff --git a/nextflow.config b/nextflow.config index 92aaa4b2..d2606572 100644 --- a/nextflow.config +++ b/nextflow.config @@ -232,6 +232,7 @@ profiles { test_local_and_downloaded { includeConfig 'conf/test_local_and_downloaded.config' } test_one_rnaseq_one_microarray { includeConfig 'conf/test_one_rnaseq_one_microarray.config' } test_eatlas_geo { includeConfig 'conf/test_eatlas_geo.config' } + local { includeConfig 'conf/local.config' } } // Load nf-core custom profiles from different Institutions From bfb2befcae44982664c4ba9b9e31ca62ba314790 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Fri, 24 Oct 2025 11:34:03 +0200 Subject: [PATCH 079/258] add pipelines tests to default.nf.test --- conf/test_one_rnaseq_one_microarray.config | 2 +- tests/default.nf.test | 102 +++++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/conf/test_one_rnaseq_one_microarray.config b/conf/test_one_rnaseq_one_microarray.config index a86eb1ec..fdb8fc92 100644 --- a/conf/test_one_rnaseq_one_microarray.config +++ b/conf/test_one_rnaseq_one_microarray.config @@ -19,6 +19,6 @@ params { species = 'arabidopsis thaliana' eatlas_accessions = 'E-GEOD-52806,E-GEOD-21945' skip_fetch_eatlas_accessions = true - + skip_fetch_geo_accessions = true outdir = "results/test_one_rnaseq_one_microarray" } diff --git a/tests/default.nf.test b/tests/default.nf.test index 463561bc..c13bcbef 100644 --- a/tests/default.nf.test +++ b/tests/default.nf.test @@ -32,4 +32,106 @@ nextflow_pipeline { ) } } + + test("-profile test_one_rnaseq_one_microarray") { + + when { + params { + species = 'arabidopsis thaliana' + eatlas_accessions = 'E-GEOD-52806,E-GEOD-21945' + skip_fetch_eatlas_accessions = true + skip_fetch_geo_accessions = true + outdir = "$outputDir" + } + } + + then { + def stable_name = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}']) + def stable_path = getAllFilesFromDir(params.outdir, ignoreFile: 'tests/.nftignore') + assertAll( + { assert workflow.success}, + { assert snapshot( + removeNextflowVersion("$outputDir/pipeline_info/nf_core_stableexpression_software_mqc_versions.yml"), + stable_name, + stable_path + ).match() } + ) + } + } + + test("-profile test_one_accession_low_gene_count") { + + when { + params { + species = 'arabidopsis thaliana' + eatlas_accessions = "E-GEOD-51720" + outdir = "results/test_one_accession_low_gene_count" + outdir = "$outputDir" + } + } + + then { + def stable_name = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}']) + def stable_path = getAllFilesFromDir(params.outdir, ignoreFile: 'tests/.nftignore') + assertAll( + { assert workflow.success}, + { assert snapshot( + removeNextflowVersion("$outputDir/pipeline_info/nf_core_stableexpression_software_mqc_versions.yml"), + stable_name, + stable_path + ).match() } + ) + } + } + + test("-profile test_local_and_downloaded") { + + when { + params { + species = 'solanum tuberosum' + keywords = "potato,stress" + eatlas_accessions = "E-MTAB-552" + datasets = "${projectDir}/tests/test_data/input_datasets/input.csv" + outdir = "$outputDir" + } + } + + then { + def stable_name = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}']) + def stable_path = getAllFilesFromDir(params.outdir, ignoreFile: 'tests/.nftignore') + assertAll( + { assert workflow.success}, + { assert snapshot( + removeNextflowVersion("$outputDir/pipeline_info/nf_core_stableexpression_software_mqc_versions.yml"), + stable_name, + stable_path + ).match() } + ) + } + } + + test("-profile test_ignore_errors") { + + when { + params { + species = 'beta vulgaris' + keywords = "leaf" + datasets = "${projectDir}/tests/test_data/input_datasets/input.csv" + outdir = "$outputDir" + } + } + + then { + def stable_name = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}']) + def stable_path = getAllFilesFromDir(params.outdir, ignoreFile: 'tests/.nftignore') + assertAll( + { assert workflow.success}, + { assert snapshot( + removeNextflowVersion("$outputDir/pipeline_info/nf_core_stableexpression_software_mqc_versions.yml"), + stable_name, + stable_path + ).match() } + ) + } + } } From a2b296fdac3c0ad825553149f7cdce681697b3da Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Fri, 24 Oct 2025 11:55:25 +0200 Subject: [PATCH 080/258] dynamic allocation of available port in dash app.py --- modules/local/dash_app/app/app.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/modules/local/dash_app/app/app.py b/modules/local/dash_app/app/app.py index 158ca312..9db11350 100755 --- a/modules/local/dash_app/app/app.py +++ b/modules/local/dash_app/app/app.py @@ -1,3 +1,4 @@ +import socket import dash_mantine_components as dmc from dash_extensions.enrich import ( @@ -64,6 +65,17 @@ def serve_layout(): genes.register_callbacks() samples.register_callbacks() +# -------------------- LAUNCH SERVER -------------------- + + +def find_port(port: int) -> int: + """Find a port not in use starting at given port""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + if s.connect_ex(("localhost", port)) == 0: + return find_port(port=port + 1) + else: + return port + if __name__ == "__main__": logger.info("Running server") @@ -74,6 +86,6 @@ def serve_layout(): app.run( debug=DEBUG, host=config.HOST, - port=config.PLOTLY_APP_PORT, + port=find_port(port=config.PLOTLY_APP_PORT), dev_tools_prune_errors=prune_errors, ) From 5225d2c236dd7d50f5b9dd32ccf50a6e4850dbd5 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Fri, 24 Oct 2025 16:08:32 +0200 Subject: [PATCH 081/258] change gprofiler id mapping name and code to integrate directly in it custom gene id mapping and gene metadata --- bin/map_ids_to_ensembl.py | 59 +++++++++++++------ conf/modules/id_mapping.config | 2 +- .../gprofiler => gprofiler/idmapping}/main.nf | 11 ++-- .../idmapping}/spec-file.txt | 0 subworkflows/local/idmapping/main.nf | 45 -------------- .../local/idmapping/gprofiler/main.nf.test | 6 +- 6 files changed, 53 insertions(+), 70 deletions(-) rename modules/local/{idmapping/gprofiler => gprofiler/idmapping}/main.nf (86%) rename modules/local/{idmapping/gprofiler => gprofiler/idmapping}/spec-file.txt (100%) delete mode 100644 subworkflows/local/idmapping/main.nf diff --git a/bin/map_ids_to_ensembl.py b/bin/map_ids_to_ensembl.py index 7cf572b2..b8f0ec2a 100755 --- a/bin/map_ids_to_ensembl.py +++ b/bin/map_ids_to_ensembl.py @@ -15,8 +15,6 @@ logger = logging.getLogger(__name__) - - ################################################################## # CONSTANTS ################################################################## @@ -39,7 +37,16 @@ def parse_args(): "--species", type=str, required=True, help="Species to convert IDs for" ) parser.add_argument( - "--custom-mappings", type=str, help="Optional file containing custom mappings" + "--custom-mappings", + type=str, + dest="custom_mappings", + help="Optional file containing custom mappings", + ) + parser.add_argument( + "--custom-metadata", + type=str, + dest="custom_metadata", + help="Optional file containing custom metadata", ) return parser.parse_args() @@ -53,6 +60,9 @@ def main(): args = parse_args() count_file = args.count_file + custom_mapping_file = args.custom_mappings + custom_metadata_file = args.custom_metadata + logger.info( f"Converting IDs for species {args.species} and count file {count_file.name}..." ) @@ -69,21 +79,17 @@ def main(): gene_ids = df.index.tolist() custom_mappings_dict = {} - custom_mapping_file = args.custom_mappings if custom_mapping_file: - if Path(custom_mapping_file).is_file(): - custom_mapping_df = pd.read_csv(custom_mapping_file) - custom_mappings_dict = custom_mapping_df.set_index( - config.ORIGINAL_GENE_ID_COLNAME - )[config.ENSEMBL_GENE_ID_COLNAME].to_dict() + custom_mapping_df = pd.read_csv(custom_mapping_file) + custom_mappings_dict = custom_mapping_df.set_index( + config.ORIGINAL_GENE_ID_COLNAME + )[config.ENSEMBL_GENE_ID_COLNAME].to_dict() gene_ids_left_to_map = [ - gene_id for gene_id in gene_ids - if gene_id not in custom_mappings_dict.keys() + gene_id for gene_id in gene_ids if gene_id not in custom_mappings_dict ] logger.info(f"Number of genes left to map: {len(gene_ids_left_to_map)}") - mapping_dict = {} gene_metadata_dfs = [] ############################################################# @@ -91,10 +97,13 @@ def main(): ############################################################# if gene_ids_left_to_map: - mapping_dict, gene_metadata_dfs = convert_ids(gene_ids_left_to_map, args.species) + gprofiler_mapping_dict, gene_metadata_dfs = convert_ids( + gene_ids_left_to_map, args.species + ) + + # overall mappings is the custom_mappings_dict complemented with gprofiler_mapping_dict + mapping_dict = custom_mappings_dict | gprofiler_mapping_dict - # adding custom mappings - mapping_dict.update(custom_mappings_dict) # if mapping dict is empty if not mapping_dict: logger.error( @@ -108,6 +117,8 @@ def main(): #############################################################" # MAPPING GENE IDS IN DATAFRAME ############################################################# + + # IMPORTANT: KEEPING ONLY GENES THAT HAVE BEEN CONVERTED # filtering the DataFrame to keep only the rows where the index can be mapped df = df.loc[df.index.isin(mapping_dict)] @@ -128,10 +139,19 @@ def main(): outfile = count_file.with_name(count_file.stem + RENAMED_FILE_SUFFIX) df.to_csv(outfile, index=False, header=True) + # if the user provides custom metadata file + if custom_metadata_file: + custom_metadata_df = pd.read_csv(custom_metadata_file) + # prepending custom metadata in gene metadata + gene_metadata_dfs = [custom_metadata_df] + gene_metadata_dfs + # concatenating all metadata and ensuring there are no duplicates if gene_metadata_dfs: gene_metadata_df = pd.concat(gene_metadata_dfs, ignore_index=True) - gene_metadata_df.drop_duplicates(inplace=True) + # dropping duplicates and keeping the first occurence + gene_metadata_df.drop_duplicates( + inplace=True, subset=[config.ENSEMBL_GENE_ID_COLNAME], keep="first" + ) # writing gene metadata to file metadata_file = count_file.with_name(count_file.stem + METADATA_FILE_SUFFIX) gene_metadata_df.to_csv(metadata_file, index=False, header=True) @@ -140,7 +160,12 @@ def main(): mapping_df = ( pd.DataFrame(mapping_dict, index=[0]) .T.reset_index() # transpose: setting keys as indexes instead of columns - .rename(columns={"index": config.ORIGINAL_GENE_ID_COLNAME, 0: config.ENSEMBL_GENE_ID_COLNAME}) + .rename( + columns={ + "index": config.ORIGINAL_GENE_ID_COLNAME, + 0: config.ENSEMBL_GENE_ID_COLNAME, + } + ) ) mapping_file = count_file.with_name(count_file.stem + MAPPING_FILE_SUFFIX) mapping_df.to_csv(mapping_file, index=False, header=True) diff --git a/conf/modules/id_mapping.config b/conf/modules/id_mapping.config index ce48c942..50e59957 100644 --- a/conf/modules/id_mapping.config +++ b/conf/modules/id_mapping.config @@ -1,6 +1,6 @@ process { - withName: 'IDMAPPING_GPROFILER' { + withName: 'GPROFILER_IDMAPPING' { publishDir = [ path: { "${params.outdir}/idmapping/datasets/" }, mode: params.publish_dir_mode diff --git a/modules/local/idmapping/gprofiler/main.nf b/modules/local/gprofiler/idmapping/main.nf similarity index 86% rename from modules/local/idmapping/gprofiler/main.nf rename to modules/local/gprofiler/idmapping/main.nf index 76cb8067..c195aeff 100644 --- a/modules/local/idmapping/gprofiler/main.nf +++ b/modules/local/gprofiler/idmapping/main.nf @@ -1,4 +1,4 @@ -process IDMAPPING_GPROFILER { +process GPROFILER_IDMAPPING { label 'process_single' @@ -34,9 +34,10 @@ process IDMAPPING_GPROFILER { tuple val(meta), path(count_file) val species val gene_id_mapping_file + val gene_metadata_file output: - tuple val(meta), path('*.renamed.csv'), emit: renamed + tuple val(meta), path('*.renamed.csv'), emit: counts path('*.metadata.csv'), optional: true, emit: metadata path('*.mapping.csv'), optional: true, emit: mapping tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions @@ -44,12 +45,14 @@ process IDMAPPING_GPROFILER { tuple val("${task.process}"), val('requests'), eval('python3 -c "import requests; print(requests.__version__)"'), topic: versions script: - def custom_mapping_arg = gene_id_mapping_file ? "--custom-mappings $gene_id_mapping_file" : "" + def custom_mapping_arg = gene_id_mapping_file ? "--custom-mappings $gene_id_mapping_file" : "" + def custom_metadata_arg = gene_metadata_file ? "--custom-metadata $gene_metadata_file" : "" """ map_ids_to_ensembl.py \\ --count-file "$count_file" \\ --species "$species" \\ - $custom_mapping_arg + $custom_mapping_arg \\ + $custom_metadata_arg """ diff --git a/modules/local/idmapping/gprofiler/spec-file.txt b/modules/local/gprofiler/idmapping/spec-file.txt similarity index 100% rename from modules/local/idmapping/gprofiler/spec-file.txt rename to modules/local/gprofiler/idmapping/spec-file.txt diff --git a/subworkflows/local/idmapping/main.nf b/subworkflows/local/idmapping/main.nf deleted file mode 100644 index f7e72754..00000000 --- a/subworkflows/local/idmapping/main.nf +++ /dev/null @@ -1,45 +0,0 @@ -include { IDMAPPING_GPROFILER } from '../../../modules/local/idmapping/gprofiler' - -/* -======================================================================================== - SUBWORKFLOW TO MAP ORIGINAL IDS TO ENSEMBL GENE IDS -======================================================================================== -*/ - -workflow IDMAPPING { - - take: - ch_datasets - ch_species - - main: - - ch_gene_metadata = params.gene_metadata ? Channel.fromPath( params.gene_metadata, checkIfExists: true ) : Channel.empty() - ch_gene_id_mapping = params.gene_id_mapping_file ? Channel.fromPath( params.gene_id_mapping, checkIfExists: true ) : Channel.empty() - - if ( !params.skip_gprofiler ) { - - // tries to map gene IDs to Ensembl IDs whenever possible - IDMAPPING_GPROFILER( - ch_datasets, - ch_species, - params.gene_id_mapping_file ? Channel.fromPath( params.gene_id_mapping_file, checkIfExists: true ) : Channel.value( [] ) - ) - - IDMAPPING_GPROFILER.out.renamed.set { ch_datasets } - - ch_gene_metadata - .mix( IDMAPPING_GPROFILER.out.metadata ) - .set { ch_gene_metadata } - - // the gene id mappings are the sum - // of those provided by the user and those fetched from g:Profiler - IDMAPPING_GPROFILER.out.mapping.set { ch_gene_id_mapping } - - } - - emit: - datasets = ch_datasets - gene_metadata = ch_gene_metadata - gene_id_mapping = ch_gene_id_mapping -} diff --git a/tests/modules/local/idmapping/gprofiler/main.nf.test b/tests/modules/local/idmapping/gprofiler/main.nf.test index 9f06851d..09c758f2 100644 --- a/tests/modules/local/idmapping/gprofiler/main.nf.test +++ b/tests/modules/local/idmapping/gprofiler/main.nf.test @@ -1,8 +1,8 @@ nextflow_process { - name "Test Process IDMAPPING_GPROFILER" - script "modules/local/idmapping/gprofiler/main.nf" - process "IDMAPPING_GPROFILER" + name "Test Process GPROFILER_IDMAPPING" + script "modules/local/gprofiler/idmapping/main.nf" + process "GPROFILER_IDMAPPING" tag "idmapping" tag "module" From fbc6ce04c82f79b97fdc1af3ca6ace506e9839de Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Fri, 24 Oct 2025 16:09:01 +0200 Subject: [PATCH 082/258] change way of merging designs --- conf/test_local_and_downloaded.config | 1 + subworkflows/local/merge_data/main.nf | 36 +++++++++++++++---- tests/default.nf.test.snap | 4 +-- tests/test_data/input_datasets/input.csv | 2 -- .../input_datasets/microarray2.normalised.csv | 10 ------ .../test_data/input_datasets/rnaseq2.raw.csv | 10 ------ workflows/stableexpression.nf | 26 ++++++++++---- 7 files changed, 52 insertions(+), 37 deletions(-) delete mode 100644 tests/test_data/input_datasets/microarray2.normalised.csv delete mode 100644 tests/test_data/input_datasets/rnaseq2.raw.csv diff --git a/conf/test_local_and_downloaded.config b/conf/test_local_and_downloaded.config index a5b5fed5..0f808ac9 100644 --- a/conf/test_local_and_downloaded.config +++ b/conf/test_local_and_downloaded.config @@ -27,6 +27,7 @@ params { species = 'solanum tuberosum' keywords = "potato,stress" eatlas_accessions = "E-MTAB-552" + skip_fetch_geo_accessions = true datasets = "tests/test_data/input_datasets/input.csv" outdir = "results/test_local_and_downloaded" } diff --git a/subworkflows/local/merge_data/main.nf b/subworkflows/local/merge_data/main.nf index c7820c47..8bb6ad08 100644 --- a/subworkflows/local/merge_data/main.nf +++ b/subworkflows/local/merge_data/main.nf @@ -1,7 +1,6 @@ include { MERGE_COUNTS as MERGE_ALL_COUNTS } from '../../../modules/local/merge/counts' include { MERGE_COUNTS as MERGE_RNASEQ_COUNTS } from '../../../modules/local/merge/counts' include { MERGE_COUNTS as MERGE_MICROARRAY_COUNTS } from '../../../modules/local/merge/counts' -include { MERGE_DESIGNS } from '../../../modules/local/merge/designs' /* @@ -25,7 +24,7 @@ workflow MERGE_DATA { .map { meta, file -> file } .set { ch_normalised_rnaseq_counts } - MERGE_RNASEQ_COUNTS ( ch_normalised_rnaseq_counts ) + MERGE_RNASEQ_COUNTS ( ch_normalised_rnaseq_counts.collect() ) MERGE_RNASEQ_COUNTS.out.counts.set { ch_merged_rnaseq_counts } ch_normalised_counts @@ -33,7 +32,7 @@ workflow MERGE_DATA { .map { meta, file -> file } .set { ch_normalised_microarray_counts } - MERGE_MICROARRAY_COUNTS ( ch_normalised_microarray_counts ) + MERGE_MICROARRAY_COUNTS ( ch_normalised_microarray_counts.collect() ) MERGE_MICROARRAY_COUNTS.out.counts.set { ch_merged_microarray_counts } // ----------------------------------------------------------------- @@ -50,13 +49,36 @@ workflow MERGE_DATA { // MERGE ALL DESIGNS IN A SINGLE TABLE // ----------------------------------------------------------------- - MERGE_DESIGNS( - ch_normalised_counts.map { meta, file -> meta.design }.collect() - ) + ch_normalised_counts + .map { + meta, _ -> // extracts design file and adds batch column whenever missing (for custom datasets) + def design_content = meta.design.splitCsv( header: true ) + // if there is no batch, it is custom data + // prepending dataset id to sample name and adding it as batch identifier + def updated_design_content = design_content.collect { row -> + row.sample = row.batch ?: "custom_${meta.dataset}_${row.sample}" + row.batch = row.batch ?: "custom_${meta.dataset}" + return row + } + [ updated_design_content ] + } + .flatten() + .unique() + .collectFile( + name: 'whole_design.csv', + seed: "batch,condition,sample", + newLine: true, + sort: true, + storeDir: "${params.outdir}/merged_datasets/" + ) { + item -> "${item.batch},${item.condition},${item.sample}" + } + .set { ch_whole_design } + emit: all_counts = MERGE_ALL_COUNTS.out.counts rnaseq_counts = ch_merged_rnaseq_counts microarray_counts = ch_merged_microarray_counts - whole_design = MERGE_DESIGNS.out.design + whole_design = ch_whole_design } diff --git a/tests/default.nf.test.snap b/tests/default.nf.test.snap index 434de457..18397c44 100644 --- a/tests/default.nf.test.snap +++ b/tests/default.nf.test.snap @@ -64,7 +64,7 @@ "polars": "1.17.1", "python": "3.12.8" }, - "IDMAPPING_GPROFILER": { + "GPROFILER_IDMAPPING": { "requests": "2.32.4", "python": "3.13.5", "pandas": "2.3.1" @@ -1207,4 +1207,4 @@ }, "timestamp": "2025-10-21T08:28:15.486728677" } -} \ No newline at end of file +} diff --git a/tests/test_data/input_datasets/input.csv b/tests/test_data/input_datasets/input.csv index 50a5b28e..6ea4aa16 100644 --- a/tests/test_data/input_datasets/input.csv +++ b/tests/test_data/input_datasets/input.csv @@ -1,5 +1,3 @@ counts,design,platform,normalised tests/test_data/input_datasets/microarray.normalised.csv,tests/test_data/input_datasets/microarray.normalised.design.csv,microarray,true tests/test_data/input_datasets/rnaseq.raw.csv,tests/test_data/input_datasets/rnaseq.raw.design.csv,rnaseq,false -tests/test_data/input_datasets/microarray2.normalised.csv,,microarray,true -tests/test_data/input_datasets/rnaseq2.raw.csv,,rnaseq,false diff --git a/tests/test_data/input_datasets/microarray2.normalised.csv b/tests/test_data/input_datasets/microarray2.normalised.csv deleted file mode 100644 index e7164776..00000000 --- a/tests/test_data/input_datasets/microarray2.normalised.csv +++ /dev/null @@ -1,10 +0,0 @@ -,FSM1528575,FSM1528576,FSM1528579,FSM1528583,FSM1528584,FSM1528585,FSM1528580,FSM1528586,FSM1528582,FSM1528578,FSM1528581,FSM1528577 -ENSRNA049453121,20925.1255070264,136184.261516502,144325.370645564,89427.0987612997,164143.182734208,34178.6378088171,28842.7323281157,76973.395782103,41906.9367255656,44756.5602263121,252562.049703724,6953.65643340122 -ENSRNA049453138,196173.051628372,16607.8367703051,344972.83715281,22602.4535330758,13678.598561184,104546.428532852,15451.4637472048,71664.8857281649,160643.257448002,91459.0578537683,88396.7173963033,281623.08555275 -ENSRNA049454388,91547.4240932405,11625.4857392136,84483.143792525,80582.6604222701,218857.576978944,58304.7350856292,42234.0009090266,88475.1675656357,87306.1181782617,17513.436610296,90922.3378933406,76490.2207674135 -ENSRNA049454416,20925.1255070264,106290.155329953,193607.204524536,47170.3378081581,392119.825420608,190998.270108096,90648.5873169351,81397.1541603848,83813.8734511313,165404.67909724,111127.301869638,194702.380135234 -ENSRNA049454647,99394.3461583754,91343.1022366783,3520.13099135521,71738.2220832404,118547.854196928,20105.0810640101,81377.7090686122,15040.7784861581,66352.6498154789,110918.431865208,55563.6509348192,111258.50293442 -ENSRNA049454661,175247.926121346,66431.3470812206,24640.9169394865,52083.9146631746,360203.095444512,36189.1459152181,70046.6356539953,85820.9125386666,13968.9789085219,50594.3724297441,25256.2049703724,52152.4232505092 -ENSRNA049454747,117703.830977024,154452.881963838,281610.479308417,29481.4611380988,191500.379856576,152798.616086476,53565.0743236435,14156.0268105017,293348.557078959,155674.99209152,63140.5124259309,243377.975169043 -ENSRNA049454887,2615.6406883783,164417.584026021,28161.0479308417,82548.0911642767,50154.861391008,136714.551235268,97859.279398964,64586.872322914,328271.004350264,159566.866893808,151537.229822234,86920.7054175153 -ENSRNA049454931,177863.566809724,81378.4001744952,235848.776420799,88444.3833902964,18238.131414912,120630.48638406,82407.8066517592,50430.8455124123,118736.320722436,68107.8090400402,232357.085727426,163410.926184929 diff --git a/tests/test_data/input_datasets/rnaseq2.raw.csv b/tests/test_data/input_datasets/rnaseq2.raw.csv deleted file mode 100644 index 79c6d156..00000000 --- a/tests/test_data/input_datasets/rnaseq2.raw.csv +++ /dev/null @@ -1,10 +0,0 @@ -,DSM1528575,DSM1528576,DSM1528579,DSM1528583,DSM1528584,DSM1528585,DSM1528580,DSM1528586,DSM1528582,DSM1528578,DSM1528581,DSM1528577 -ENSRNA049453121,1,82,8,82,4,68,88,73,46,57,25,22 -ENSRNA049453138,67,93,41,84,36,18,28,96,84,85,92,32 -ENSRNA049454388,38,10,0,23,11,17,95,57,25,82,10,70 -ENSRNA049454416,75,55,7,30,79,60,15,97,12,35,60,56 -ENSRNA049454647,35,64,55,91,48,95,68,100,24,26,100,47 -ENSRNA049454661,8,99,80,48,86,29,80,17,19,9,48,2 -ENSRNA049454747,67,7,98,53,3,10,52,87,4,80,22,15 -ENSRNA049454887,8,40,27,90,42,52,79,81,94,28,35,81 -ENSRNA049454931,42,49,67,73,26,76,41,16,34,47,36,25 diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index b0e09ae7..ed2b1e12 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -6,7 +6,6 @@ include { EXPRESSIONATLAS_FETCHDATA } from '../subworkflows/local/expressionatlas_fetchdata' include { GEO_FETCHDATA } from '../subworkflows/local/geo_fetchdata' -include { IDMAPPING } from '../subworkflows/local/idmapping' include { EXPRESSION_NORMALISATION } from '../subworkflows/local/expression_normalisation' include { DATA_CLEANSING } from '../subworkflows/local/data_cleansing' include { MERGE_DATA } from '../subworkflows/local/merge_data' @@ -14,6 +13,7 @@ include { BASE_STATISTICS } from '../subworkflows/local/b include { STABILITY_SCORING } from '../subworkflows/local/stability_scoring' include { MULTIQC_WORKFLOW } from '../subworkflows/local/multiqc' +include { GPROFILER_IDMAPPING } from '../modules/local/gprofiler/idmapping' include { AGGREGATE_RESULTS } from '../modules/local/aggregate_results' include { DASH_APP } from '../modules/local/dash_app' @@ -61,20 +61,31 @@ workflow STABLEEXPRESSION { ch_input_datasets .concat( EXPRESSIONATLAS_FETCHDATA.out.downloaded_datasets ) .concat( GEO_FETCHDATA.out.downloaded_datasets ) - .set { ch_datasets } + .set { ch_counts } // ----------------------------------------------------------------- // IDMAPPING // ----------------------------------------------------------------- - IDMAPPING ( ch_datasets, ch_species ) + if ( !params.skip_gprofiler ) { + + // tries to map gene IDs to Ensembl IDs whenever possible + GPROFILER_IDMAPPING( + ch_counts, + ch_species, + params.gene_id_mapping_file ? Channel.fromPath( params.gene_id_mapping_file, checkIfExists: true ) : Channel.value( [] ), + params.gene_metadata ? Channel.fromPath( params.gene_metadata, checkIfExists: true ) : Channel.value( [] ) + ) + GPROFILER_IDMAPPING.out.counts.set { ch_counts } + + } // ----------------------------------------------------------------- // NORMALISATION OF RAW COUNT DATASETS (INCLUDING RNA-SEQ DATASETS) // ----------------------------------------------------------------- EXPRESSION_NORMALISATION( - IDMAPPING.out.datasets, + ch_counts, params.normalisation_method, params.quantile_normalisation_target_distribution ) @@ -94,6 +105,7 @@ workflow STABLEEXPRESSION { // ----------------------------------------------------------------- MERGE_DATA ( DATA_CLEANSING.out.cleaned_counts ) + MERGE_DATA.out.all_counts.set { ch_all_counts } MERGE_DATA.out.whole_design.set { ch_whole_design } @@ -122,12 +134,14 @@ workflow STABLEEXPRESSION { // ----------------------------------------------------------------- // AGGREGATE ALL RESULTS FOR MULTIQC // ----------------------------------------------------------------- + ch_candidate_gene_stats_with_scores.view { v -> "ch_candidate_gene_stats_with_scores " + v} + ch_all_counts.view { v -> "ch_all_counts " + v} AGGREGATE_RESULTS ( ch_all_counts, ch_candidate_gene_stats_with_scores, - IDMAPPING.out.gene_metadata, - IDMAPPING.out.gene_id_mapping + GPROFILER_IDMAPPING.out.metadata, + GPROFILER_IDMAPPING.out.mapping ) AGGREGATE_RESULTS.out.top_stable_genes_summary.set { ch_top_stable_genes_summary } From c8a99352f4bb735d8a82e5c95df3ae29328aad81 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Fri, 24 Oct 2025 16:41:29 +0200 Subject: [PATCH 083/258] fix bug in normfinder.py when of group chunk dataframe is full of null values --- bin/normfinder.py | 333 +++++++++++++++----------- subworkflows/local/merge_data/main.nf | 2 - 2 files changed, 189 insertions(+), 146 deletions(-) diff --git a/bin/normfinder.py b/bin/normfinder.py index 2542a9a6..43d98c3f 100755 --- a/bin/normfinder.py +++ b/bin/normfinder.py @@ -26,18 +26,14 @@ # POLARS EXTENSIONS ############################################################################ + @pl.api.register_expr_namespace("row") class StatsExtension: def __init__(self, expr: pl.Expr): self._expr = expr def not_null_values(self): - return ( - self._expr - .list - .eval(pl.element().drop_nulls().drop_nans()) - .list - ) + return self._expr.list.eval(pl.element().drop_nulls().drop_nans()).list def mean(self) -> pl.Expr: """Mean over non nulls values in row""" @@ -56,6 +52,7 @@ def min(self) -> pl.Expr: # NUMBA-ACCELERATED FUNCTIONS ############################################################################ + @njit(parallel=True) def compute_minvars(z: np.ndarray, target_idx: np.ndarray) -> np.ndarray: """ @@ -83,8 +80,8 @@ def compute_minvars(z: np.ndarray, target_idx: np.ndarray) -> np.ndarray: if i == j: continue diffs = z[i, :] - z[j, :] - mean = np.sum(diffs) / nsamples # scalar - var = np.sum((diffs - mean) ** 2) / (nsamples - 1) # scalar + mean = np.sum(diffs) / nsamples # scalar + var = np.sum((diffs - mean) ** 2) / (nsamples - 1) # scalar if np.isnan(var): continue # skip if var < minv: @@ -97,9 +94,9 @@ def compute_minvars(z: np.ndarray, target_idx: np.ndarray) -> np.ndarray: # NORMFINDER CLASS ##################################################### + @dataclass class NormFinder: - count_lf: pl.LazyFrame design_df: pl.DataFrame @@ -111,84 +108,76 @@ class NormFinder: n_genes: int = field(init=False) def __post_init__(self): - # format_design - self.design_df = ( - self.design_df - .with_columns(pl.concat_str([pl.col('batch'), pl.col('condition')], separator="_").alias("group")) - .select("sample", "group") - ) + self.design_df = self.design_df.with_columns( + pl.concat_str([pl.col("batch"), pl.col("condition")], separator="_").alias( + "group" + ) + ).select("sample", "group") # make dict associating a group to the list of its samples - group_to_sample_df = ( - self.design_df - .group_by("group", maintain_order=True) # maintain order is better for repeatability and testing - .agg("sample") - ) + group_to_sample_df = self.design_df.group_by("group", maintain_order=True).agg( + "sample" + ) # maintain order is better for repeatability and testing - self.group_to_samples_dict = { d["group"]: d["sample"] for d in group_to_sample_df.to_dicts() } + self.group_to_samples_dict = { + d["group"]: d["sample"] for d in group_to_sample_df.to_dicts() + } groups = list(self.group_to_samples_dict.keys()) self.n_groups = len(groups) - self.genes = self.count_lf.select(config.ENSEMBL_GENE_ID_COLNAME).collect().to_series().to_list() + self.genes = ( + self.count_lf.select(config.ENSEMBL_GENE_ID_COLNAME) + .collect() + .to_series() + .to_list() + ) self.n_genes = len(self.genes) if self.n_genes <= 2: logger.error("Too few genes") sys.exit(100) - @staticmethod def get_overall_mean_for_group(df_with_means_over_samples: pl.DataFrame) -> float: return df_with_means_over_samples.mean().item() - @staticmethod def get_means_over_samples(df: pl.DataFrame) -> pl.DataFrame: - return ( - df - .with_columns( - mean_over_samples_for_gene=pl.concat_list(pl.all()).row.mean() - ) - .select("mean_over_samples_for_gene") - ) - + return df.with_columns( + mean_over_samples_for_gene=pl.concat_list(pl.all()).row.mean() + ).select("mean_over_samples_for_gene") def correct_negative_values( - self, - intra_var_df: pl.DataFrame, - group_count_df: pl.DataFrame + self, intra_var_df: pl.DataFrame, group_count_df: pl.DataFrame ) -> pl.DataFrame: - - genes_with_negative_values = ( - intra_var_df - .select(col for col in self.genes if - (intra_var_df[col] < 0).all()) # intra_var_df has only one row but it is a dataframe - .columns - ) + genes_with_negative_values = intra_var_df.select( + col for col in self.genes if (intra_var_df[col] < 0).all() + ).columns # intra_var_df has only one row but it is a dataframe # getting indexes of genes for which we must compute minvar indexes_of_genes_with_negative_values = np.array( - [i for i, gene in enumerate(self.genes) if gene in genes_with_negative_values], - dtype=np.int64 + [ + i + for i, gene in enumerate(self.genes) + if gene in genes_with_negative_values + ], + dtype=np.int64, ) minvars = compute_minvars( - group_count_df.to_numpy(), - indexes_of_genes_with_negative_values + group_count_df.to_numpy(), indexes_of_genes_with_negative_values ) # associating back minvars to their respective gene - minvar_dict = { gene: minvars[i] for i, gene in enumerate(genes_with_negative_values) } - return ( - intra_var_df - .with_columns([ - pl.lit(val).alias(col) for col, val in minvar_dict.items() - ]) + minvar_dict = { + gene: minvars[i] for i, gene in enumerate(genes_with_negative_values) + } + return intra_var_df.with_columns( + [pl.lit(val).alias(col) for col, val in minvar_dict.items()] ) - def get_unbiased_intragroup_variance_for_group( self, group_count_df: pl.DataFrame, @@ -196,11 +185,10 @@ def get_unbiased_intragroup_variance_for_group( group_overall_mean: float, samples: list[str], ): - # TODO: see if it's correct # if only one sample in the group, there's no variance if len(samples) == 1: - data = { gene: [0] for gene in self.genes } + data = {gene: [0] for gene in self.genes} return pl.DataFrame(data) # lf is a lazyframe with a column being the gene ids (ensembl_gene_id) @@ -209,56 +197,90 @@ def get_unbiased_intragroup_variance_for_group( # means_over_samples_df is a single column dataframe containing the means across each row (ie for each gene across samples) ng = len(samples) - means_over_samples = means_over_samples_df.to_series().rename("mean_over_samples_for_gene") + means_over_samples = means_over_samples_df.to_series().rename( + "mean_over_samples_for_gene" + ) - mean_over_genes = group_count_df.mean().transpose().to_series().rename("mean_over_genes_for_sample") + mean_over_genes = ( + group_count_df.mean() + .transpose() + .to_series() + .rename("mean_over_genes_for_sample") + ) sample_variance_df = ( - group_count_df - .hstack([means_over_samples]) # adding column containing means over all samples in this group (for each gene) - .select([ - (pl.col(c) - pl.col("mean_over_samples_for_gene")).alias(c) # y_igj - mean(y_ig*) - for c in samples - ]) - .transpose(include_header=True, column_names=self.genes) # columns are now genes - .hstack([mean_over_genes]) # adding column containing means over all genes (for each sample) - .select([ - ((pl.col(c) - pl.col("mean_over_genes_for_sample") + group_overall_mean) ** 2).alias(c) # r_igj ^2 = (y_igj - mean(y_ig*) -mean(y_*gj) + mean(y_*g*) ) ^ 2 - for c in self.genes - ]) + group_count_df.hstack( + [means_over_samples] + ) # adding column containing means over all samples in this group (for each gene) + .select( + [ + (pl.col(c) - pl.col("mean_over_samples_for_gene")).alias( + c + ) # y_igj - mean(y_ig*) + for c in samples + ] + ) + .transpose( + include_header=True, column_names=self.genes + ) # columns are now genes + .hstack( + [mean_over_genes] + ) # adding column containing means over all genes (for each sample) + .select( + [ + ( + ( + pl.col(c) + - pl.col("mean_over_genes_for_sample") + + group_overall_mean + ) + ** 2 + ).alias( + c + ) # r_igj ^2 = (y_igj - mean(y_ig*) -mean(y_*gj) + mean(y_*g*) ) ^ 2 + for c in self.genes + ] + ) .transpose(include_header=True, column_names=samples) .with_columns( - sample_variance=pl.concat_list(samples).row.sum() / ((ng - 1) * (1 - 2 / self.n_genes)) # sum over j (samples) of r_igj ^2 terms + sample_variance=pl.concat_list(samples).row.sum() + / ( + (ng - 1) * (1 - 2 / self.n_genes) + ) # sum over j (samples) of r_igj ^2 terms ) .select("sample_variance") .transpose() - .rename( {f"column_{i}": gene for i, gene in enumerate(self.genes)} ) + .rename({f"column_{i}": gene for i, gene in enumerate(self.genes)}) ) # sum of all sample variances for all genes - sample_variance_sum_over_genes = sample_variance_df.select(pl.sum_horizontal(pl.all())).item() # sum of all s_ij² over all genes - - intra_var_df = ( - sample_variance_df - .select([ - ( pl.col(c) - sample_variance_sum_over_genes / (self.n_genes * (self.n_genes -1)) ).alias(c) + sample_variance_sum_over_genes = sample_variance_df.select( + pl.sum_horizontal(pl.all()) + ).item() # sum of all s_ij² over all genes + + intra_var_df = sample_variance_df.select( + [ + ( + pl.col(c) + - sample_variance_sum_over_genes + / (self.n_genes * (self.n_genes - 1)) + ).alias(c) for c in self.genes - ]) + ] ) # if some values are negative, we need a special process - corrected_intra_var_df = self.correct_negative_values(intra_var_df, group_count_df) + corrected_intra_var_df = self.correct_negative_values( + intra_var_df, group_count_df + ) return corrected_intra_var_df - def get_unbiased_intragroup_variances(self): - unbiased_intragroup_variance_dfs = [] means_over_samples_dfs = [] group_overall_means = [] for group, samples in tqdm(self.group_to_samples_dict.items()): - # sub dataframe corresponding to this group chunk_df = self.count_lf.select(samples).collect() # computing means over samples for each gene @@ -266,17 +288,20 @@ def get_unbiased_intragroup_variances(self): # getting overall expression average in the group for all genes group_overall_mean = self.get_overall_mean_for_group(means_over_samples_df) - group_unbiased_intragroup_variance_df = self.get_unbiased_intragroup_variance_for_group( - chunk_df, - means_over_samples_df, - group_overall_mean, - samples + group_unbiased_intragroup_variance_df = ( + self.get_unbiased_intragroup_variance_for_group( + chunk_df, means_over_samples_df, group_overall_mean, samples + ) ) # storing intragroup values for each gene in this group - unbiased_intragroup_variance_dfs.append(group_unbiased_intragroup_variance_df) + unbiased_intragroup_variance_dfs.append( + group_unbiased_intragroup_variance_df + ) # storing means over samples in this group for each gene - means_over_samples_df = means_over_samples_df.rename({"mean_over_samples_for_gene": group}) + means_over_samples_df = means_over_samples_df.rename( + {"mean_over_samples_for_gene": group} + ) means_over_samples_dfs.append(means_over_samples_df) # storing overall mean of expression in this group, for all genes and samples group_overall_means.append(group_overall_mean) @@ -287,100 +312,113 @@ def get_unbiased_intragroup_variances(self): for df in unbiased_intragroup_variance_dfs ] + # removing None values in group_overall_means + # which would originate from group chunk dataframes that are full of null values + group_overall_means = [mean for mean in group_overall_means if mean is not None] + # before returning: # concatenate together all intragroup variance data to have a single df for all groups # stack all means over samples horizontally (becomes a gene * group df ) # get the mean of group_overall_means to get the overall mean expression value in the count dataframe - return ( pl.concat(unbiased_intragroup_variance_dfs), pl.concat(means_over_samples_dfs, how="horizontal"), - mean(group_overall_means) + mean(group_overall_means), ) - def adjust_for_nb_of_samples_in_groups(self, unbiased_intragroup_variance_df): - n_samples_list = [ len(samples) for samples in self.group_to_samples_dict.values() ] - return ( - unbiased_intragroup_variance_df - .with_columns( - n_samples=pl.Series(n_samples_list) - ) - .select([ - (pl.col(c) / pl.col("n_samples")).alias(c) - for c in self.genes - ]) - ) - + n_samples_list = [ + len(samples) for samples in self.group_to_samples_dict.values() + ] + return unbiased_intragroup_variance_df.with_columns( + n_samples=pl.Series(n_samples_list) + ).select([(pl.col(c) / pl.col("n_samples")).alias(c) for c in self.genes]) - def get_unbiased_intergroup_variance(self, gene_means_in_groups_df, dataset_overall_mean): - mean_over_genes = gene_means_in_groups_df.mean().transpose().to_series().rename("mean_over_genes_for_group") + def get_unbiased_intergroup_variance( + self, gene_means_in_groups_df, dataset_overall_mean + ): + mean_over_genes = ( + gene_means_in_groups_df.mean() + .transpose() + .to_series() + .rename("mean_over_genes_for_group") + ) return ( - gene_means_in_groups_df - .with_columns( + gene_means_in_groups_df.with_columns( mean_over_groups_for_gene=pl.concat_list(pl.all()).row.mean() ) - .select([ - (pl.col(c) - pl.col("mean_over_groups_for_gene")).alias(c) - for c in gene_means_in_groups_df.columns - ]) + .select( + [ + (pl.col(c) - pl.col("mean_over_groups_for_gene")).alias(c) + for c in gene_means_in_groups_df.columns + ] + ) .transpose(column_names=self.genes) .hstack([mean_over_genes]) - .select([ - (pl.col(c) - pl.col("mean_over_genes_for_group") + dataset_overall_mean).alias(c) - for c in self.genes - ]) - .select([ (pl.col(c) ** 2).alias(c) for c in self.genes ]) # square to get variance + .select( + [ + ( + pl.col(c) + - pl.col("mean_over_genes_for_group") + + dataset_overall_mean + ).alias(c) + for c in self.genes + ] + ) + .select( + [(pl.col(c) ** 2).alias(c) for c in self.genes] + ) # square to get variance ) - def compute_gamma_factor(self, diff_df, vardiff_df): logger.info("Computing gamma factor") first_term = ( - diff_df - .with_columns( - sum_of_squares=pl.concat_list(pl.all()).row.sum() # sum over columns + diff_df.with_columns( + sum_of_squares=pl.concat_list(pl.all()).row.sum() # sum over columns ) .select("sum_of_squares") - .sum() # sum over rows + .sum() # sum over rows .select( - ( pl.col("sum_of_squares") / ((self.n_groups - 1) * (self.n_genes - 1)) ).alias("normalised_sum_of_squares") - + ( + pl.col("sum_of_squares") + / ((self.n_groups - 1) * (self.n_genes - 1)) + ).alias("normalised_sum_of_squares") ) .item() ) second_term = ( - vardiff_df - .with_columns( - sum=pl.concat_list(pl.all()).row.sum() # sum over columns + vardiff_df.with_columns( + sum=pl.concat_list(pl.all()).row.sum() # sum over columns ) .select("sum") - .sum() # sum over rows + .sum() # sum over rows .select( (pl.col("sum") / (self.n_groups * self.n_genes)).alias("normalised_sum") - ) .item() ) return max(first_term - second_term, 0) - @staticmethod def apply_gamma_factor(gamma, diff_df, vardiff_df): difnew = diff_df * gamma / (gamma + vardiff_df) varnew = vardiff_df + gamma * vardiff_df / (gamma + vardiff_df) return difnew, varnew - def apply_shrinkage(self, intergroup_variance_df, group_mean_variance_df): - gamma = self.compute_gamma_factor(intergroup_variance_df, group_mean_variance_df) - return self.apply_gamma_factor(gamma, intergroup_variance_df, group_mean_variance_df) - + gamma = self.compute_gamma_factor( + intergroup_variance_df, group_mean_variance_df + ) + return self.apply_gamma_factor( + gamma, intergroup_variance_df, group_mean_variance_df + ) - def get_stability_values(self, shrunk_intervar_df: pl.DataFrame, shrunk_gr_mean_var_df: pl.DataFrame): + def get_stability_values( + self, shrunk_intervar_df: pl.DataFrame, shrunk_gr_mean_var_df: pl.DataFrame + ): return ( ( shrunk_intervar_df.select([pl.col(c).abs() for c in self.genes]) @@ -390,33 +428,39 @@ def get_stability_values(self, shrunk_intervar_df: pl.DataFrame, shrunk_gr_mean_ .transpose( include_header=True, header_name=config.ENSEMBL_GENE_ID_COLNAME, - column_names=[config.NORMFINDER_STABILITY_VALUE_COLNAME] + column_names=[config.NORMFINDER_STABILITY_VALUE_COLNAME], ) ) - def compute_stability_scoring(self): - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # UNBIASED INTRAGROUP VARIANCE # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ logger.info("Computing intragroup variances") - intragroup_variance_df, gene_means_in_groups_df, dataset_overall_mean = self.get_unbiased_intragroup_variances() + intragroup_variance_df, gene_means_in_groups_df, dataset_overall_mean = ( + self.get_unbiased_intragroup_variances() + ) logger.info("Adjusting variances by group size") - group_mean_variance_df = self.adjust_for_nb_of_samples_in_groups(intragroup_variance_df) + group_mean_variance_df = self.adjust_for_nb_of_samples_in_groups( + intragroup_variance_df + ) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # INTERGROUP VARIANCE # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ logger.info("Computing intergroup variances") - intergroup_variance_df = self.get_unbiased_intergroup_variance(gene_means_in_groups_df, dataset_overall_mean) + intergroup_variance_df = self.get_unbiased_intergroup_variance( + gene_means_in_groups_df, dataset_overall_mean + ) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # STABILITY VALUES # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ logger.info("Shrinking intragroup and intergroup variances using gamma factor") - shrunk_intervar_df, shrunk_gr_mean_var_df = self.apply_shrinkage(intergroup_variance_df, group_mean_variance_df) + shrunk_intervar_df, shrunk_gr_mean_var_df = self.apply_shrinkage( + intergroup_variance_df, group_mean_variance_df + ) logger.info("Computing stability values") return self.get_stability_values(shrunk_intervar_df, shrunk_gr_mean_var_df) @@ -448,7 +492,6 @@ def export_stability(stabilities: pl.DataFrame): def main(): - args = parse_args() logger.info(f"Getting counts from {args.count_file}") @@ -457,7 +500,9 @@ def main(): logger.info(f"Getting design from {args.design_file}") design_df = pl.read_csv(args.design_file) # filter design df to keep only samples that are present in the count dataframe - design_df = design_df.filter(pl.col("sample").is_in(count_lf.collect_schema().names())) + design_df = design_df.filter( + pl.col("sample").is_in(count_lf.collect_schema().names()) + ) nfd = NormFinder(count_lf, design_df) stabilities = nfd.compute_stability_scoring() diff --git a/subworkflows/local/merge_data/main.nf b/subworkflows/local/merge_data/main.nf index 8bb6ad08..a2583125 100644 --- a/subworkflows/local/merge_data/main.nf +++ b/subworkflows/local/merge_data/main.nf @@ -54,9 +54,7 @@ workflow MERGE_DATA { meta, _ -> // extracts design file and adds batch column whenever missing (for custom datasets) def design_content = meta.design.splitCsv( header: true ) // if there is no batch, it is custom data - // prepending dataset id to sample name and adding it as batch identifier def updated_design_content = design_content.collect { row -> - row.sample = row.batch ?: "custom_${meta.dataset}_${row.sample}" row.batch = row.batch ?: "custom_${meta.dataset}" return row } From 33700491608a807c3d6b4e84d289ec2674b28d93 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Fri, 24 Oct 2025 17:20:15 +0200 Subject: [PATCH 084/258] fix conflict of keywords and skip_fetch_eatlas_accessions / skip_fetch_geo_accessions parameters --- conf/test_local_and_downloaded.config | 3 ++- subworkflows/local/expressionatlas_fetchdata/main.nf | 2 +- subworkflows/local/geo_fetchdata/main.nf | 2 +- .../local/utils_nfcore_stableexpression_pipeline/main.nf | 4 ++++ workflows/stableexpression.nf | 2 -- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/conf/test_local_and_downloaded.config b/conf/test_local_and_downloaded.config index 0f808ac9..83fc0d30 100644 --- a/conf/test_local_and_downloaded.config +++ b/conf/test_local_and_downloaded.config @@ -26,7 +26,8 @@ params { // Input data species = 'solanum tuberosum' keywords = "potato,stress" - eatlas_accessions = "E-MTAB-552" + eatlas_accessions = "E-MTAB-7711" + skip_fetch_eatlas_accessions = true skip_fetch_geo_accessions = true datasets = "tests/test_data/input_datasets/input.csv" outdir = "results/test_local_and_downloaded" diff --git a/subworkflows/local/expressionatlas_fetchdata/main.nf b/subworkflows/local/expressionatlas_fetchdata/main.nf index c5ee4ce2..e7dfc65e 100644 --- a/subworkflows/local/expressionatlas_fetchdata/main.nf +++ b/subworkflows/local/expressionatlas_fetchdata/main.nf @@ -30,7 +30,7 @@ workflow EXPRESSIONATLAS_FETCHDATA { .set { ch_input_accessions } // fetching Expression Atlas accessions if applicable - if ( !params.skip_fetch_eatlas_accessions || params.keywords ) { + if ( !params.skip_fetch_eatlas_accessions ) { // getting Expression Atlas accessions given a species name and keywords // keywords can be an empty string diff --git a/subworkflows/local/geo_fetchdata/main.nf b/subworkflows/local/geo_fetchdata/main.nf index 956cacfc..09820a57 100644 --- a/subworkflows/local/geo_fetchdata/main.nf +++ b/subworkflows/local/geo_fetchdata/main.nf @@ -30,7 +30,7 @@ workflow GEO_FETCHDATA { .set { ch_input_accessions } // fetching GEO accessions if applicable - if ( !params.skip_fetch_geo_accessions || params.keywords ) { + if ( !params.skip_fetch_geo_accessions ) { ch_excluded_accessions .collectFile( diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index 4c17fbac..2bf84276 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -182,6 +182,10 @@ def validateInputParameters(params) { } } + if ( params.keywords && params.skip_fetch_eatlas_accessions && params.skip_fetch_geo_accessions ) { + log.warn "Ignoring keywords as accessions will not be fetched from Expression Atlas or GEO" + } + } // diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index ed2b1e12..0709c83c 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -134,8 +134,6 @@ workflow STABLEEXPRESSION { // ----------------------------------------------------------------- // AGGREGATE ALL RESULTS FOR MULTIQC // ----------------------------------------------------------------- - ch_candidate_gene_stats_with_scores.view { v -> "ch_candidate_gene_stats_with_scores " + v} - ch_all_counts.view { v -> "ch_all_counts " + v} AGGREGATE_RESULTS ( ch_all_counts, From 96e4826abaed1af35f7950d841a040e3a0575719 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Fri, 24 Oct 2025 18:22:06 +0200 Subject: [PATCH 085/258] merge gene id mapping files and gene metadata files in the merge data subworkflow --- subworkflows/local/merge_data/main.nf | 42 ++++++++++++++++++++++++++- workflows/stableexpression.nf | 19 ++++++++---- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/subworkflows/local/merge_data/main.nf b/subworkflows/local/merge_data/main.nf index a2583125..021b5e68 100644 --- a/subworkflows/local/merge_data/main.nf +++ b/subworkflows/local/merge_data/main.nf @@ -13,12 +13,15 @@ workflow MERGE_DATA { take: ch_normalised_counts + ch_gene_id_mapping + ch_gene_metadata main: // ----------------------------------------------------------------- // MERGE COUNTS FOR EACH PLATFORM SEPARATELY // ----------------------------------------------------------------- + ch_normalised_counts .filter { meta, file -> meta.platform == "rnaseq" } .map { meta, file -> file } @@ -43,7 +46,7 @@ workflow MERGE_DATA { .mix ( ch_merged_microarray_counts ) .set { ch_platform_counts } - MERGE_ALL_COUNTS( ch_platform_counts.collect()) + MERGE_ALL_COUNTS( ch_platform_counts.collect() ) // ----------------------------------------------------------------- // MERGE ALL DESIGNS IN A SINGLE TABLE @@ -73,10 +76,47 @@ workflow MERGE_DATA { } .set { ch_whole_design } + // ----------------------------------------------------------------- + // MERGE ALL GENE ID MAPPINGS + // ----------------------------------------------------------------- + + ch_gene_id_mapping + .splitCsv( header: true ) + .unique() + .collectFile( + name: 'whole_gene_id_mapping.csv', + seed: "original_gene_id,ensembl_gene_id", + newLine: true, + sort: true, + storeDir: "${params.outdir}/idmapping/" + ) { + item -> "${item.original_gene_id},${item.ensembl_gene_id}" + } + .set { ch_whole_gene_id_mapping } + + // ----------------------------------------------------------------- + // MERGE ALL GENE METADATA + // ----------------------------------------------------------------- + + ch_gene_metadata + .splitCsv( header: true ) + .unique() + .collectFile( + name: 'whole_gene_metadata.csv', + seed: "ensembl_gene_id,name,description", + newLine: true, + sort: true, + storeDir: "${params.outdir}/idmapping/" + ) { + item -> "${item.ensembl_gene_id},${item.name},${item.description}" + } + .set { ch_whole_gene_metadata } emit: all_counts = MERGE_ALL_COUNTS.out.counts rnaseq_counts = ch_merged_rnaseq_counts microarray_counts = ch_merged_microarray_counts whole_design = ch_whole_design + whole_gene_id_mapping = ch_whole_gene_id_mapping + whole_gene_metadata = ch_whole_gene_metadata } diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 0709c83c..921a6090 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -67,16 +67,21 @@ workflow STABLEEXPRESSION { // IDMAPPING // ----------------------------------------------------------------- + ch_gene_id_mapping = params.gene_id_mapping_file ? Channel.fromPath( params.gene_id_mapping_file, checkIfExists: true ) : Channel.value( [] ) + ch_gene_metadata = params.gene_metadata ? Channel.fromPath( params.gene_metadata, checkIfExists: true ) : Channel.value( [] ) + if ( !params.skip_gprofiler ) { // tries to map gene IDs to Ensembl IDs whenever possible GPROFILER_IDMAPPING( ch_counts, ch_species, - params.gene_id_mapping_file ? Channel.fromPath( params.gene_id_mapping_file, checkIfExists: true ) : Channel.value( [] ), - params.gene_metadata ? Channel.fromPath( params.gene_metadata, checkIfExists: true ) : Channel.value( [] ) + ch_gene_id_mapping, + ch_gene_metadata ) GPROFILER_IDMAPPING.out.counts.set { ch_counts } + GPROFILER_IDMAPPING.out.mapping.set { ch_gene_id_mapping } + GPROFILER_IDMAPPING.out.metadata.set { ch_gene_metadata } } @@ -104,7 +109,11 @@ workflow STABLEEXPRESSION { // MERGE DATA // ----------------------------------------------------------------- - MERGE_DATA ( DATA_CLEANSING.out.cleaned_counts ) + MERGE_DATA ( + DATA_CLEANSING.out.cleaned_counts , + ch_gene_id_mapping, + ch_gene_metadata + ) MERGE_DATA.out.all_counts.set { ch_all_counts } MERGE_DATA.out.whole_design.set { ch_whole_design } @@ -138,8 +147,8 @@ workflow STABLEEXPRESSION { AGGREGATE_RESULTS ( ch_all_counts, ch_candidate_gene_stats_with_scores, - GPROFILER_IDMAPPING.out.metadata, - GPROFILER_IDMAPPING.out.mapping + MERGE_DATA.out.whole_gene_metadata, + MERGE_DATA.out.whole_gene_id_mapping ) AGGREGATE_RESULTS.out.top_stable_genes_summary.set { ch_top_stable_genes_summary } From cf84887b2184fe43a84417367f9ee9df8830395b Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Fri, 24 Oct 2025 18:59:44 +0200 Subject: [PATCH 086/258] normfinder and genorm and not run by default --- bin/merge_counts.py | 10 +++++----- conf/test_local_and_downloaded.config | 1 - nextflow.config | 3 ++- nextflow.config.rej | 11 +++++++++++ tests/default.nf.test | 28 +++++++++++++++++++++++++++ workflows/stableexpression.nf | 2 +- 6 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 nextflow.config.rej diff --git a/bin/merge_counts.py b/bin/merge_counts.py index 270fcbc6..ded11b8d 100755 --- a/bin/merge_counts.py +++ b/bin/merge_counts.py @@ -24,9 +24,7 @@ def parse_args(): - parser = argparse.ArgumentParser( - description="Merge count datasets" - ) + parser = argparse.ArgumentParser(description="Merge count datasets") parser.add_argument( "--counts", type=str, dest="count_files", required=True, help="Count files" ) @@ -88,7 +86,9 @@ def get_count_columns(lf: pl.LazyFrame) -> list[str]: The config.ENSEMBL_GENE_ID_COLNAME column contains only gene IDs. """ - return lf.select(pl.exclude(config.ENSEMBL_GENE_ID_COLNAME)).collect_schema().names() + return ( + lf.select(pl.exclude(config.ENSEMBL_GENE_ID_COLNAME)).collect_schema().names() + ) def get_counts(files: list[Path]) -> pl.DataFrame: @@ -125,7 +125,7 @@ def get_nb_rows(lf: pl.LazyFrame) -> int: ##################################################### -def export_data(count_df: pl.DataFrame ): +def export_data(count_df: pl.DataFrame): """Export gene expression data.""" logger.info(f"Exporting normalised counts to: {ALL_COUNTS_PARQUET_OUTFILENAME}") count_df.write_parquet(ALL_COUNTS_PARQUET_OUTFILENAME) diff --git a/conf/test_local_and_downloaded.config b/conf/test_local_and_downloaded.config index 83fc0d30..d120153f 100644 --- a/conf/test_local_and_downloaded.config +++ b/conf/test_local_and_downloaded.config @@ -25,7 +25,6 @@ params { // Input data species = 'solanum tuberosum' - keywords = "potato,stress" eatlas_accessions = "E-MTAB-7711" skip_fetch_eatlas_accessions = true skip_fetch_geo_accessions = true diff --git a/nextflow.config b/nextflow.config index d2606572..0273db48 100644 --- a/nextflow.config +++ b/nextflow.config @@ -47,7 +47,7 @@ params { ks_pvalue_threshold = 0 // stability scoring - skip_genorm = false + run_genorm = false candidate_selection_descriptor = "std" // Boilerplate options @@ -223,6 +223,7 @@ profiles { } test { includeConfig 'conf/test.config' } + test_run_normfinder_genorm { includeConfig 'conf/test_run_normfinder_genorm.config' } test_ignore_errors { includeConfig 'conf/test_ignore_errors.config' } test_eatlas_only { includeConfig 'conf/test_eatlas_only.config' } test_full { includeConfig 'conf/test_full.config' } diff --git a/nextflow.config.rej b/nextflow.config.rej new file mode 100644 index 00000000..34c3740b --- /dev/null +++ b/nextflow.config.rej @@ -0,0 +1,11 @@ +diff a/nextflow.config b/nextflow.config (rejected hunks) +@@ -47,7 +47,8 @@ params { + ks_pvalue_threshold = 0 + + // stability scoring +- skip_genorm = false ++ run_normfinder = false ++ run_genorm = false + candidate_selection_descriptor = "std" + + // MultiQC options diff --git a/tests/default.nf.test b/tests/default.nf.test index c13bcbef..a5642489 100644 --- a/tests/default.nf.test +++ b/tests/default.nf.test @@ -134,4 +134,32 @@ nextflow_pipeline { ) } } + + test("-profile test_run_normfinder_genorm") { + + when { + params { + species = 'solanum tuberosum' + eatlas_accessions = "E-MTAB-7711" + skip_fetch_eatlas_accessions = true + skip_fetch_geo_accessions = true + run_normfinder = true + run_genorm = true + outdir = "$outputDir" + } + } + + then { + def stable_name = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}']) + def stable_path = getAllFilesFromDir(params.outdir, ignoreFile: 'tests/.nftignore') + assertAll( + { assert workflow.success}, + { assert snapshot( + removeNextflowVersion("$outputDir/pipeline_info/nf_core_stableexpression_software_mqc_versions.yml"), + stable_name, + stable_path + ).match() } + ) + } + } } diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 921a6090..87353245 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -139,7 +139,7 @@ workflow STABLEEXPRESSION { ) STABILITY_SCORING.out.summary_statistics.set { ch_candidate_gene_stats_with_scores } - + //ch_candidate_gene_stats_with_scores.splitCsv(header: true).view() // ----------------------------------------------------------------- // AGGREGATE ALL RESULTS FOR MULTIQC // ----------------------------------------------------------------- From 60222114d1df984f20db7bdb53ea632ef698f115 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Fri, 24 Oct 2025 19:09:07 +0200 Subject: [PATCH 087/258] correction of weights by division by the sum of weights for the computation of stability score --- bin/compute_stability_scores.py | 39 ++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/bin/compute_stability_scores.py b/bin/compute_stability_scores.py index b3581501..b6fd5012 100755 --- a/bin/compute_stability_scores.py +++ b/bin/compute_stability_scores.py @@ -22,25 +22,22 @@ @dataclass class StabilityScorer: - N_QUANTILES: ClassVar[int] = 1000 WEIGHT: ClassVar[dict] = { config.VARIATION_COEFFICIENT_COLNAME: 0.7, config.MAD_COLNAME: 0.1, config.NORMFINDER_STABILITY_VALUE_COLNAME: 0.1, - config.GENORM_M_MEASURE_COLNAME: 0.1 + config.GENORM_M_MEASURE_COLNAME: 0.1, } WEIGHT_RATIO_NB_NULLS_TO_SCORING: ClassVar[float] = 1 df: pl.DataFrame - def __post_init__(self): self.compute_stability_score() - @staticmethod def quantile_normalise(data: pl.Series, new_name: str) -> pl.Series: """ @@ -51,28 +48,38 @@ def quantile_normalise(data: pl.Series, new_name: str) -> pl.Series: normalised_array = transformer.fit_transform(array) return pl.Series(new_name, normalised_array.ravel()) - def compute_stability_score(self) -> pl.LazyFrame: logger.info("Computing stability score for candidate genes") - candidate_df = self.df.filter(pl.col(config.IS_CANDIDATE_COLNAME) == 1) # keep only candidate genes + candidate_df = self.df.filter( + pl.col(config.IS_CANDIDATE_COLNAME) == 1 + ) # keep only candidate genes non_candidate_df = self.df.filter(pl.col(config.IS_CANDIDATE_COLNAME).is_null()) normalised_data = {} null_data = {} - for col in self.WEIGHT: + weight_sum = 0 + for col, weight in self.WEIGHT.items(): if col not in self.df.columns: continue data = candidate_df.select(col).to_series() normalised_col = f"{col}_normalised" - normalised_data[col] = self.quantile_normalise(data, new_name=normalised_col) + normalised_data[col] = self.quantile_normalise( + data, new_name=normalised_col + ) # creating a null column with same name null_data[col] = pl.Series(normalised_col, [None] * len(non_candidate_df)) + # if this column is present, add its weight to the sum + weight_sum += weight # replacing original data with quantile normalised ones - candidate_df = candidate_df.with_columns(data for data in normalised_data.values()) + candidate_df = candidate_df.with_columns( + data for data in normalised_data.values() + ) # adding null columns to the non-candidate df to allow concatenation - non_candidate_df = non_candidate_df.with_columns(data for data in null_data.values()) + non_candidate_df = non_candidate_df.with_columns( + data for data in null_data.values() + ) # concatenating with non candidate genes to have all genes self.df = pl.concat([candidate_df, non_candidate_df]) @@ -83,13 +90,14 @@ def compute_stability_score(self) -> pl.LazyFrame: # adding penalty for samples with null values # genes with at least one zero value are already excluded at that stage stability_scoring_expr = ( - pl.col(config.RATIO_NULLS_VALID_SAMPLES_COLNAME) * self.WEIGHT_RATIO_NB_NULLS_TO_SCORING + pl.col(config.RATIO_NULLS_VALID_SAMPLES_COLNAME) + * self.WEIGHT_RATIO_NB_NULLS_TO_SCORING ) for col, weight in self.WEIGHT.items(): if col not in self.df.columns: continue normalised_col = f"{col}_normalised" - stability_scoring_expr += (pl.col(normalised_col) * weight) + stability_scoring_expr += pl.col(normalised_col) * weight / weight_sum # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -104,8 +112,9 @@ def compute_stability_score(self) -> pl.LazyFrame: def get_statistics_with_stability_scores(self): return ( - self.df - .sort(config.STABILITY_SCORE_COLNAME, descending=False, nulls_last=True) + self.df.sort( + config.STABILITY_SCORE_COLNAME, descending=False, nulls_last=True + ) .with_row_index(name="index") .with_columns((pl.col("index") + 1).alias(config.RANK_COLNAME)) .drop("index") @@ -128,7 +137,7 @@ def parse_args(): type=str, dest="platform_stat_files", required=True, - help="Platform stat file" + help="Platform stat file", ) parser.add_argument( "--stabilities", From 53b8a0f5c47d515e7e23a8b38a6dc2c3c4cead6a Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sat, 25 Oct 2025 10:59:53 +0200 Subject: [PATCH 088/258] run normfinder automatically, genorm stays optional --- bin/compute_stability_scores.py | 62 +++++++++++++------ conf/test_run_normfinder_genorm.config | 34 ++++++++++ .../local/compute_stability_scores/main.nf | 9 ++- nextflow.config | 1 + nextflow.config.rej | 11 ---- nextflow_schema.json | 14 ++++- subworkflows/local/stability_scoring/main.nf | 15 ++--- 7 files changed, 104 insertions(+), 42 deletions(-) create mode 100644 conf/test_run_normfinder_genorm.config delete mode 100644 nextflow.config.rej diff --git a/bin/compute_stability_scores.py b/bin/compute_stability_scores.py index b6fd5012..03810bc4 100755 --- a/bin/compute_stability_scores.py +++ b/bin/compute_stability_scores.py @@ -6,7 +6,6 @@ import polars as pl from pathlib import Path from sklearn.preprocessing import QuantileTransformer -import numpy as np from dataclasses import dataclass, field from typing import ClassVar import logging @@ -24,19 +23,28 @@ class StabilityScorer: N_QUANTILES: ClassVar[int] = 1000 - WEIGHT: ClassVar[dict] = { - config.VARIATION_COEFFICIENT_COLNAME: 0.7, - config.MAD_COLNAME: 0.1, - config.NORMFINDER_STABILITY_VALUE_COLNAME: 0.1, - config.GENORM_M_MEASURE_COLNAME: 0.1, - } + WEIGHT_FIELDS: ClassVar[list] = [ + config.VARIATION_COEFFICIENT_COLNAME, + config.MAD_COLNAME, + config.NORMFINDER_STABILITY_VALUE_COLNAME, + config.GENORM_M_MEASURE_COLNAME, + ] WEIGHT_RATIO_NB_NULLS_TO_SCORING: ClassVar[float] = 1 df: pl.DataFrame + stability_score_weights_str: str + weights: dict[str, float] = field(default_factory=dict) def __post_init__(self): self.compute_stability_score() + self.parse_stability_score_weights() + + def parse_stability_score_weights(self): + for field, weight in zip( + self.WEIGHT_FIELDS, self.stability_score_weights_str.split(",") + ): + self.weights[field] = float(weight) @staticmethod def quantile_normalise(data: pl.Series, new_name: str) -> pl.Series: @@ -59,7 +67,7 @@ def compute_stability_score(self) -> pl.LazyFrame: normalised_data = {} null_data = {} weight_sum = 0 - for col, weight in self.WEIGHT.items(): + for col, weight in self.weights.items(): if col not in self.df.columns: continue data = candidate_df.select(col).to_series() @@ -93,7 +101,7 @@ def compute_stability_score(self) -> pl.LazyFrame: pl.col(config.RATIO_NULLS_VALID_SAMPLES_COLNAME) * self.WEIGHT_RATIO_NB_NULLS_TO_SCORING ) - for col, weight in self.WEIGHT.items(): + for col, weight in self.weights.items(): if col not in self.df.columns: continue normalised_col = f"{col}_normalised" @@ -140,13 +148,25 @@ def parse_args(): help="Platform stat file", ) parser.add_argument( - "--stabilities", + "--normfinder-stability", type=str, - dest="stability_files", required=True, - help="Output files of Normfinder / Genorm", + dest="normfinder_stability_file", + help="Output files of Normfinder", + ) + parser.add_argument( + "--genorm-stability", + type=str, + dest="genorm_stability_file", + help="Output files of Genorm", + ) + parser.add_argument( + "--weights", + dest="stability_score_weights", + type=str, + required=True, + help="Weights for Standard deviation / Median absolute deviation / Normfinder / Genorm respectively. Must be a comma-separated string. Example: 0.7,0.1,0.1,0.1", ) - return parser.parse_args() @@ -187,18 +207,22 @@ def export_data(scored_lf: pl.LazyFrame): def main(): args = parse_args() - stability_files = [Path(file) for file in args.stability_files.split(" ")] stat_files = [Path(file) for file in args.platform_stat_files.split(" ")] + stat_lf = get_statistics(stat_files) - # getting metadata and mappings - stability_df = get_stabilities(stability_files) - stat_df = get_statistics(stat_files) + stability_files = [ + Path(file) + for file in [args.normfinder_stability_file, args.genorm_stability_file] + if file is not None + ] + # getting metadata and mappings + stability_lf = get_stabilities(stability_files) # merges base statistics with computed stability measurements - lf = stat_df.join(stability_df, on=config.ENSEMBL_GENE_ID_COLNAME, how="left") + lf = stat_lf.join(stability_lf, on=config.ENSEMBL_GENE_ID_COLNAME, how="left") # sort genes according to the metrics present in the dataframe - stability_scorer = StabilityScorer(lf.collect()) + stability_scorer = StabilityScorer(lf.collect(), args.stability_score_weights) scored_lf = stability_scorer.get_statistics_with_stability_scores() # exporting computed data diff --git a/conf/test_run_normfinder_genorm.config b/conf/test_run_normfinder_genorm.config new file mode 100644 index 00000000..efc56522 --- /dev/null +++ b/conf/test_run_normfinder_genorm.config @@ -0,0 +1,34 @@ +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Nextflow config file for running minimal tests +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Defines input files and everything required to run a fast and simple pipeline test. + It tests the different ways to use the pipeline, with small data + + Use as follows: + nextflow run nf-core/stableexpression -profile test, --outdir + +---------------------------------------------------------------------------------------- +*/ + +process { + resourceLimits = [ + cpus: 4, + memory: '15.GB', + time: '1.h' + ] +} + +params { + config_profile_name = 'Test dataset profile' + config_profile_description = 'Minimal test dataset to check pipeline function' + + // Input data + species = 'solanum tuberosum' + eatlas_accessions = "E-MTAB-7711" + skip_fetch_eatlas_accessions = true + skip_fetch_geo_accessions = true + run_normfinder = true + run_genorm = true + outdir = "results/test" +} diff --git a/modules/local/compute_stability_scores/main.nf b/modules/local/compute_stability_scores/main.nf index b3241f53..a6fad34c 100644 --- a/modules/local/compute_stability_scores/main.nf +++ b/modules/local/compute_stability_scores/main.nf @@ -9,7 +9,9 @@ process COMPUTE_STABILITY_SCORES { input: path stat_file - path stability_files, stageAs: "?/*" + val stability_score_weights + path normfinder_stability_file + val genorm_stability_file output: path 'stats_with_scores.csv', emit: stats_with_stability_scores @@ -17,10 +19,13 @@ process COMPUTE_STABILITY_SCORES { tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions script: + def genorm_stability_file_arg = genorm_stability_file ? "--genorm-stability $genorm_stability_file" : "" """ compute_stability_scores.py \\ --stats $stat_file \\ - --stabilities "$stability_files" + --weights "$stability_score_weights" \\ + --normfinder-stability $normfinder_stability_file \\ + $genorm_stability_file_arg """ } diff --git a/nextflow.config b/nextflow.config index 0273db48..9e878dda 100644 --- a/nextflow.config +++ b/nextflow.config @@ -49,6 +49,7 @@ params { // stability scoring run_genorm = false candidate_selection_descriptor = "std" + stability_score_weights = "0.7,0.1,0.1,0.1" // Boilerplate options outdir = null diff --git a/nextflow.config.rej b/nextflow.config.rej deleted file mode 100644 index 34c3740b..00000000 --- a/nextflow.config.rej +++ /dev/null @@ -1,11 +0,0 @@ -diff a/nextflow.config b/nextflow.config (rejected hunks) -@@ -47,7 +47,8 @@ params { - ks_pvalue_threshold = 0 - - // stability scoring -- skip_genorm = false -+ run_normfinder = false -+ run_genorm = false - candidate_selection_descriptor = "std" - - // MultiQC options diff --git a/nextflow_schema.json b/nextflow_schema.json index a03706c4..40c79a5f 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -248,11 +248,19 @@ "default": "std", "help_text": "Candidate genes are chosen based on a certain statistical descriptor. Set this parameter to modify the descriptor used." }, - "skip_genorm": { + "run_genorm": { "type": "boolean", - "description": "Skip the Genorm step", + "description": "Run Genorm", "fa_icon": "fas fa-ban", - "help": "Genorm is run by default and provides additional information about gene stability. Set this parameter to skip this step." + "help": "Genorm is NOT run by default. To run and get additional information about gene stability, set this parameter." + }, + "stability_score_weights": { + "type": "string", + "description": "Weights for stability score calculation", + "fa_icon": "fas fa-chart-simple", + "default": "0.7,0.1,0.1,0.1", + "help_text": "Weights for Standard deviation / Median absolute deviation / Normfinder / Genorm respectively. Must be a comma-separated string. Example: 0.7,0.1,0.1,0.1", + "pattern": "^\\d+(\\.\\d+)?,\\d+(\\.\\d+)?,\\d+(\\.\\d+)?,\\d+(\\.\\d+)?$" }, "nb_top_gene_candidates": { "type": "integer", diff --git a/subworkflows/local/stability_scoring/main.nf b/subworkflows/local/stability_scoring/main.nf index e7e4ad14..20c4bd65 100644 --- a/subworkflows/local/stability_scoring/main.nf +++ b/subworkflows/local/stability_scoring/main.nf @@ -38,18 +38,17 @@ workflow STABILITY_SCORING { ch_candidate_gene_counts, ch_design ) - NORMFINDER.out.stability_values.set { ch_stability_scores } + NORMFINDER.out.stability_values.set { ch_normfinder_stability } // ----------------------------------------------------------------- // GENORM // ----------------------------------------------------------------- - if ( !params.skip_genorm ) { + if ( params.run_genorm ) { GENORM ( ch_candidate_gene_counts ) - - ch_stability_scores - .mix ( GENORM.out.m_measures ) - .set { ch_stability_scores } + GENORM.out.m_measures.set { ch_genorm_stability } + } else { + ch_genorm_stability = Channel.value([]) } // ----------------------------------------------------------------- @@ -58,7 +57,9 @@ workflow STABILITY_SCORING { COMPUTE_STABILITY_SCORES ( ch_stats, - ch_stability_scores.collect() + params.stability_score_weights, + ch_normfinder_stability, + ch_genorm_stability ) From 4c9fbde0c76f801b2939e09be84aa184780c1b22 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sun, 26 Oct 2025 00:00:07 +0200 Subject: [PATCH 089/258] add nb genes and nb samples in meta map at several steps of the workflow --- workflows/stableexpression.nf | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 87353245..7f1c2289 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -17,6 +17,28 @@ include { GPROFILER_IDMAPPING } from '../modules/local/gprofi include { AGGREGATE_RESULTS } from '../modules/local/aggregate_results' include { DASH_APP } from '../modules/local/dash_app' + +/* +======================================================================================== + FUNCTIONS +======================================================================================== +*/ +// +// Check and validate pipeline parameters +// + +def storeDatasetSize( ch_counts, nb_genes_key, nb_samples_key ) { + // adding nb genes and nb samples in the meta map + return ch_counts + .map { meta, file -> + def content = file.splitCsv( header: true ) + meta[nb_genes_key] = content.size() + meta[nb_samples_key] = content[0].findAll {it.key != 'ensembl_gene_id'}.size() + [ meta, file ] + } +} + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ RUN MAIN WORKFLOW @@ -63,6 +85,8 @@ workflow STABLEEXPRESSION { .concat( GEO_FETCHDATA.out.downloaded_datasets ) .set { ch_counts } + ch_counts = storeDatasetSize( ch_counts, "nb_genes", "nb_samples" ) + // ----------------------------------------------------------------- // IDMAPPING // ----------------------------------------------------------------- @@ -83,6 +107,8 @@ workflow STABLEEXPRESSION { GPROFILER_IDMAPPING.out.mapping.set { ch_gene_id_mapping } GPROFILER_IDMAPPING.out.metadata.set { ch_gene_metadata } + ch_counts = storeDatasetSize( ch_counts, "nb_genes_after_idmapping", "nb_samples_after_idmapping" ) + } // ----------------------------------------------------------------- @@ -105,6 +131,8 @@ workflow STABLEEXPRESSION { params.ks_pvalue_threshold ) + ch_counts = storeDatasetSize( ch_counts, "nb_genes_after_cleaning", "nb_samples_after_cleaning" ) + ch_counts.view() // ----------------------------------------------------------------- // MERGE DATA // ----------------------------------------------------------------- From 516280fc11a4917ef35c74b02449ebfcf5a99476 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sun, 26 Oct 2025 08:48:24 +0100 Subject: [PATCH 090/258] build memory requirement for MERGE_COUNTS dynamically based on total dataset size --- conf/modules/merging.config | 5 +++ modules/local/merge/counts/main.nf | 8 ++++- .../local/expressionatlas_fetchdata/main.nf | 7 ++-- subworkflows/local/merge_data/main.nf | 32 +++++++++++++++---- .../main.nf | 20 ++++++++++-- workflows/stableexpression.nf | 27 +++------------- 6 files changed, 64 insertions(+), 35 deletions(-) diff --git a/conf/modules/merging.config b/conf/modules/merging.config index eab59238..ce76a569 100644 --- a/conf/modules/merging.config +++ b/conf/modules/merging.config @@ -1,5 +1,10 @@ process { + withName: 'MERGE_COUNTS' { + cpus = 12 + maxRetries = 5 + } + withName: 'MERGE_RNASEQ_COUNTS' { publishDir = [ path: { "${params.outdir}/merged_datasets/rnaseq/" }, diff --git a/modules/local/merge/counts/main.nf b/modules/local/merge/counts/main.nf index e2614e86..335f7644 100644 --- a/modules/local/merge/counts/main.nf +++ b/modules/local/merge/counts/main.nf @@ -1,6 +1,10 @@ process MERGE_COUNTS { - label 'process_high' + memory { def calc = (dataset_size / 5000).toInteger() + def result = Math.max(1, calc) // Ensure at least 1 MB + def multiplicator = 1 + 0.2 * task.attempt // increase memory usage with each attempt by 20% + return 1.MB * result * multiplicator + } conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? @@ -9,6 +13,7 @@ process MERGE_COUNTS { input: path count_files, stageAs: "?/*" + val dataset_size output: path 'all_counts.parquet', emit: counts @@ -16,6 +21,7 @@ process MERGE_COUNTS { tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions script: + println task.memory """ merge_counts.py \\ --counts "$count_files" diff --git a/subworkflows/local/expressionatlas_fetchdata/main.nf b/subworkflows/local/expressionatlas_fetchdata/main.nf index e7dfc65e..1a1a0ee0 100644 --- a/subworkflows/local/expressionatlas_fetchdata/main.nf +++ b/subworkflows/local/expressionatlas_fetchdata/main.nf @@ -1,8 +1,9 @@ include { EXPRESSIONATLAS_GETACCESSIONS } from '../../../modules/local/expressionatlas/getaccessions' include { EXPRESSIONATLAS_GETDATA } from '../../../modules/local/expressionatlas/getdata' -include { addDatasetIdToMetadata } from '../utils_nfcore_stableexpression_pipeline' -include { groupFilesByDatasetId } from '../utils_nfcore_stableexpression_pipeline' -include { augmentToMetadata } from '../utils_nfcore_stableexpression_pipeline' + +include { addDatasetIdToMetadata } from '../utils_nfcore_stableexpression_pipeline' +include { groupFilesByDatasetId } from '../utils_nfcore_stableexpression_pipeline' +include { augmentToMetadata } from '../utils_nfcore_stableexpression_pipeline' /* ======================================================================================== diff --git a/subworkflows/local/merge_data/main.nf b/subworkflows/local/merge_data/main.nf index 021b5e68..51e5d138 100644 --- a/subworkflows/local/merge_data/main.nf +++ b/subworkflows/local/merge_data/main.nf @@ -2,6 +2,8 @@ include { MERGE_COUNTS as MERGE_ALL_COUNTS } from '../../../modules include { MERGE_COUNTS as MERGE_RNASEQ_COUNTS } from '../../../modules/local/merge/counts' include { MERGE_COUNTS as MERGE_MICROARRAY_COUNTS } from '../../../modules/local/merge/counts' +include { getWholeDatasetSize } from '../../../subworkflows/local/utils_nfcore_stableexpression_pipeline' + /* ======================================================================================== @@ -22,20 +24,30 @@ workflow MERGE_DATA { // MERGE COUNTS FOR EACH PLATFORM SEPARATELY // ----------------------------------------------------------------- + // RNASEQ ch_normalised_counts .filter { meta, file -> meta.platform == "rnaseq" } - .map { meta, file -> file } .set { ch_normalised_rnaseq_counts } - MERGE_RNASEQ_COUNTS ( ch_normalised_rnaseq_counts.collect() ) + ch_whole_rnaseq_size = getWholeDatasetSize ( ch_normalised_rnaseq_counts ) + + MERGE_RNASEQ_COUNTS ( + ch_normalised_rnaseq_counts.map { meta, file -> file }.collect(), + ch_whole_rnaseq_size + ) MERGE_RNASEQ_COUNTS.out.counts.set { ch_merged_rnaseq_counts } - ch_normalised_counts + // MICROARRAY + ch_normalised_counts .filter { meta, file -> meta.platform == "microarray" } - .map { meta, file -> file } .set { ch_normalised_microarray_counts } - MERGE_MICROARRAY_COUNTS ( ch_normalised_microarray_counts.collect() ) + ch_whole_microarray_size = getWholeDatasetSize ( ch_normalised_microarray_counts ) + + MERGE_MICROARRAY_COUNTS ( + ch_normalised_microarray_counts.map { meta, file -> file }.collect(), + ch_whole_microarray_size + ) MERGE_MICROARRAY_COUNTS.out.counts.set { ch_merged_microarray_counts } // ----------------------------------------------------------------- @@ -46,7 +58,15 @@ workflow MERGE_DATA { .mix ( ch_merged_microarray_counts ) .set { ch_platform_counts } - MERGE_ALL_COUNTS( ch_platform_counts.collect() ) + ch_whole_rnaseq_size + .mix(ch_whole_microarray_size) + .reduce { rnaseq_size, microarray_size -> rnaseq_size + microarray_size } + .set { ch_whole_size } + + MERGE_ALL_COUNTS( + ch_platform_counts.collect(), + ch_whole_size + ) // ----------------------------------------------------------------- // MERGE ALL DESIGNS IN A SINGLE TABLE diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index 2bf84276..c4f5fccf 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -384,6 +384,22 @@ def augmentToMetadata( ch_files ) { } } +def storeDatasetSize( ch_counts, nb_genes_key, nb_samples_key ) { + // adding nb genes and nb samples in the meta map under keys provided as parameters + return ch_counts + .map { meta, file -> + def content = file.splitCsv( header: true ) + meta[nb_genes_key] = content.size() + meta[nb_samples_key] = content[0].findAll {it.key != 'ensembl_gene_id'}.size() + [ meta, file ] + } +} - - +def getWholeDatasetSize( ch_counts ) { + return ch_counts + .map { meta, file -> + [ meta.nb_genes_final * meta.nb_samples_final ] + } + .reduce { size_1, size_2 -> size_1 + size_2 } + .flatten() +} diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 7f1c2289..5188be0b 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -17,26 +17,7 @@ include { GPROFILER_IDMAPPING } from '../modules/local/gprofi include { AGGREGATE_RESULTS } from '../modules/local/aggregate_results' include { DASH_APP } from '../modules/local/dash_app' - -/* -======================================================================================== - FUNCTIONS -======================================================================================== -*/ -// -// Check and validate pipeline parameters -// - -def storeDatasetSize( ch_counts, nb_genes_key, nb_samples_key ) { - // adding nb genes and nb samples in the meta map - return ch_counts - .map { meta, file -> - def content = file.splitCsv( header: true ) - meta[nb_genes_key] = content.size() - meta[nb_samples_key] = content[0].findAll {it.key != 'ensembl_gene_id'}.size() - [ meta, file ] - } -} +include { storeDatasetSize } from '../subworkflows/local/utils_nfcore_stableexpression_pipeline' /* @@ -131,8 +112,8 @@ workflow STABLEEXPRESSION { params.ks_pvalue_threshold ) - ch_counts = storeDatasetSize( ch_counts, "nb_genes_after_cleaning", "nb_samples_after_cleaning" ) - ch_counts.view() + ch_counts = storeDatasetSize( ch_counts, "nb_genes_final", "nb_samples_final" ) + // ----------------------------------------------------------------- // MERGE DATA // ----------------------------------------------------------------- @@ -147,7 +128,7 @@ workflow STABLEEXPRESSION { MERGE_DATA.out.whole_design.set { ch_whole_design } // ----------------------------------------------------------------- - // COMPUTE BASE STATISTICS FOR ALL GENES + // COMPUTE BASE STATISTICS FOR ALL addDatasetIdToMetadataGENES // ----------------------------------------------------------------- BASE_STATISTICS ( From dcbcb460378ae383d0c177dbe50b7cadafc1392b Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sun, 26 Oct 2025 19:13:07 +0100 Subject: [PATCH 091/258] use dataframes instead of lazyframes during count merging --- bin/merge_counts.py | 75 ++++++++++++++---------------- modules/local/merge/counts/main.nf | 2 +- 2 files changed, 36 insertions(+), 41 deletions(-) diff --git a/bin/merge_counts.py b/bin/merge_counts.py index ded11b8d..1d1953a8 100755 --- a/bin/merge_counts.py +++ b/bin/merge_counts.py @@ -3,6 +3,7 @@ # Written by Olivier Coen. Released under the MIT license. import argparse +from tqdm import tqdm import polars as pl from pathlib import Path import logging @@ -36,23 +37,22 @@ def parse_args(): ##################################################### -def parse_count_file(count_file: Path) -> pl.LazyFrame: - lf = pl.scan_parquet(count_file) +def parse_count_file(count_file: Path) -> pl.DataFrame: + df = pl.read_parquet(count_file) # in some cases, the first column may have an empty name or be different than config.ENSEMBL_GENE_ID_COLNAME # in any case, this column must have the config.ENSEMBL_GENE_ID_COLNAME name - first_column_name = lf.collect_schema().names()[0] + first_column_name = df.columns[0] if first_column_name != config.ENSEMBL_GENE_ID_COLNAME: - lf = lf.rename({first_column_name: config.ENSEMBL_GENE_ID_COLNAME}) - return lf + df = df.rename({first_column_name: config.ENSEMBL_GENE_ID_COLNAME}) + return df -def is_valid_df(lf: pl.LazyFrame, file: Path) -> bool: - """Check if a LazyFrame is valid. - - A LazyFrame is considered valid if it contains at least one row. +def is_valid_df(df: pl.DataFrame, file: Path) -> bool: + """Check if a DataFrame is valid. + A DataFrame is considered valid if it contains at least one row. """ try: - return not lf.limit(1).collect().is_empty() + return not df.limit(1).is_empty() except FileNotFoundError: # strangely enough we get this error for some files existing but empty logger.error(f"Could not find file {str(file)}") @@ -62,33 +62,30 @@ def is_valid_df(lf: pl.LazyFrame, file: Path) -> bool: return False -def get_valid_lazy_dfs(files: list[Path]) -> list[pl.LazyFrame]: - """Get a list of valid LazyFrames from a list of files. - - A LazyFrame is considered valid if it contains at least one row. +def get_valid_lazy_dfs(files: list[Path]) -> list[pl.DataFrame]: + """Get a list of valid DataFrames from a list of files. + A DataFrame is considered valid if it contains at least one row. """ - lf_dict = {file: parse_count_file(file) for file in files} - return [lf for file, lf in lf_dict.items() if is_valid_df(lf, file)] + df_dict = {file: parse_count_file(file) for file in tqdm(files)} + return [df for file, df in df_dict.items()] -def join_count_dfs(lf1: pl.LazyFrame, lf2: pl.LazyFrame) -> pl.LazyFrame: - """Join two LazyFrames on the config.ENSEMBL_GENE_ID_COLNAME column. +def join_count_dfs(df1: pl.DataFrame, df2: pl.DataFrame) -> pl.DataFrame: + """Join two DataFrames on the config.ENSEMBL_GENE_ID_COLNAME column. The how parameter is set to "full" to include all rows from both dfs. The coalesce parameter is set to True to fill NaN values in the resulting dataframe with values from the other dataframe. """ - return lf1.join(lf2, on=config.ENSEMBL_GENE_ID_COLNAME, how="full", coalesce=True) + return df1.join(df2, on=config.ENSEMBL_GENE_ID_COLNAME, how="full", coalesce=True) -def get_count_columns(lf: pl.LazyFrame) -> list[str]: +def get_count_columns(df: pl.DataFrame) -> list[str]: """Get all column names except the config.ENSEMBL_GENE_ID_COLNAME column. The config.ENSEMBL_GENE_ID_COLNAME column contains only gene IDs. """ - return ( - lf.select(pl.exclude(config.ENSEMBL_GENE_ID_COLNAME)).collect_schema().names() - ) + return df.select(pl.exclude(config.ENSEMBL_GENE_ID_COLNAME)).columns def get_counts(files: list[Path]) -> pl.DataFrame: @@ -97,27 +94,24 @@ def get_counts(files: list[Path]) -> pl.DataFrame: The files are merged into a single dataframe. The config.ENSEMBL_GENE_ID_COLNAME column is cast to String, and all other columns are cast to Float64. """ - # lazy loading - lfs = get_valid_lazy_dfs(files) + logger.info("Parsing counts") + dfs = get_valid_lazy_dfs(files) + # joining all count files - merged_lf = reduce(join_count_dfs, lfs) + logger.info( + f"Joining count files recursively on the {config.ENSEMBL_GENE_ID_COLNAME} column" + ) + merged_df = reduce(join_count_dfs, tqdm(dfs)) - count_columns = get_count_columns(merged_lf) + count_columns = get_count_columns(merged_df) # casting count columns to Float64 - # casting gene id column to String + # casting gene id column to Stringcount_files # casting nans to nulls - return ( - merged_lf.select( - [pl.col(config.ENSEMBL_GENE_ID_COLNAME).cast(pl.String)] - + [pl.col(column).cast(pl.Float64) for column in count_columns] - ) - .fill_nan(None) - .collect() - ) - - -def get_nb_rows(lf: pl.LazyFrame) -> int: - return lf.select(pl.len()).collect().item() + logger.info("Cleaning mergeed dataframe") + return merged_df.select( + [pl.col(config.ENSEMBL_GENE_ID_COLNAME).cast(pl.String)] + + [pl.col(column).cast(pl.Float64) for column in count_columns] + ).fill_nan(None) ##################################################### @@ -141,6 +135,7 @@ def export_data(count_df: pl.DataFrame): def main(): args = parse_args() count_files = [Path(file) for file in args.count_files.split(" ")] + logger.info(f"Merging {len(count_files)} count files") # putting all counts into a single dataframe count_df = get_counts(count_files) diff --git a/modules/local/merge/counts/main.nf b/modules/local/merge/counts/main.nf index 335f7644..2f9f3da5 100644 --- a/modules/local/merge/counts/main.nf +++ b/modules/local/merge/counts/main.nf @@ -1,6 +1,6 @@ process MERGE_COUNTS { - memory { def calc = (dataset_size / 5000).toInteger() + memory { def calc = (dataset_size / 10000).toInteger() def result = Math.max(1, calc) // Ensure at least 1 MB def multiplicator = 1 + 0.2 * task.attempt // increase memory usage with each attempt by 20% return 1.MB * result * multiplicator From 79fcbb5ed48a91b7233ca1c87cf37bf0658b7ba9 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sun, 26 Oct 2025 20:11:15 +0100 Subject: [PATCH 092/258] add filter on non empty datasets when computing dataset size --- bin/clean_count_data.py | 15 ++++++++++----- .../main.nf | 3 +++ workflows/stableexpression.nf | 10 +++++----- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/bin/clean_count_data.py b/bin/clean_count_data.py index 4be6e463..d138999d 100755 --- a/bin/clean_count_data.py +++ b/bin/clean_count_data.py @@ -99,7 +99,9 @@ def get_count_columns(lf: pl.LazyFrame) -> list[str]: The config.ENSEMBL_GENE_ID_COLNAME column contains only gene IDs. """ - return lf.select(pl.exclude(config.ENSEMBL_GENE_ID_COLNAME)).collect_schema().names() + return ( + lf.select(pl.exclude(config.ENSEMBL_GENE_ID_COLNAME)).collect_schema().names() + ) def get_counts( @@ -112,8 +114,9 @@ def get_counts( def remove_samples_with_low_ks_pvalue( count_lf: pl.LazyFrame, ks_stats_file: Path, ks_pvalue_threshold: str ) -> pl.LazyFrame: - - ks_stats_df = pl.read_csv(ks_stats_file, has_header=True).select([config.SAMPLE_COLNAME, config.KS_TEST_COLNAME]) + ks_stats_df = pl.read_csv(ks_stats_file, has_header=True).select( + [config.SAMPLE_COLNAME, config.KS_TEST_COLNAME] + ) # parsing threshold try: @@ -148,7 +151,7 @@ def remove_samples_with_low_ks_pvalue( return count_lf.select([config.ENSEMBL_GENE_ID_COLNAME] + valid_samples) -def export_data( all_counts_lf: pl.LazyFrame): +def export_data(all_counts_lf: pl.LazyFrame): all_counts_lf.collect().write_parquet(ALL_COUNTS_FILTERED_PARQUET_OUTFILENAME) logger.info("Done") @@ -167,7 +170,9 @@ def main(): count_lf = get_counts(args.count_file) # removing aberrant samples (ks p-value under the threshold) - count_lf = remove_samples_with_low_ks_pvalue(count_lf, args.ks_stats_file, args.ks_pvalue_threshold) + count_lf = remove_samples_with_low_ks_pvalue( + count_lf, args.ks_stats_file, args.ks_pvalue_threshold + ) # exporting computed data export_data(count_lf) diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index c4f5fccf..a200138d 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -397,6 +397,9 @@ def storeDatasetSize( ch_counts, nb_genes_key, nb_samples_key ) { def getWholeDatasetSize( ch_counts ) { return ch_counts + .filter { meta, file -> + meta.nb_genes_final > 0 && meta.nb_samples_final > 0 + } .map { meta, file -> [ meta.nb_genes_final * meta.nb_samples_final ] } diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 5188be0b..6911f36f 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -88,10 +88,10 @@ workflow STABLEEXPRESSION { GPROFILER_IDMAPPING.out.mapping.set { ch_gene_id_mapping } GPROFILER_IDMAPPING.out.metadata.set { ch_gene_metadata } - ch_counts = storeDatasetSize( ch_counts, "nb_genes_after_idmapping", "nb_samples_after_idmapping" ) - } + ch_counts = storeDatasetSize( ch_counts, "nb_genes_after_idmapping", "nb_samples_after_idmapping" ) + // ----------------------------------------------------------------- // NORMALISATION OF RAW COUNT DATASETS (INCLUDING RNA-SEQ DATASETS) // ----------------------------------------------------------------- @@ -112,14 +112,14 @@ workflow STABLEEXPRESSION { params.ks_pvalue_threshold ) - ch_counts = storeDatasetSize( ch_counts, "nb_genes_final", "nb_samples_final" ) + ch_counts = storeDatasetSize( DATA_CLEANSING.out.cleaned_counts, "nb_genes_final", "nb_samples_final" ) // ----------------------------------------------------------------- // MERGE DATA // ----------------------------------------------------------------- MERGE_DATA ( - DATA_CLEANSING.out.cleaned_counts , + ch_counts, ch_gene_id_mapping, ch_gene_metadata ) @@ -148,7 +148,7 @@ workflow STABLEEXPRESSION { ) STABILITY_SCORING.out.summary_statistics.set { ch_candidate_gene_stats_with_scores } - //ch_candidate_gene_stats_with_scores.splitCsv(header: true).view() + // ----------------------------------------------------------------- // AGGREGATE ALL RESULTS FOR MULTIQC // ----------------------------------------------------------------- From 4ad3f250a0f7fb4049fb42d198582f8188f26898 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sun, 26 Oct 2025 21:30:29 +0100 Subject: [PATCH 093/258] fix big bug with workflow.containerEngine --- modules/local/aggregate_results/main.nf | 2 +- modules/local/clean_count_data/main.nf | 2 +- modules/local/compute_base_statistics/main.nf | 2 +- .../local/compute_stability_scores/main.nf | 2 +- modules/local/dash_app/main.nf | 2 +- modules/local/dataset_statistics/main.nf | 2 +- .../expressionatlas/getaccessions/main.nf | 2 +- modules/local/expressionatlas/getdata/main.nf | 2 +- .../local/genorm/compute_m_measure/main.nf | 2 +- modules/local/genorm/cross_join/main.nf | 2 +- modules/local/genorm/expression_ratio/main.nf | 2 +- modules/local/genorm/make_chunks/main.nf | 2 +- .../genorm/ratio_standard_variation/main.nf | 2 +- modules/local/geo/getaccessions/main.nf | 2 +- modules/local/geo/getdata/main.nf | 2 +- modules/local/get_candidate_genes/main.nf | 2 +- modules/local/gprofiler/idmapping/main.nf | 2 +- modules/local/merge/counts/main.nf | 8 +++--- modules/local/merge/counts/spec-file.txt | 28 ++++++++++--------- modules/local/merge/designs/main.nf | 2 +- modules/local/normalisation/deseq2/main.nf | 2 +- modules/local/normalisation/edger/main.nf | 2 +- modules/local/normfinder/main.nf | 2 +- modules/local/quantile_normalisation/main.nf | 2 +- modules/nf-core/multiqc/main.nf | 2 +- 25 files changed, 42 insertions(+), 40 deletions(-) diff --git a/modules/local/aggregate_results/main.nf b/modules/local/aggregate_results/main.nf index 95565cfa..10e2d86e 100644 --- a/modules/local/aggregate_results/main.nf +++ b/modules/local/aggregate_results/main.nf @@ -3,7 +3,7 @@ process AGGREGATE_RESULTS { label 'process_single' conda "${moduleDir}/spec-file.txt" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" diff --git a/modules/local/clean_count_data/main.nf b/modules/local/clean_count_data/main.nf index d99a474b..59c454fd 100644 --- a/modules/local/clean_count_data/main.nf +++ b/modules/local/clean_count_data/main.nf @@ -16,7 +16,7 @@ process CLEAN_COUNT_DATA { } conda "${moduleDir}/spec-file.txt" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" diff --git a/modules/local/compute_base_statistics/main.nf b/modules/local/compute_base_statistics/main.nf index 91517e11..e9fd5ba9 100644 --- a/modules/local/compute_base_statistics/main.nf +++ b/modules/local/compute_base_statistics/main.nf @@ -13,7 +13,7 @@ process COMPUTE_BASE_STATISTICS { } conda "${moduleDir}/spec-file.txt" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" diff --git a/modules/local/compute_stability_scores/main.nf b/modules/local/compute_stability_scores/main.nf index a6fad34c..2e2a009f 100644 --- a/modules/local/compute_stability_scores/main.nf +++ b/modules/local/compute_stability_scores/main.nf @@ -3,7 +3,7 @@ process COMPUTE_STABILITY_SCORES { label 'process_single' conda "${moduleDir}/spec-file.txt" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/01/0118e0577564644b18f94fa6525fe3a2aec845721081b55d82a18e803a50ab17/data': 'community.wave.seqera.io/library/polars_scikit-learn:036e189d7c1f9704' }" diff --git a/modules/local/dash_app/main.nf b/modules/local/dash_app/main.nf index be861623..dc09a3de 100644 --- a/modules/local/dash_app/main.nf +++ b/modules/local/dash_app/main.nf @@ -3,7 +3,7 @@ process DASH_APP { label 'process_single' conda "${moduleDir}/app/spec-file.txt" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/b3/b39ecd56e298b0ba94bed41bb36d67b0a2bc24634bc53baff9773dcc3d422c01/data': 'community.wave.seqera.io/library/dash-ag-grid_dash-extensions_dash-iconify_dash-mantine-components_pruned:138d9ff01702db68' }" diff --git a/modules/local/dataset_statistics/main.nf b/modules/local/dataset_statistics/main.nf index e985e18f..e9747155 100644 --- a/modules/local/dataset_statistics/main.nf +++ b/modules/local/dataset_statistics/main.nf @@ -5,7 +5,7 @@ process DATASET_STATISTICS { tag "${meta.dataset}" conda "${moduleDir}/spec-file.txt" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/5f/5fe497e7a739fa611fedd6f72ab9a3cf925873a5ded3188161fc85fd376b2c1c/data': 'community.wave.seqera.io/library/pandas_pyarrow_python_scipy:7cad0d297a717147' }" diff --git a/modules/local/expressionatlas/getaccessions/main.nf b/modules/local/expressionatlas/getaccessions/main.nf index 97d2c5a6..10941a81 100644 --- a/modules/local/expressionatlas/getaccessions/main.nf +++ b/modules/local/expressionatlas/getaccessions/main.nf @@ -3,7 +3,7 @@ process EXPRESSIONATLAS_GETACCESSIONS { label 'process_medium' conda "${moduleDir}/spec-file.txt" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/f2/f2219a174683388670dc0817da45717014aca444323027480f84aaaf12bfb460/data': 'community.wave.seqera.io/library/nltk_data_pandas_pyyaml_requests_tenacity:5f5f82f858433879' }" diff --git a/modules/local/expressionatlas/getdata/main.nf b/modules/local/expressionatlas/getdata/main.nf index a48377dd..0ce1db0c 100644 --- a/modules/local/expressionatlas/getdata/main.nf +++ b/modules/local/expressionatlas/getdata/main.nf @@ -40,7 +40,7 @@ process EXPRESSIONATLAS_GETDATA { } conda "${moduleDir}/spec-file.txt" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/7f/7fd21450c3a3f7df37fa0480170780019e9686be319da1c9e10712f7f17cca26/data': 'community.wave.seqera.io/library/bioconductor-expressionatlas_r-base_r-optparse:ca0f8cd9d3f44af9' }" diff --git a/modules/local/genorm/compute_m_measure/main.nf b/modules/local/genorm/compute_m_measure/main.nf index 7e4009fb..c0d6c264 100644 --- a/modules/local/genorm/compute_m_measure/main.nf +++ b/modules/local/genorm/compute_m_measure/main.nf @@ -4,7 +4,7 @@ process COMPUTE_M_MEASURE { publishDir "${params.outdir}/genorm/m_measures" conda "${moduleDir}/environment.yml" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" diff --git a/modules/local/genorm/cross_join/main.nf b/modules/local/genorm/cross_join/main.nf index 84307eba..7d927822 100644 --- a/modules/local/genorm/cross_join/main.nf +++ b/modules/local/genorm/cross_join/main.nf @@ -4,7 +4,7 @@ process CROSS_JOIN { publishDir "${params.outdir}/genorm/cross_joins" conda "${moduleDir}/environment.yml" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" diff --git a/modules/local/genorm/expression_ratio/main.nf b/modules/local/genorm/expression_ratio/main.nf index ed017326..da9bb1f1 100644 --- a/modules/local/genorm/expression_ratio/main.nf +++ b/modules/local/genorm/expression_ratio/main.nf @@ -4,7 +4,7 @@ process EXPRESSION_RATIO { publishDir "${params.outdir}/genorm/expression_ratios" conda "${moduleDir}/environment.yml" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" diff --git a/modules/local/genorm/make_chunks/main.nf b/modules/local/genorm/make_chunks/main.nf index 0dede9dd..8cfca9b6 100644 --- a/modules/local/genorm/make_chunks/main.nf +++ b/modules/local/genorm/make_chunks/main.nf @@ -4,7 +4,7 @@ process MAKE_CHUNKS { publishDir "${params.outdir}/genorm/chunks" conda "${moduleDir}/environment.yml" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" diff --git a/modules/local/genorm/ratio_standard_variation/main.nf b/modules/local/genorm/ratio_standard_variation/main.nf index 5792d8c0..7e2b9de2 100644 --- a/modules/local/genorm/ratio_standard_variation/main.nf +++ b/modules/local/genorm/ratio_standard_variation/main.nf @@ -4,7 +4,7 @@ process RATIO_STANDARD_VARIATION { publishDir "${params.outdir}/genorm/ratio_standard_variations" conda "${moduleDir}/environment.yml" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" diff --git a/modules/local/geo/getaccessions/main.nf b/modules/local/geo/getaccessions/main.nf index 5c85ba41..3344c712 100644 --- a/modules/local/geo/getaccessions/main.nf +++ b/modules/local/geo/getaccessions/main.nf @@ -3,7 +3,7 @@ process GEO_GETACCESSIONS { label 'process_high_cpus' conda "${moduleDir}/spec-file.txt" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/ca/caae35ec5dc72367102a616a47b6f1a7b3de9ff272422f2c08895b8bb5f0566c/data': 'community.wave.seqera.io/library/biopython_nltk_pandas_parallelbar_pruned:5fc501b07f8e0428' }" diff --git a/modules/local/geo/getdata/main.nf b/modules/local/geo/getdata/main.nf index ef7f0b7e..686115cb 100644 --- a/modules/local/geo/getdata/main.nf +++ b/modules/local/geo/getdata/main.nf @@ -39,7 +39,7 @@ process GEO_GETDATA { } conda "${moduleDir}/spec-file.txt" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/4c/4cb08d96e62942e7b6288abf2cfd30e813521a022459700e610325a3a7c0b1c8/data': 'community.wave.seqera.io/library/bioconductor-geoquery_r-base_r-dplyr_r-optparse:fcd002470b7d6809' }" diff --git a/modules/local/get_candidate_genes/main.nf b/modules/local/get_candidate_genes/main.nf index 84117ea8..fa657ed3 100644 --- a/modules/local/get_candidate_genes/main.nf +++ b/modules/local/get_candidate_genes/main.nf @@ -3,7 +3,7 @@ process GET_CANDIDATE_GENES { label 'process_single' conda "${moduleDir}/spec-file.txt" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" diff --git a/modules/local/gprofiler/idmapping/main.nf b/modules/local/gprofiler/idmapping/main.nf index c195aeff..237d64e3 100644 --- a/modules/local/gprofiler/idmapping/main.nf +++ b/modules/local/gprofiler/idmapping/main.nf @@ -26,7 +26,7 @@ process GPROFILER_IDMAPPING { } conda "${moduleDir}/spec-file.txt" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/5c/5c28c8e613c062828aaee4b950029bc90a1a1aa94d5f61016a588c8ec7be8b65/data': 'community.wave.seqera.io/library/pandas_requests_tenacity:5ba56df089a9d718' }" diff --git a/modules/local/merge/counts/main.nf b/modules/local/merge/counts/main.nf index 2f9f3da5..4844b440 100644 --- a/modules/local/merge/counts/main.nf +++ b/modules/local/merge/counts/main.nf @@ -7,9 +7,9 @@ process MERGE_COUNTS { } conda "${moduleDir}/spec-file.txt" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': - 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/90/90617e987f709570820b8e7752baf9004ba85917111425d4b44b429b27b201ca/data': + 'community.wave.seqera.io/library/polars_tqdm:54b124dde91d1bf3' }" input: path count_files, stageAs: "?/*" @@ -19,9 +19,9 @@ process MERGE_COUNTS { path 'all_counts.parquet', emit: counts tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions + tuple val("${task.process}"), val('tqdm'), eval('python3 -c "import tqdm; print(tqdm.__version__)"'), topic: versions script: - println task.memory """ merge_counts.py \\ --counts "$count_files" diff --git a/modules/local/merge/counts/spec-file.txt b/modules/local/merge/counts/spec-file.txt index 1bf5d691..79747f60 100644 --- a/modules/local/merge/counts/spec-file.txt +++ b/modules/local/merge/counts/spec-file.txt @@ -5,36 +5,38 @@ https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda#3cd1a7238a0dd3d0860fdefc496cc854 https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda#9e60c55e725c20d23125a5f0dd69af5d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda#e66f2b8ad787e7beb0f846e4bd7e8493 https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62ee74e96c5ebb0af99386de58cf9553 https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.6.15-hbd8a1cb_0.conda#72525f07d72806e3b639ad4504c30ce5 +https://conda.anaconda.org/conda-forge/linux-64/python-3.12.8-h9e4cc4f_1_cpython.conda#7fd2fd79436d9b473812f14e86746844 +https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda#962b9857ee8e7018c22f2776ffa0b2d7 https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_0.conda#e31316a586cac398b1fcdb10ace786b9 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda#530566b68c3b8ce7eec4cd047eae19fe -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda#bfbca721fd33188ef923dfe9ba172f29 -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda#323dc8f259224d13078aaf7ce96c3efe https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-32_h59b9bed_openblas.conda#2af9f3d5c2e39f417ce040f5a35c40c6 https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-32_he106b2a_openblas.conda#3d3f9355e52f269cd8bc2c440d8a5263 https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda#db0bfbe7dd197b68ad5f30333bae6ce0 +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda#9e60c55e725c20d23125a5f0dd69af5d https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda#e66f2b8ad787e7beb0f846e4bd7e8493 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda#bfbca721fd33188ef923dfe9ba172f29 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda#530566b68c3b8ce7eec4cd047eae19fe https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-32_h7ac8fdf_openblas.conda#6c3f04ccb6c578138e9f9899da0bd714 https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda#d864d34357c3b65a4b731f78c0801dc4 -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda#323dc8f259224d13078aaf7ce96c3efe https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.2-h6cd9bfd_0.conda#b04c7eda6d7dab1e6503135e7fad4d25 https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda#6d11a5edae89fe413c0569f16d308f5a https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 +https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.1-py312h6cf2f7f_0.conda#7e086a30150af2536a1059885368dcf0 https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda#c87df2ab1448ba69169652ab9547082d +https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda#58335b26c38bf4a20f399384c33cbcf9 +https://conda.anaconda.org/conda-forge/noarch/pip-25.1.1-pyh8b19718_0.conda#32d0781ace05105cc99af55d36cbec7c +https://conda.anaconda.org/conda-forge/linux-64/polars-1.17.1-py312hda0fa55_1.conda#d9d77bfc286b6044dc045d1696c6acdc +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda#0dfcdc155cf23812a0c9deada86fb723 https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 +https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 +https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda#9efbfdc37242619130ea42b1cc4ed861 https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.12.8-h9e4cc4f_1_cpython.conda#7fd2fd79436d9b473812f14e86746844 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda#0dfcdc155cf23812a0c9deada86fb723 -https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.1-py312h6cf2f7f_0.conda#7e086a30150af2536a1059885368dcf0 -https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda#58335b26c38bf4a20f399384c33cbcf9 -https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e https://conda.anaconda.org/conda-forge/noarch/wheel-0.45.1-pyhd8ed1ab_1.conda#75cb7132eb58d97896e173ef12ac9986 -https://conda.anaconda.org/conda-forge/noarch/pip-25.1.1-pyh8b19718_0.conda#32d0781ace05105cc99af55d36cbec7c -https://conda.anaconda.org/conda-forge/linux-64/polars-1.17.1-py312hda0fa55_1.conda#d9d77bfc286b6044dc045d1696c6acdc diff --git a/modules/local/merge/designs/main.nf b/modules/local/merge/designs/main.nf index be55ff5b..eec246e3 100644 --- a/modules/local/merge/designs/main.nf +++ b/modules/local/merge/designs/main.nf @@ -3,7 +3,7 @@ process MERGE_DESIGNS { label 'process_high' conda "${moduleDir}/spec-file.txt" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/f1/f1c30725ef181337de8749d5b54eacb1a8e1f97ac5e43fe15ec34a61789a7320/data': 'community.wave.seqera.io/library/pandas:2.3.2--baef3004955c4a32' }" diff --git a/modules/local/normalisation/deseq2/main.nf b/modules/local/normalisation/deseq2/main.nf index b26c29fc..f792b91b 100644 --- a/modules/local/normalisation/deseq2/main.nf +++ b/modules/local/normalisation/deseq2/main.nf @@ -9,7 +9,7 @@ process NORMALISATION_DESEQ2 { errorStrategy { task.exitStatus == 100 ? 'ignore' : 'terminate' } conda "${moduleDir}/spec-file.txt" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/ce/cef7164b168e74e5db11dcd9acf6172d47ed6753e4814c68f39835d0c6c22f6d/data': 'community.wave.seqera.io/library/bioconductor-deseq2_r-base_r-optparse:c84cd7ffdb298fa7' }" diff --git a/modules/local/normalisation/edger/main.nf b/modules/local/normalisation/edger/main.nf index 8a8ad4a8..da1405ff 100644 --- a/modules/local/normalisation/edger/main.nf +++ b/modules/local/normalisation/edger/main.nf @@ -9,7 +9,7 @@ process NORMALISATION_EDGER { errorStrategy { task.exitStatus == 100 ? 'ignore' : 'terminate' } conda "${moduleDir}/spec-file.txt" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/89/89bbc9544e18b624ed6d0a30e701cf8cec63e063cc9b5243e1efde362fe92228/data': 'community.wave.seqera.io/library/bioconductor-edger_r-base_r-optparse:400aaabddeea1574' }" diff --git a/modules/local/normfinder/main.nf b/modules/local/normfinder/main.nf index 7f8c6020..0ef1828e 100644 --- a/modules/local/normfinder/main.nf +++ b/modules/local/normfinder/main.nf @@ -3,7 +3,7 @@ process NORMFINDER { label 'process_high' conda "${moduleDir}/spec-file.txt" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0e/0e0445114887dd260f1632afe116b1e81e02e1acc74a86adca55099469b490d9/data': 'community.wave.seqera.io/library/numba_numpy_polars_tqdm:6923cfab6fc04dec' }" diff --git a/modules/local/quantile_normalisation/main.nf b/modules/local/quantile_normalisation/main.nf index 108b2cc7..c28a21fd 100644 --- a/modules/local/quantile_normalisation/main.nf +++ b/modules/local/quantile_normalisation/main.nf @@ -5,7 +5,7 @@ process QUANTILE_NORMALISATION { tag "${meta.dataset}" conda "${moduleDir}/spec-file.txt" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/2d/2df931a4ea181fe1ea9527abe0fd4aff9453d6ea56d56aee7c4ac5dceed611e3/data': 'community.wave.seqera.io/library/pandas_pyarrow_python_scikit-learn:6f85e3c4d1706e81' }" diff --git a/modules/nf-core/multiqc/main.nf b/modules/nf-core/multiqc/main.nf index 5288f5cc..03e75c57 100644 --- a/modules/nf-core/multiqc/main.nf +++ b/modules/nf-core/multiqc/main.nf @@ -2,7 +2,7 @@ process MULTIQC { label 'process_single' conda "${moduleDir}/environment.yml" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/ef/eff0eafe78d5f3b65a6639265a16b89fdca88d06d18894f90fcdb50142004329/data' : 'community.wave.seqera.io/library/multiqc:1.31--1efbafd542a23882' }" From 5ff13af3a60408153b610f84373f4d63e69e4f2b Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sun, 26 Oct 2025 22:15:17 +0100 Subject: [PATCH 094/258] upgraded synthax to better adapt to strict synthax --- modules/local/compute_base_statistics/main.nf | 5 +--- .../expressionatlas/getaccessions/main.nf | 2 +- modules/local/expressionatlas/getdata/main.nf | 2 +- modules/local/geo/getaccessions/main.nf | 2 +- modules/local/geo/getdata/main.nf | 2 +- .../local/expressionatlas_fetchdata/main.nf | 8 +++--- subworkflows/local/genorm/main.nf | 28 +++++++++---------- subworkflows/local/geo_fetchdata/main.nf | 8 +++--- subworkflows/local/merge_data/main.nf | 2 +- .../main.nf | 12 ++++---- 10 files changed, 32 insertions(+), 39 deletions(-) diff --git a/modules/local/compute_base_statistics/main.nf b/modules/local/compute_base_statistics/main.nf index e9fd5ba9..7484622c 100644 --- a/modules/local/compute_base_statistics/main.nf +++ b/modules/local/compute_base_statistics/main.nf @@ -4,10 +4,7 @@ process COMPUTE_BASE_STATISTICS { errorStrategy { if (task.exitStatus == 100) { - log.error( - "No count could be found before merging datasets! " - + "Please check the provided accessions and datasets and run again" - ) + log.error("No count could be found before merging datasets! Please check the provided accessions and datasets and run again") return 'terminate' } } diff --git a/modules/local/expressionatlas/getaccessions/main.nf b/modules/local/expressionatlas/getaccessions/main.nf index 10941a81..4fae8a79 100644 --- a/modules/local/expressionatlas/getaccessions/main.nf +++ b/modules/local/expressionatlas/getaccessions/main.nf @@ -35,7 +35,7 @@ process EXPRESSIONATLAS_GETACCESSIONS { } // the folder where nltk will download data needs to be writable (necessary for singularity) """ - NLTK_DATA=$PWD get_eatlas_accessions.py $args + NLTK_DATA=${task.workDir} get_eatlas_accessions.py $args """ stub: diff --git a/modules/local/expressionatlas/getdata/main.nf b/modules/local/expressionatlas/getdata/main.nf index 0ce1db0c..6c5101f1 100644 --- a/modules/local/expressionatlas/getdata/main.nf +++ b/modules/local/expressionatlas/getdata/main.nf @@ -27,7 +27,7 @@ process EXPRESSIONATLAS_GETDATA { return 'ignore' } else if (task.exitStatus == 137) { // override default behaviour to sleep some time before retry // in case of OOM errors, we wait a bit and try again (2 retries) - if ( task.attempt in <= 2) { + if ( task.attempt <= 2) { sleep(Math.pow(2, task.attempt) * 2000 as long) return 'retry' } else { diff --git a/modules/local/geo/getaccessions/main.nf b/modules/local/geo/getaccessions/main.nf index 3344c712..c43d7b30 100644 --- a/modules/local/geo/getaccessions/main.nf +++ b/modules/local/geo/getaccessions/main.nf @@ -48,7 +48,7 @@ process GEO_GETACCESSIONS { export HOME=/tmp/biopython mkdir -p /tmp/biopython - export NLTK_DATA=$PWD + export NLTK_DATA=${task.workDir} get_geo_dataset_accessions.py \\ $args \\ diff --git a/modules/local/geo/getdata/main.nf b/modules/local/geo/getdata/main.nf index 686115cb..d6e5173b 100644 --- a/modules/local/geo/getdata/main.nf +++ b/modules/local/geo/getdata/main.nf @@ -26,7 +26,7 @@ process GEO_GETDATA { return 'ignore' } else if (task.exitStatus == 137) { // override default behaviour to sleep some time before retry // in case of OOM errors, we wait a bit and try again (2 retries) - if ( task.attempt in <= 2) { + if ( task.attempt <= 2) { sleep(Math.pow(2, task.attempt) * 2000 as long) return 'retry' } else { diff --git a/subworkflows/local/expressionatlas_fetchdata/main.nf b/subworkflows/local/expressionatlas_fetchdata/main.nf index 1a1a0ee0..2c0e04d3 100644 --- a/subworkflows/local/expressionatlas_fetchdata/main.nf +++ b/subworkflows/local/expressionatlas_fetchdata/main.nf @@ -27,7 +27,7 @@ workflow EXPRESSIONATLAS_FETCHDATA { Channel.fromList( params.eatlas_accessions.tokenize(',') ) .mix( ch_eatlas_accessions_file.splitText() ) .unique() - .map { it -> it.trim() } + .map { acc -> acc.trim() } .set { ch_input_accessions } // fetching Expression Atlas accessions if applicable @@ -54,7 +54,7 @@ workflow EXPRESSIONATLAS_FETCHDATA { Channel.fromList( params.exclude_eatlas_accessions.tokenize(',') ) .mix( ch_exclude_eatlas_accessions_file.splitText() ) .unique() - .map { it -> it.trim() } + .map { acc -> acc.trim() } .toList() .map { lst -> [lst] } // list of lists : mandatory when combining in the next step .set { ch_excluded_accessions } @@ -66,8 +66,8 @@ workflow EXPRESSIONATLAS_FETCHDATA { ch_input_accessions .mix( ch_fetched_accessions ) .unique() - .map { it -> it.trim() } - .filter { it.startsWith('E-') && !it.startsWith('E-PROT-') } + .map { acc -> acc.trim() } + .filter { acc -> acc.startsWith('E-') && !acc.startsWith('E-PROT-') } .combine ( ch_excluded_accessions ) .filter { accession, excluded_accessions -> !(accession in excluded_accessions) } .map { accession, excluded_accessions -> accession } diff --git a/subworkflows/local/genorm/main.nf b/subworkflows/local/genorm/main.nf index c83da3a5..340184c2 100644 --- a/subworkflows/local/genorm/main.nf +++ b/subworkflows/local/genorm/main.nf @@ -62,21 +62,19 @@ workflow GENORM { // def getUniqueFilePairs( ch_count_chunks ) { - ch_count_chunks_with_indexes = ch_count_chunks - .map { file -> [file.name.tokenize('.')[1], file] } // extract file index + def ch_count_chunks_with_indexes = ch_count_chunks + .map { file -> [file.name.tokenize('.')[1], file] } // extract file index return ch_count_chunks_with_indexes - .combine( ch_count_chunks_with_indexes ) // full cartesian product with itself - .map { // steps not mandatory but helps to make the filter clearer - index_1, file_1, index_2, file_2 -> - [index_1: index_1, index_2: index_2, file_1: file_1, file_2: file_2] - } - .filter { it -> it.index_1 <= it.index_2 } // keeps only pairs where i <= j - .map { - it -> - def meta = [index_1: it.index_1, index_2: it.index_2] // puts indexes in a meta tuple - [ meta, it.file_1, it.file_2 ] - } + .combine( ch_count_chunks_with_indexes ) // full cartesian product with itself + .map { // steps not mandatory but helps to make the filter clearer + index_1, file_1, index_2, file_2 -> + [index_1: index_1, index_2: index_2, file_1: file_1, file_2: file_2] + } + .filter { it -> it.index_1 <= it.index_2 } // keeps only pairs where i <= j + .map { + it -> + def meta = [index_1: it.index_1, index_2: it.index_2] // puts indexes in a meta tuple + [ meta, it.file_1, it.file_2 ] + } } - - diff --git a/subworkflows/local/geo_fetchdata/main.nf b/subworkflows/local/geo_fetchdata/main.nf index 09820a57..31ee4e32 100644 --- a/subworkflows/local/geo_fetchdata/main.nf +++ b/subworkflows/local/geo_fetchdata/main.nf @@ -26,7 +26,7 @@ workflow GEO_FETCHDATA { Channel.fromList( params.geo_accessions.tokenize(',') ) .mix( ch_geo_accessions_file.splitText() ) .unique() - .map { it -> it.trim() } + .map { acc -> acc.trim() } .set { ch_input_accessions } // fetching GEO accessions if applicable @@ -64,7 +64,7 @@ workflow GEO_FETCHDATA { Channel.fromList( params.exclude_geo_accessions.tokenize(',') ) .mix( ch_exclude_geo_accessions_file.splitText() ) .unique() - .map { it -> it.trim() } + .map { acc -> acc.trim() } .toList() .map { lst -> [lst] } // list of lists : mandatory when combining in the next step .set { ch_excluded_accessions } @@ -75,8 +75,8 @@ workflow GEO_FETCHDATA { ch_input_accessions .mix( ch_fetched_accessions ) .unique() - .map { it -> it.trim() } - .filter { it.startsWith('GSE') } + .map { acc -> acc.trim() } + .filter { acc -> acc.startsWith('GSE') } .combine ( ch_excluded_accessions ) .filter { accession, excluded_accessions -> !(accession in excluded_accessions) } .map { accession, excluded_accessions -> accession } diff --git a/subworkflows/local/merge_data/main.nf b/subworkflows/local/merge_data/main.nf index 51e5d138..c2dd3265 100644 --- a/subworkflows/local/merge_data/main.nf +++ b/subworkflows/local/merge_data/main.nf @@ -74,7 +74,7 @@ workflow MERGE_DATA { ch_normalised_counts .map { - meta, _ -> // extracts design file and adds batch column whenever missing (for custom datasets) + meta, file -> // extracts design file and adds batch column whenever missing (for custom datasets) def design_content = meta.design.splitCsv( header: true ) // if there is no batch, it is custom data def updated_design_content = design_content.collect { row -> diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index a200138d..34c0b557 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -2,8 +2,6 @@ // Subworkflow with functionality specific to the nf-core/stableexpression pipeline // -import org.yaml.snakeyaml.Yaml - /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ IMPORT FUNCTIONS / MODULES / SUBWORKFLOWS @@ -175,7 +173,7 @@ def validateInputParameters(params) { // if expression atlas accessions are provided, checking that they are well formated if ( params.eatlas_accessions ) { - for ( accession in params.eatlas_accessions.tokenize(',') ) { + params.eatlas_accessions.tokenize(',').each { accession -> if ( !accession.startsWith('E-') ) { error('Expression Atlas accession ' + accession + ' is not well formated. All accessions should start with "E-".') } @@ -196,7 +194,7 @@ def parseInputDatasets(samplesheet) { .map { item -> def (meta, count_file) = item - new_meta = meta + [dataset: count_file.getBaseName()] + def new_meta = meta + [dataset: count_file.getBaseName()] [new_meta, count_file] } } @@ -293,8 +291,8 @@ def methodsDescriptionText(mqc_methods_yaml) { // temporary replacements of the native processVersionsFromYAML // def customProcessVersionsFromYAML(yaml_file) { - Yaml yaml = new Yaml() - versions = yaml.load(yaml_file) + def yaml = new org.yaml.snakeyaml.Yaml() + def versions = yaml.load(yaml_file) return yaml.dumpAsMap(versions).trim() } @@ -388,7 +386,7 @@ def storeDatasetSize( ch_counts, nb_genes_key, nb_samples_key ) { // adding nb genes and nb samples in the meta map under keys provided as parameters return ch_counts .map { meta, file -> - def content = file.splitCsv( header: true ) + def content = file.splitCsv( header: true ) meta[nb_genes_key] = content.size() meta[nb_samples_key] = content[0].findAll {it.key != 'ensembl_gene_id'}.size() [ meta, file ] From 7a006dfd7718c1222384b91b22b44f211074243d Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sun, 26 Oct 2025 22:46:30 +0100 Subject: [PATCH 095/258] little fixes --- bin/merge_counts.py | 4 ++-- .../local/utils_nfcore_stableexpression_pipeline/main.nf | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bin/merge_counts.py b/bin/merge_counts.py index 1d1953a8..5917e6bd 100755 --- a/bin/merge_counts.py +++ b/bin/merge_counts.py @@ -62,7 +62,7 @@ def is_valid_df(df: pl.DataFrame, file: Path) -> bool: return False -def get_valid_lazy_dfs(files: list[Path]) -> list[pl.DataFrame]: +def get_valid_dfs(files: list[Path]) -> list[pl.DataFrame]: """Get a list of valid DataFrames from a list of files. A DataFrame is considered valid if it contains at least one row. """ @@ -95,7 +95,7 @@ def get_counts(files: list[Path]) -> pl.DataFrame: to String, and all other columns are cast to Float64. """ logger.info("Parsing counts") - dfs = get_valid_lazy_dfs(files) + dfs = get_valid_dfs(files) # joining all count files logger.info( diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index 34c0b557..2c801e9d 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -385,11 +385,11 @@ def augmentToMetadata( ch_files ) { def storeDatasetSize( ch_counts, nb_genes_key, nb_samples_key ) { // adding nb genes and nb samples in the meta map under keys provided as parameters return ch_counts - .map { meta, file -> - def content = file.splitCsv( header: true ) + .map { meta, count_file -> + def content = count_file.splitCsv( header: true ) meta[nb_genes_key] = content.size() meta[nb_samples_key] = content[0].findAll {it.key != 'ensembl_gene_id'}.size() - [ meta, file ] + [ meta, count_file ] } } From e9ff95edbe6bceae05daac7f98beb1df03ec4373 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sun, 26 Oct 2025 23:22:04 +0100 Subject: [PATCH 096/258] made some cleaning --- modules/local/expressionatlas/getaccessions/main.nf | 2 +- modules/local/geo/getaccessions/main.nf | 2 +- subworkflows/local/data_cleansing/main.nf | 4 ---- .../local/utils_nfcore_stableexpression_pipeline/main.nf | 4 ++-- workflows/stableexpression.nf | 4 +--- 5 files changed, 5 insertions(+), 11 deletions(-) diff --git a/modules/local/expressionatlas/getaccessions/main.nf b/modules/local/expressionatlas/getaccessions/main.nf index 4fae8a79..e44d2c53 100644 --- a/modules/local/expressionatlas/getaccessions/main.nf +++ b/modules/local/expressionatlas/getaccessions/main.nf @@ -35,7 +35,7 @@ process EXPRESSIONATLAS_GETACCESSIONS { } // the folder where nltk will download data needs to be writable (necessary for singularity) """ - NLTK_DATA=${task.workDir} get_eatlas_accessions.py $args + NLTK_DATA=\${PWD} get_eatlas_accessions.py $args """ stub: diff --git a/modules/local/geo/getaccessions/main.nf b/modules/local/geo/getaccessions/main.nf index c43d7b30..744e5066 100644 --- a/modules/local/geo/getaccessions/main.nf +++ b/modules/local/geo/getaccessions/main.nf @@ -48,7 +48,7 @@ process GEO_GETACCESSIONS { export HOME=/tmp/biopython mkdir -p /tmp/biopython - export NLTK_DATA=${task.workDir} + export NLTK_DATA=\${PWD} get_geo_dataset_accessions.py \\ $args \\ diff --git a/subworkflows/local/data_cleansing/main.nf b/subworkflows/local/data_cleansing/main.nf index 9c1ae1f4..60dc8174 100644 --- a/subworkflows/local/data_cleansing/main.nf +++ b/subworkflows/local/data_cleansing/main.nf @@ -39,7 +39,3 @@ workflow DATA_CLEANSING { cleaned_counts = CLEAN_COUNT_DATA.out.counts } - - - - diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index 2c801e9d..1dc02039 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -396,10 +396,10 @@ def storeDatasetSize( ch_counts, nb_genes_key, nb_samples_key ) { def getWholeDatasetSize( ch_counts ) { return ch_counts .filter { meta, file -> - meta.nb_genes_final > 0 && meta.nb_samples_final > 0 + meta.nb_genes_after_idmapping > 0 && meta.nb_samples_after_idmapping > 0 } .map { meta, file -> - [ meta.nb_genes_final * meta.nb_samples_final ] + [ meta.nb_genes_after_idmapping * meta.nb_samples_after_idmapping ] } .reduce { size_1, size_2 -> size_1 + size_2 } .flatten() diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 6911f36f..ecdd4baa 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -112,14 +112,12 @@ workflow STABLEEXPRESSION { params.ks_pvalue_threshold ) - ch_counts = storeDatasetSize( DATA_CLEANSING.out.cleaned_counts, "nb_genes_final", "nb_samples_final" ) - // ----------------------------------------------------------------- // MERGE DATA // ----------------------------------------------------------------- MERGE_DATA ( - ch_counts, + DATA_CLEANSING.out.cleaned_counts, ch_gene_id_mapping, ch_gene_metadata ) From ce07b870ba9d88a812fbdc47caa12ce10cb5f6cb Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Mon, 27 Oct 2025 08:46:53 +0100 Subject: [PATCH 097/258] fix bug with computation of total dataset size --- conf/test_eatlas_geo.config | 2 +- ... => test_eatlas_only_with_keywords.config} | 2 +- nextflow.config | 1 + .../main.nf | 2 +- tests/default.nf.test | 25 +++++++++++++++++++ 5 files changed, 29 insertions(+), 3 deletions(-) rename conf/{test_eatlas_only.config => test_eatlas_only_with_keywords.config} (96%) diff --git a/conf/test_eatlas_geo.config b/conf/test_eatlas_geo.config index 5d85872f..af0321de 100644 --- a/conf/test_eatlas_geo.config +++ b/conf/test_eatlas_geo.config @@ -16,6 +16,6 @@ params { config_profile_description = 'Minimal test dataset with custom gene metadata to check pipeline function' // Input data - species = 'solanum lycopersicum' + species = 'beta vulgaris' outdir = "results/test_eatlas_geo" } diff --git a/conf/test_eatlas_only.config b/conf/test_eatlas_only_with_keywords.config similarity index 96% rename from conf/test_eatlas_only.config rename to conf/test_eatlas_only_with_keywords.config index 9dd7b19a..29621a26 100644 --- a/conf/test_eatlas_only.config +++ b/conf/test_eatlas_only_with_keywords.config @@ -26,6 +26,6 @@ params { // Input data species = 'solanum tuberosum' keywords = "potato,stress" - eatlas_accessions = "E-MTAB-552" + skip_fetch_geo_accessions = true outdir = "results/test" } diff --git a/nextflow.config b/nextflow.config index 9e878dda..c0ff8bdf 100644 --- a/nextflow.config +++ b/nextflow.config @@ -224,6 +224,7 @@ profiles { } test { includeConfig 'conf/test.config' } + test_eatlas_only_with_keywords { includeConfig 'conf/test_eatlas_only_with_keywords.config' } test_run_normfinder_genorm { includeConfig 'conf/test_run_normfinder_genorm.config' } test_ignore_errors { includeConfig 'conf/test_ignore_errors.config' } test_eatlas_only { includeConfig 'conf/test_eatlas_only.config' } diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index 1dc02039..631c1b9b 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -399,7 +399,7 @@ def getWholeDatasetSize( ch_counts ) { meta.nb_genes_after_idmapping > 0 && meta.nb_samples_after_idmapping > 0 } .map { meta, file -> - [ meta.nb_genes_after_idmapping * meta.nb_samples_after_idmapping ] + meta.nb_genes_after_idmapping * meta.nb_samples_after_idmapping } .reduce { size_1, size_2 -> size_1 + size_2 } .flatten() diff --git a/tests/default.nf.test b/tests/default.nf.test index a5642489..ffe84fb2 100644 --- a/tests/default.nf.test +++ b/tests/default.nf.test @@ -162,4 +162,29 @@ nextflow_pipeline { ) } } + + test("-profile test_eatlas_only_with_keywords") { + + when { + params { + species = 'solanum tuberosum' + keywords = "potato,stress" + skip_fetch_geo_accessions = true + outdir = "$outputDir" + } + } + + then { + def stable_name = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}']) + def stable_path = getAllFilesFromDir(params.outdir, ignoreFile: 'tests/.nftignore') + assertAll( + { assert workflow.success}, + { assert snapshot( + removeNextflowVersion("$outputDir/pipeline_info/nf_core_stableexpression_software_mqc_versions.yml"), + stable_name, + stable_path + ).match() } + ) + } + } } From 7ba5785dbf2b052f6fa74f207edd647423915549 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Mon, 27 Oct 2025 12:55:37 +0100 Subject: [PATCH 098/258] improve ouptut of get_geo_dataset_accessions --- bin/get_geo_dataset_accessions.py | 221 +++++++++++------------- conf/test_eatlas_geo.config | 2 +- modules/local/geo/getaccessions/main.nf | 10 +- 3 files changed, 113 insertions(+), 120 deletions(-) diff --git a/bin/get_geo_dataset_accessions.py b/bin/get_geo_dataset_accessions.py index 87fbfa3d..9e02a040 100755 --- a/bin/get_geo_dataset_accessions.py +++ b/bin/get_geo_dataset_accessions.py @@ -20,7 +20,6 @@ wait_exponential, before_sleep_log, ) -import yaml from functools import partial import logging from requests.exceptions import HTTPError, ConnectionError @@ -37,15 +36,22 @@ ACCESSION_OUTFILE_NAME = "accessions.txt" SPECIES_DATASETS_OUTFILE_NAME = "species_datasets.metadata.tsv" -FILTERED_DATASETS_METADATA_OUTFILE_NAME = "filtered_datasets.metadata.tsv" -REJECTED_DATASETS_METADATA_OUTFILE_NAME = "rejected_datasets.metadata.tsv" -SELECTED_DATASETS_METADATA_OUTFILE_NAME = "selected_datasets.metadata.tsv" +WRONG_SPECIES_DATASETS_METADATA_OUTFILE_NAME = "wrong_species_datasets.metadata.tsv" +WRONG_SPECS_DATASETS_METADATA_OUTFILE_NAME = ( + "wrong_platform_moltype_datasets.metadata.tsv" +) +WRONG_KEYWORDS_DATASETS_METADATA_OUTFILE_NAME = "wrong_keywords_datasets.metadata.tsv" +PLATFORM_NOT_AVAILABLE_DATASETS_METADATA_OUTFILE_NAME = ( + "platform_not_available_datasets.metadata.tsv" +) +GENE_ID_MAPPING_ISSUES_DATASETS_METADATA_OUTFILE_NAME = ( + "gene_id_mapping_issues_datasets.metadata.tsv" +) FINAL_DATASETS_METADATA_OUTFILE_NAME = "final_datasets.metadata.tsv" -FILTERED_EXPERIMENTS_WITH_KEYWORDS_OUTFILE_NAME = "selected_datasets.keywords.yaml" ENTREZ_QUERY_MAX_RESULTS = 9999 ENTREZ_EMAIL = "stableexpression@nfcore.com" -ENTREZ_CHUNKSIZE = 2000 +PLATFORM_METADATA_CHUNKSIZE = 2000 NCBI_API_BASE_URL = ( "https://www.ncbi.nlm.nih.gov/geo/query/acc.cgi?view=data&acc={accession}" @@ -682,6 +688,36 @@ def filter_metadata_with_keywords(metadata: dict, keywords: list[str]) -> dict | return None +def export_filtered_out_datasets_if_any( + original_dataset_metadata_list: list[dict], + filtered_dataset_metadata_list: list[dict], + filtered_out_outfile_name: str, + filtered_feature: str, +): + # checking if all datasets were ok + filtered_out_dataset_metadata_list = [ + dataset + for dataset in original_dataset_metadata_list + if dataset not in filtered_dataset_metadata_list + ] + if filtered_out_dataset_metadata_list: + logger.warning( + f"{len(filtered_out_dataset_metadata_list)} dataset(s) did not have the correct {filtered_feature}!" + ) + logger.info( + f"Writing metadata of datasets corresponding to the wrong {filtered_feature} to {filtered_out_outfile_name}" + ) + df = pd.DataFrame.from_dict(filtered_out_dataset_metadata_list) + df.to_csv( + filtered_out_outfile_name, + sep="\t", + index=False, + header=True, + ) + else: + logger.info(f"All datasets had the correct {filtered_feature}") + + ################################################################## ################################################################## # MAIN @@ -704,6 +740,13 @@ def main(): f"Found {len(dataset_metadata_list)} datasets for species {args.species}" ) + if dataset_metadata_list: + logger.info( + f"Writing metadata of all experiments for species {args.species} to {SPECIES_DATASETS_OUTFILE_NAME}" + ) + df = pd.DataFrame.from_dict(dataset_metadata_list) + df.to_csv(SPECIES_DATASETS_OUTFILE_NAME, sep="\t", index=False, header=True) + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # FOR DEV PURPOSES / TESTING: RESTRICT TO SPECIFIC ACCESSIONS # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -736,52 +779,54 @@ def main(): # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ logger.info("Excluding wrong species") - tmp_lst = [ + good_species_dataset_metadata_list = [ dataset for dataset in dataset_metadata_list if species_is_ok(dataset, args.species) ] - # checking if all datasets were ok - if len(tmp_lst) < len(dataset_metadata_list): - logger.warning( - f"{len(dataset_metadata_list) - len(tmp_lst)} dataset(s) did not have the correct species!" - ) - selected_metadata_list = [] - else: - logger.info("All datasets had the correct species") - - dataset_metadata_list = tmp_lst + export_filtered_out_datasets_if_any( + original_dataset_metadata_list=dataset_metadata_list, + filtered_dataset_metadata_list=good_species_dataset_metadata_list, + filtered_out_outfile_name=WRONG_SPECIES_DATASETS_METADATA_OUTFILE_NAME, + filtered_feature="species", + ) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # PARSING METADATA # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ logger.info("Parsing metadata") - metadata_list = [] + augmented_dataset_metadata_list = [] with ( Pool(processes=args.nb_cpus) as p, tqdm(total=len(dataset_metadata_list)) as pbar, ): - for result in p.imap_unordered(parse_metadata, dataset_metadata_list): + for result in p.imap_unordered( + parse_metadata, good_species_dataset_metadata_list + ): pbar.update() pbar.refresh() if result is None: continue - metadata_list.append(result) + augmented_dataset_metadata_list.append(result) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # CHECKING MOLECULE TYPE / PLATFORM TECHNOLOGIES # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ logger.info("Validating datasets") - filtered_metadata_list = [ + specs_filtered_metadata_list = [ metadata - for metadata in metadata_list + for metadata in augmented_dataset_metadata_list if dataset_is_valid(metadata, args.platform) ] - logger.info( - f"{len(filtered_metadata_list)} datasets remaining after checking technology platform and molecule type" + + export_filtered_out_datasets_if_any( + original_dataset_metadata_list=augmented_dataset_metadata_list, + filtered_dataset_metadata_list=specs_filtered_metadata_list, + filtered_out_outfile_name=WRONG_SPECS_DATASETS_METADATA_OUTFILE_NAME, + filtered_feature="molecule type / platform technology", ) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -792,47 +837,47 @@ def main(): logger.info(f"Filtering experiments with keywords {args.keywords}") func = partial(filter_metadata_with_keywords, keywords=args.keywords) - selected_metadata_list = [] + keywords_filtered_metadata_list = [] with ( Pool(processes=args.nb_cpus) as p, - tqdm(total=len(filtered_metadata_list)) as pbar, + tqdm(total=len(specs_filtered_metadata_list)) as pbar, ): - for result in p.imap_unordered(func, filtered_metadata_list): + for result in p.imap_unordered(func, specs_filtered_metadata_list): pbar.update() pbar.refresh() if result is None: continue - selected_metadata_list.append(result) + keywords_filtered_metadata_list.append(result) - logger.info( - f"{len(selected_metadata_list)} datasets remaining after filtering with keywords" + export_filtered_out_datasets_if_any( + original_dataset_metadata_list=specs_filtered_metadata_list, + filtered_dataset_metadata_list=keywords_filtered_metadata_list, + filtered_out_outfile_name=WRONG_KEYWORDS_DATASETS_METADATA_OUTFILE_NAME, + filtered_feature="keywords", ) else: - selected_metadata_list = filtered_metadata_list + keywords_filtered_metadata_list = specs_filtered_metadata_list # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # GETTING METADATA OF SEQUENCING PLATFORMS # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ logger.info("Getting platform metadata") - tmp_lst = [] + platform_augmented_dataset_metadata_list = [] for selected_metadata_chunk_list in tqdm( - chunk_list(selected_metadata_list, ENTREZ_CHUNKSIZE) + chunk_list(keywords_filtered_metadata_list, PLATFORM_METADATA_CHUNKSIZE) ): - tmp_lst += get_platform_metadata(selected_metadata_chunk_list) - - # checking if platform metadata was found for all datasets - if len(tmp_lst) < len(selected_metadata_list): - logger.warning( - f"Platform metadata could not be retrieved for {len(selected_metadata_list) - len(tmp_lst)} dataset(s)!" + platform_augmented_dataset_metadata_list += get_platform_metadata( + selected_metadata_chunk_list ) - selected_metadata_list = [] - else: - logger.info("Platform metadata found for all datasets!") - # augmenting selected_metadata_list with platform metadata - selected_metadata_list = tmp_lst + export_filtered_out_datasets_if_any( + original_dataset_metadata_list=keywords_filtered_metadata_list, + filtered_dataset_metadata_list=platform_augmented_dataset_metadata_list, + filtered_out_outfile_name=PLATFORM_NOT_AVAILABLE_DATASETS_METADATA_OUTFILE_NAME, + filtered_feature="platform metadata", + ) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # FILTERING OUT DATASETS FOR WHICH ID MAPPING DOES NOT WORK @@ -845,99 +890,36 @@ def main(): with ( Pool(processes=args.nb_cpus) as p, - tqdm(total=len(filtered_metadata_list)) as pbar, + tqdm(total=len(platform_augmented_dataset_metadata_list)) as pbar, ): for metadata, can_be_converted in p.imap_unordered( - func, selected_metadata_list + func, platform_augmented_dataset_metadata_list ): pbar.update() pbar.refresh() if can_be_converted: final_metadata_list.append(metadata) - logger.info( - f"{len(final_metadata_list)} datasets remaining after checking gene ID mapping issues" + export_filtered_out_datasets_if_any( + original_dataset_metadata_list=platform_augmented_dataset_metadata_list, + filtered_dataset_metadata_list=final_metadata_list, + filtered_out_outfile_name=GENE_ID_MAPPING_ISSUES_DATASETS_METADATA_OUTFILE_NAME, + filtered_feature="gene id mapping", ) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # GETTING ACCESSIONS TO DOWNLOAD # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - if final_metadata_list: - logger.info(f"Kept {len(final_metadata_list)} datasets") - # getting accessions of selected experiments - selected_accessions = [ - metadata["accession"] for metadata in final_metadata_list - ] - - else: - msg = f"Could not find experiments for species {args.species}" - if args.keywords: - msg += f" and keywords {args.keywords}" - logger.warning(msg) - - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # EXPORTING DATA - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + logger.info(f"Kept {len(final_metadata_list)} datasets") + # getting accessions of selected experiments + selected_accessions = [metadata["accession"] for metadata in final_metadata_list] # exporting list of accessions logger.info(f"Writing accessions to {ACCESSION_OUTFILE_NAME}") with open(ACCESSION_OUTFILE_NAME, "w") as fout: fout.writelines([f"{acc}\n" for acc in selected_accessions]) - # exporting metadata - logger.info( - f"Writing metadata of all experiments for species {args.species} to {SPECIES_DATASETS_OUTFILE_NAME}" - ) - df = pd.DataFrame.from_dict(dataset_metadata_list) - df.to_csv(SPECIES_DATASETS_OUTFILE_NAME, sep="\t", index=False, header=True) - - if filtered_metadata_list: - logger.info( - f"Writing metadata of filtered datasets to {FILTERED_DATASETS_METADATA_OUTFILE_NAME}" - ) - df = pd.DataFrame.from_dict(filtered_metadata_list) - df.to_csv( - FILTERED_DATASETS_METADATA_OUTFILE_NAME, - sep="\t", - index=False, - header=True, - ) - - # exporting in YAML format too - logger.info( - f"Writing filtered experiments with keywords to {FILTERED_EXPERIMENTS_WITH_KEYWORDS_OUTFILE_NAME}" - ) - with open(FILTERED_EXPERIMENTS_WITH_KEYWORDS_OUTFILE_NAME, "w") as fout: - yaml.dump(selected_metadata_list, fout) - - rejected_metadata_list = [ - metadata for metadata in metadata_list if metadata not in filtered_metadata_list - ] - if rejected_metadata_list: - logger.info( - f"Writing metadata of rejected datasets to {REJECTED_DATASETS_METADATA_OUTFILE_NAME}" - ) - df = pd.DataFrame.from_dict(rejected_metadata_list) - df.to_csv( - REJECTED_DATASETS_METADATA_OUTFILE_NAME, - sep="\t", - index=False, - header=True, - ) - - if selected_metadata_list: - logger.info( - f"Writing metadata of selected datasets to {SELECTED_DATASETS_METADATA_OUTFILE_NAME}" - ) - df = pd.DataFrame.from_dict(selected_metadata_list) - df.to_csv( - SELECTED_DATASETS_METADATA_OUTFILE_NAME, - sep="\t", - index=False, - header=True, - ) - if final_metadata_list: logger.info( f"Writing metadata of selected datasets to {FINAL_DATASETS_METADATA_OUTFILE_NAME}" @@ -949,6 +931,11 @@ def main(): index=False, header=True, ) + else: + msg = f"Could not find experiments for species {args.species}" + if args.keywords: + msg += f" and keywords {args.keywords}" + logger.warning(msg) if __name__ == "__main__": diff --git a/conf/test_eatlas_geo.config b/conf/test_eatlas_geo.config index af0321de..63337e37 100644 --- a/conf/test_eatlas_geo.config +++ b/conf/test_eatlas_geo.config @@ -16,6 +16,6 @@ params { config_profile_description = 'Minimal test dataset with custom gene metadata to check pipeline function' // Input data - species = 'beta vulgaris' + species = 'solanum tuberosum' outdir = "results/test_eatlas_geo" } diff --git a/modules/local/geo/getaccessions/main.nf b/modules/local/geo/getaccessions/main.nf index 744e5066..6081287c 100644 --- a/modules/local/geo/getaccessions/main.nf +++ b/modules/local/geo/getaccessions/main.nf @@ -16,8 +16,14 @@ process GEO_GETACCESSIONS { output: path "accessions.txt", emit: accessions - path "*.metadata.tsv", emit: metadata - path "selected_datasets.keywords.yaml", optional: true, topic: selected_experiment_keywords + path "final_datasets.metadata.tsv", optional: true, emit: final_datasets_metadata + path "wrong_species_datasets.metadata.tsv", optional: true, emit: wrong_species_datasets_metadata + path "wrong_platform_moltype_datasets.metadata.tsv", optional: true, emit: wrong_platform_moltype_datasets_metadata + path "wrong_keywords_datasets.metadata.tsv", optional: true, emit: wrong_keywords_datasets_metadata + path "platform_not_available_datasets.metadata.tsv", optional: true, emit: platform_not_available_datasets_metadata + path "gene_id_mapping_issues_datasets.metadata.tsv", optional: true, emit: gene_id_mapping_issues_datasets_metadata + path "species_datasets.metadata.tsv", optional: true, emit: all_datasets_species_metadata + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('requests'), eval('python3 -c "import requests; print(requests.__version__)"'), topic: versions tuple val("${task.process}"), val('nltk'), eval('python3 -c "import nltk; print(nltk.__version__)"'), topic: versions From 1072dae39a0b2b1472fa9d63622ebb7f9d0413a0 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Mon, 27 Oct 2025 13:51:47 +0100 Subject: [PATCH 099/258] fix bug with scores not taken into account for final ranking --- bin/compute_stability_scores.py | 25 +++++++++----- bin/get_candidate_genes.py | 57 ++++++++++++++++++-------------- bin/normfinder.py | 2 +- modules/local/normfinder/main.nf | 4 +-- workflows/stableexpression.nf | 2 +- 5 files changed, 53 insertions(+), 37 deletions(-) diff --git a/bin/compute_stability_scores.py b/bin/compute_stability_scores.py index 03810bc4..0e054f40 100755 --- a/bin/compute_stability_scores.py +++ b/bin/compute_stability_scores.py @@ -37,14 +37,14 @@ class StabilityScorer: weights: dict[str, float] = field(default_factory=dict) def __post_init__(self): - self.compute_stability_score() self.parse_stability_score_weights() + self.compute_stability_score() def parse_stability_score_weights(self): - for field, weight in zip( + for weight_field, weight in zip( self.WEIGHT_FIELDS, self.stability_score_weights_str.split(",") ): - self.weights[field] = float(weight) + self.weights[weight_field] = float(weight) @staticmethod def quantile_normalise(data: pl.Series, new_name: str) -> pl.Series: @@ -101,8 +101,11 @@ def compute_stability_score(self) -> pl.LazyFrame: pl.col(config.RATIO_NULLS_VALID_SAMPLES_COLNAME) * self.WEIGHT_RATIO_NB_NULLS_TO_SCORING ) + print(self.weights) for col, weight in self.weights.items(): + print(col, weight) if col not in self.df.columns: + logger.warning(f"Column {col} not found in dataframe") continue normalised_col = f"{col}_normalised" stability_scoring_expr += pl.col(normalised_col) * weight / weight_sum @@ -118,7 +121,7 @@ def compute_stability_score(self) -> pl.LazyFrame: self.df = self.df.with_columns(expr.alias(config.STABILITY_SCORE_COLNAME)) print(self.df) - def get_statistics_with_stability_scores(self): + def get_statistics_with_stability_scores(self) -> pl.DataFrame: return ( self.df.sort( config.STABILITY_SCORE_COLNAME, descending=False, nulls_last=True @@ -190,10 +193,10 @@ def get_statistics(stat_files: list[Path]) -> pl.LazyFrame: return lf -def export_data(scored_lf: pl.LazyFrame): +def export_data(scored_df: pl.DataFrame): """Export gene expression data to CSV files.""" logger.info(f"Exporting stability scores to: {STATISTICS_WITH_SCORES_OUTFILENAME}") - scored_lf.write_csv(STATISTICS_WITH_SCORES_OUTFILENAME) + scored_df.write_csv(STATISTICS_WITH_SCORES_OUTFILENAME) logger.info("Done") @@ -223,10 +226,14 @@ def main(): # sort genes according to the metrics present in the dataframe stability_scorer = StabilityScorer(lf.collect(), args.stability_score_weights) - scored_lf = stability_scorer.get_statistics_with_stability_scores() - + scored_df = stability_scorer.get_statistics_with_stability_scores() + print( + scored_df.filter(pl.col(config.STABILITY_SCORE_COLNAME) == 0).select( + [config.VARIATION_COEFFICIENT_COLNAME] + ) + ) # exporting computed data - export_data(scored_lf) + export_data(scored_df) if __name__ == "__main__": diff --git a/bin/get_candidate_genes.py b/bin/get_candidate_genes.py index 5e009ea8..55874ae9 100755 --- a/bin/get_candidate_genes.py +++ b/bin/get_candidate_genes.py @@ -34,21 +34,21 @@ def parse_args(): type=Path, dest="count_file", required=True, - help="File containing counts for all genes" + help="File containing counts for all genes", ) parser.add_argument( "--stats", type=Path, dest="stat_file", required=True, - help="File containing statistics of expression over all datasets" + help="File containing statistics of expression over all datasets", ) parser.add_argument( "--candidate_selection_descriptor", type=str, dest="candidate_selection_descriptor", required=True, - help="Statistical descriptor for gene candidate selection." + help="Statistical descriptor for gene candidate selection.", ) parser.add_argument( "--nb-top-stable-genes", @@ -61,9 +61,8 @@ def parse_args(): def get_counts_for_candidates(file: Path, best_candidates: list[str]) -> pl.LazyFrame: - return ( - pl.scan_parquet(file) - .filter(pl.col(config.ENSEMBL_GENE_ID_COLNAME).is_in(best_candidates)) + return pl.scan_parquet(file).filter( + pl.col(config.ENSEMBL_GENE_ID_COLNAME).is_in(best_candidates) ) @@ -71,11 +70,14 @@ def get_stats(file: Path) -> pl.LazyFrame: return pl.scan_csv(file) -def get_best_candidates(stat_lf: pl.LazyFrame, candidate_selection_descriptor: str, nb_top_stable_genes: int) -> list[str]: - column_for_sorting = config.SCORING_BASE_TO_STABILITY_SCORE_COLUMN[candidate_selection_descriptor] +def get_best_candidates( + stat_lf: pl.LazyFrame, candidate_selection_descriptor: str, nb_top_stable_genes: int +) -> list[str]: + column_for_sorting = config.SCORING_BASE_TO_STABILITY_SCORE_COLUMN[ + candidate_selection_descriptor + ] return ( - stat_lf - .sort(column_for_sorting, descending=False, nulls_last=True) + stat_lf.sort(column_for_sorting, descending=False, nulls_last=True) .head(nb_top_stable_genes) .select(config.ENSEMBL_GENE_ID_COLNAME) .collect() @@ -83,36 +85,34 @@ def get_best_candidates(stat_lf: pl.LazyFrame, candidate_selection_descriptor: s .to_list() ) + def filter_out_genes_with_zero_counts(stat_lf: pl.LazyFrame) -> pl.LazyFrame: # keep only genes that show no zero count (ie. count > 0 for all samples) - return ( - stat_lf - .filter(pl.col(config.RATIO_ZEROS_COLNAME) == 0) - ) + return stat_lf.filter(pl.col(config.RATIO_ZEROS_COLNAME) == 0) def filter_out_low_expression_genes(stat_lf: pl.LazyFrame) -> pl.LazyFrame: max_quantile = ( - stat_lf - .select(config.EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME) + stat_lf.select(config.EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME) .max() .collect() .item() ) - return ( - stat_lf - .filter( - pl.col(config.EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME) >= max_quantile * FRACTION_LOWER_QUANTILES_TO_EXCLUDE - ) + return stat_lf.filter( + pl.col(config.EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME) + >= max_quantile * FRACTION_LOWER_QUANTILES_TO_EXCLUDE ) def export_data(filtered_count_lf: pl.LazyFrame): """Export gene expression data to CSV files.""" - logger.info(f"Exporting counts for candidate genes to: {CANDIDATE_COUNTS_OUTFILENAME}") + logger.info( + f"Exporting counts for candidate genes to: {CANDIDATE_COUNTS_OUTFILENAME}" + ) filtered_count_lf.collect().write_parquet(CANDIDATE_COUNTS_OUTFILENAME) logger.info("Done") + ##################################################### ##################################################### # MAIN @@ -125,10 +125,19 @@ def main(): stat_lf = get_stats(args.stat_file) + # first basic filters stat_lf = filter_out_low_expression_genes(stat_lf) - best_candidates = get_best_candidates(stat_lf, args.candidate_selection_descriptor, args.nb_top_stable_genes) + stat_lf = filter_out_genes_with_zero_counts(stat_lf) - candidate_gene_count_lf = get_counts_for_candidates(args.count_file, best_candidates) + # get base candidate genes based on the chosen statistical descriptor (std, mad, ...) + best_candidates = get_best_candidates( + stat_lf, args.candidate_selection_descriptor, args.nb_top_stable_genes + ) + + # get counts for candidate genes + candidate_gene_count_lf = get_counts_for_candidates( + args.count_file, best_candidates + ) export_data(candidate_gene_count_lf) diff --git a/bin/normfinder.py b/bin/normfinder.py index 43d98c3f..2464a107 100755 --- a/bin/normfinder.py +++ b/bin/normfinder.py @@ -19,7 +19,7 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -STABILITY_OUTFILENAME = "stability_values.csv" +STABILITY_OUTFILENAME = "stability_values.normfinder.csv" ############################################################################ diff --git a/modules/local/normfinder/main.nf b/modules/local/normfinder/main.nf index 0ef1828e..48094ca4 100644 --- a/modules/local/normfinder/main.nf +++ b/modules/local/normfinder/main.nf @@ -12,7 +12,7 @@ process NORMFINDER { path design_file output: - path('stability_values.csv'), emit: stability_values + path('stability_values.normfinder.csv'), emit: stability_values tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions @@ -25,7 +25,7 @@ process NORMFINDER { stub: """ - touch stability_values.csv + touch stability_values.normfinder.csv """ } diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index ecdd4baa..95e2f301 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -126,7 +126,7 @@ workflow STABLEEXPRESSION { MERGE_DATA.out.whole_design.set { ch_whole_design } // ----------------------------------------------------------------- - // COMPUTE BASE STATISTICS FOR ALL addDatasetIdToMetadataGENES + // COMPUTE BASE STATISTICS FOR ALL GENES // ----------------------------------------------------------------- BASE_STATISTICS ( From 8a6142f93d2189d64094c0bcb0614726aec63b44 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Mon, 27 Oct 2025 17:55:32 +0100 Subject: [PATCH 100/258] fix multiple bugs linked to scoring and final display in multiqc --- assets/multiqc_config.yml | 36 ++++++- bin/aggregate_results.py | 87 ++++++--------- bin/compute_base_statistics.py | 101 +++++++++--------- bin/config.py | 7 +- modules/local/aggregate_results/main.nf | 12 ++- modules/local/compute_base_statistics/main.nf | 8 +- subworkflows/local/base_statistics/main.nf | 2 +- workflows/stableexpression.nf | 12 ++- 8 files changed, 137 insertions(+), 128 deletions(-) diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index 1d3a53c4..6f03bec8 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -21,6 +21,10 @@ disable_version_detection: true max_table_rows: 5000 table_cond_formatting_colours: + - first: "#ffd700" + - second: "#C0C0C0" + - third: "#CD7F32" + - between_fourth_and_tenth: "#468F8F" - very_low: "#337ab7" - low: "#5bc0de" - medium: "#5cb85c" @@ -32,47 +36,69 @@ custom_data: section_name: "Top stable genes - ranked by stability" file_format: "csv" no_violin: true - sort_rows: false + #sort_rows: false description: | Expression descriptive statistics of all genes, ranked by stability. Expression was first normalised dataset per dataset, then log2(cpm + 1) transformed (cpm :counts per million) and finally fitted to [0, 1] using quantile normalisation. Genes are sorted by stability score - from the most stable to the least stable. plot_type: "table" pconfig: - col1_header: "Rank" + col1_header: "Ensembl Gene ID" + sort_rows: false headers: - rank: - rid: "Rank" - hidden: true ensembl_gene_id: title: "Ensembl Gene ID" description: | Gene IDs as shown in Ensembl + rank: + title: "Rank" + description: | + Rank of the gene based on stability score + cond_formatting_rules: + between_fourth_and_tenth: + - eq: 4 + - eq: 5 + - eq: 6 + - eq: 7 + - eq: 8 + - eq: 9 + - eq: 10 + third: + - eq: 3 + second: + - eq: 2 + first: + - eq: 1 stability_score: title: "Stability score" description: | Final stability score : the lower, the better format: "{:,.6f}" + scale: "RdYlGn-rev" variation_coefficient_normalised: title: "Normalised coefficient of variation" description: | Quantile normalised (among candidate genes) coefficient of variation ( std(expression) / mean(expression) ) across all samples. format: "{:,.6f}" + scale: "PRGn-rev" normfinder_stability_value_normalised: title: "Normalised Normfinder stability value " description: | Quantile normalised (among candidate genes) stability value as computed by Normfinder format: "{:,.6f}" + scale: "PRGn-rev" genorm_m_measure_normalised: title: "Normalised Genorm M-measure" description: | Quantile normalised (among candidate genes) M-measure as computed by Genorm format: "{:,.6f}" + scale: "PRGn-rev" median_absolute_deviation_normalised: title: "Normalised median absolute deviation" description: | Quantile normalised (among candidate genes) median absolute deviation of the expression across all samples. format: "{:,.4f}" + scale: "PRGn-rev" standard_deviation: title: "Standard deviation" description: | diff --git a/bin/aggregate_results.py b/bin/aggregate_results.py index 4f40146f..010865bb 100755 --- a/bin/aggregate_results.py +++ b/bin/aggregate_results.py @@ -18,41 +18,11 @@ ALL_COUNTS_FILTERED_PARQUET_OUTFILENAME = "all_counts_filtered.parquet" TOP_STABLE_GENES_COUNTS_OUTFILENAME = "top_stable_genes_transposed_counts_filtered.csv" -STAT_COLS = [ - config.RANK_COLNAME, - config.ENSEMBL_GENE_ID_COLNAME, - config.STABILITY_SCORE_COLNAME, - config.NORMFINDER_STABILITY_VALUE_COLNAME, - config.GENORM_M_MEASURE_COLNAME, - config.STANDARD_DEVIATION_COLNAME, - config.VARIATION_COEFFICIENT_COLNAME, - config.MEAN_COLNAME, - config.MEDIAN_COLNAME, - config.MAD_COLNAME, - config.EXPRESSION_LEVEL_STATUS_COLNAME, - config.RATIO_NULLS_COLNAME, - config.RATIO_NULLS_VALID_SAMPLES_COLNAME, - config.RATIO_ZEROS_COLNAME, -] - -# making complete list of columns to export -final_cols = [] -for colname in STAT_COLS: - final_cols.append(colname) - for platform in ["rnaseq", "microarray"]: - final_cols.append(f"{platform}_{colname}") -# adding gene description columns -final_cols += [ - config.GENE_NAME_COLNAME, - config.GENE_DESCRIPTION_COLNAME, - config.ORIGINAL_GENE_IDS_COLNAME, -] - # nb of top stable genes to select and to display at the end NB_TOP_STABLE_GENES = 1000 # quantile intervals NB_QUANTILES = 100 -NB_TOP_GENES_TO_SHOW_IN_LOG_COUNTS = 100 +NB_TOP_GENES_TO_SHOW_IN_BOX_PLOTS = 100 ALL_GENES_STATS_COLS = [ config.ENSEMBL_GENE_ID_COLNAME, @@ -85,6 +55,18 @@ def parse_args(): required=True, help="File containing statistics for all genes and stability scores by candidate genes", ) + parser.add_argument( + "--rnaseq", + type=Path, + dest="rnaseq_dataset_stat_file", + help="File containing base statistics for all genes and for all RNAseq datasets", + ) + parser.add_argument( + "--microarray", + type=Path, + dest="microarray_dataset_stat_file", + help="File containing base statistics for all genes and for all Microarray datasets", + ) parser.add_argument( "--metadata", type=str, @@ -162,7 +144,7 @@ def cast_count_columns_to_float32(lf: pl.LazyFrame) -> pl.LazyFrame: ) -def join_data_on_gene_id(stat_lf: pl.LazyFrame, *lfs) -> pl.LazyFrame: +def join_data_on_gene_id(stat_lf: pl.LazyFrame, *lfs: pl.LazyFrame) -> pl.LazyFrame: """Merge the statistics dataframe with the metadata dataframe and the mapping dataframe.""" # we need to ensure that the index of stat_lf are strings for lf in lfs: @@ -196,10 +178,6 @@ def get_mappings(mapping_files: list[Path]) -> pl.LazyFrame: ) -def get_statistics(stat_file: Path) -> pl.LazyFrame: - return pl.scan_csv(stat_file) - - def format_all_genes_statistics(stat_lf: pl.LazyFrame) -> pl.LazyFrame: """ Format the dataframe containing statistics for all genes by selecting the right columns @@ -240,21 +218,16 @@ def add_expression_level_status(lf: pl.LazyFrame) -> pl.LazyFrame: ) -def get_top_stable_gene_summary( - stat_summary_df: pl.LazyFrame, metadata_lf: pl.LazyFrame, mapping_lf: pl.LazyFrame +def get_all_genes_summary( + stat_summary_lf: pl.LazyFrame, *lfs: pl.LazyFrame ) -> pl.LazyFrame: """ Extract the most stable genes from the statistics dataframe. """ # add gene name, description and original gene IDs to statistics summary - stat_summary_df = join_data_on_gene_id(stat_summary_df, metadata_lf, mapping_lf) - - stat_summary_df = add_expression_level_status(stat_summary_df) - - available_columns = stat_summary_df.collect_schema().names() - return stat_summary_df.head(NB_TOP_STABLE_GENES).select( - [column for column in final_cols if column in available_columns] - ) + stat_summary_lf = join_data_on_gene_id(stat_summary_lf, *lfs) + stat_summary_lf = add_expression_level_status(stat_summary_lf) + return stat_summary_lf.head(NB_TOP_STABLE_GENES) def get_top_stable_genes_counts( @@ -262,7 +235,7 @@ def get_top_stable_genes_counts( ) -> pl.DataFrame: # getting list of top stable genes with their order top_genes_with_order = ( - stat_summary_df.head(NB_TOP_GENES_TO_SHOW_IN_LOG_COUNTS) + stat_summary_df.head(NB_TOP_GENES_TO_SHOW_IN_BOX_PLOTS) .select(config.ENSEMBL_GENE_ID_COLNAME) .with_row_index("sort_order") ) @@ -330,26 +303,32 @@ def main(): count_lf = get_counts(args.count_file) # getting data, including metadata and mappings - stat_summary_df = get_statistics(args.stat_file) + all_genes_stat_summary_lf = pl.scan_csv(args.stat_file) + + platform_datasets_stat_lfs = [ + pl.scan_csv(file) + for file in [args.rnaseq_dataset_stat_file, args.microarray_dataset_stat_file] + if file is not None + ] metadata_lf = get_metadata(metadata_files) mapping_lf = get_mappings(mapping_files) - formated_stat_lf = format_all_genes_statistics(stat_summary_df) + formated_stat_lf = format_all_genes_statistics(all_genes_stat_summary_lf) - top_stable_stat_summary_df = get_top_stable_gene_summary( - stat_summary_df, metadata_lf, mapping_lf + additional_data_lfs = [metadata_lf, mapping_lf] + platform_datasets_stat_lfs + top_stable_stat_summary_lf = get_all_genes_summary( + all_genes_stat_summary_lf, *additional_data_lfs ) # reducing dataframe size (it is only used for plotting by MultiQC) count_lf = cast_count_columns_to_float32(count_lf) top_stable_genes_counts_df = get_top_stable_genes_counts( - count_lf, top_stable_stat_summary_df + count_lf, top_stable_stat_summary_lf ) - # exporting computed data export_data( - top_stable_stat_summary_df, + top_stable_stat_summary_lf, formated_stat_lf, count_lf, top_stable_genes_counts_df, diff --git a/bin/compute_base_statistics.py b/bin/compute_base_statistics.py index 0674eace..16648baf 100755 --- a/bin/compute_base_statistics.py +++ b/bin/compute_base_statistics.py @@ -15,32 +15,26 @@ logger = logging.getLogger(__name__) # outfile names -ALL_GENES_RESULT_OUTFILENAME = "stats_all_genes.csv" - +ALL_GENES_RESULT_OUTFILE_SUFFIX = "stats_all_genes.csv" ############################################################################ # POLARS EXTENSIONS ############################################################################ + @pl.api.register_expr_namespace("row") class StatsExtension: def __init__(self, expr: pl.Expr): self._expr = expr def not_null_values(self): - return ( - self._expr - .list - .drop_nulls() - .list - ) + return self._expr.list.drop_nulls().list def mean(self) -> pl.Expr: """Mean over non nulls values in row""" return self.not_null_values().mean() - def std(self) -> pl.Expr: """Std over non nulls values in row""" return self.not_null_values().std() @@ -55,15 +49,13 @@ def mad(self) -> pl.Expr: self.not_null_values() .eval( (pl.element() - pl.element().median()).abs().median() - ) # returns a list with one element + ) # returns a list with one element .list.first() ) - @dataclass class GeneStatistician: - # we want to select samples that show a particularly low nb of genes MIN_RATIO_GENE_COUNT_TO_MEAN: ClassVar[float] = 0.75 # experimentally chosen # quantile intervals @@ -79,10 +71,13 @@ class GeneStatistician: def __post_init__(self): self.gene_count_per_sample_df = self.get_gene_counts_per_sample() - self.samples = self.count_lf.select(pl.exclude(config.ENSEMBL_GENE_ID_COLNAME)).collect_schema().names() + self.samples = ( + self.count_lf.select(pl.exclude(config.ENSEMBL_GENE_ID_COLNAME)) + .collect_schema() + .names() + ) self.samples_with_low_gene_count = self.get_samples_with_low_gene_count() - def get_colname(self, colname: str) -> str: return f"{self.platform}_{colname}" if self.platform else colname @@ -102,14 +97,14 @@ def get_gene_counts_per_sample(self) -> pl.DataFrame: .count() .collect() .transpose( - include_header=True, - header_name="sample", - column_names=["count"] + include_header=True, header_name="sample", column_names=["count"] ) ) def get_samples_with_low_gene_count(self) -> list[str]: - mean_gene_count = self.gene_count_per_sample_df[config.GENE_COUNT_COLNAME].mean() + mean_gene_count = self.gene_count_per_sample_df[ + config.GENE_COUNT_COLNAME + ].mean() return ( self.gene_count_per_sample_df.filter( (pl.col(config.GENE_COUNT_COLNAME) / mean_gene_count) @@ -130,7 +125,7 @@ def get_main_statistics(self) -> pl.LazyFrame: mean=pl.concat_list(self.samples).row.mean(), std=pl.concat_list(self.samples).row.std(), median=pl.concat_list(self.samples).row.median(), - mad=pl.concat_list(self.samples).row.mad() + mad=pl.concat_list(self.samples).row.mad(), ) return augmented_count_lf.select( @@ -139,51 +134,52 @@ def get_main_statistics(self) -> pl.LazyFrame: pl.col("std").alias(self.get_colname(config.STANDARD_DEVIATION_COLNAME)), pl.col("median").alias(self.get_colname(config.MEDIAN_COLNAME)), pl.col("mad").alias(self.get_colname(config.MAD_COLNAME)), - (pl.col("std") / pl.col("mean")).alias(self.get_colname(config.VARIATION_COEFFICIENT_COLNAME)), + (pl.col("std") / pl.col("mean")).alias( + self.get_colname(config.VARIATION_COEFFICIENT_COLNAME) + ), ) def compute_ratios_null_values(self): # the samples showing a low gene count will not be taken into account for the zero count penalty - valid_samples = [sample for sample in self.samples if sample not in self.samples_with_low_gene_count] + valid_samples = [ + sample + for sample in self.samples + if sample not in self.samples_with_low_gene_count + ] nb_nulls = ( - self.count_lf - .select(pl.exclude(config.ENSEMBL_GENE_ID_COLNAME).is_null()) + self.count_lf.select(pl.exclude(config.ENSEMBL_GENE_ID_COLNAME).is_null()) .collect() .sum_horizontal() ) nb_nulls_valid_samples = ( - self.count_lf - .select(pl.col(valid_samples).is_null()) + self.count_lf.select(pl.col(valid_samples).is_null()) .collect() .sum_horizontal() ) - self.stat_lf = ( - self.stat_lf - .with_columns( - ( nb_nulls / len(self.samples) ).alias(self.get_colname(config.RATIO_NULLS_COLNAME)), - ( nb_nulls_valid_samples / len(valid_samples) ).alias(self.get_colname(config.RATIO_NULLS_VALID_SAMPLES_COLNAME)) - ) + self.stat_lf = self.stat_lf.with_columns( + (nb_nulls / len(self.samples)).alias( + self.get_colname(config.RATIO_NULLS_COLNAME) + ), + (nb_nulls_valid_samples / len(valid_samples)).alias( + self.get_colname(config.RATIO_NULLS_VALID_SAMPLES_COLNAME) + ), ) - def compute_ratio_zeros(self): nb_zeros = ( - self.count_lf - .select(pl.exclude(config.ENSEMBL_GENE_ID_COLNAME) == 0) + self.count_lf.select(pl.exclude(config.ENSEMBL_GENE_ID_COLNAME) == 0) .collect() .sum_horizontal() ) - self.stat_lf = ( - self.stat_lf - .with_columns( - (nb_zeros / len(self.samples)).alias(self.get_colname(config.RATIO_ZEROS_COLNAME)), - ) + self.stat_lf = self.stat_lf.with_columns( + (nb_zeros / len(self.samples)).alias( + self.get_colname(config.RATIO_ZEROS_COLNAME) + ), ) - def get_quantile_intervals(self): """ Compute the quantile intervals for the mean expression levels of each gene in the dataframe. @@ -191,8 +187,13 @@ def get_quantile_intervals(self): The function assigns to each gene a quantile interval of its mean cpm compared to all genes. """ logger.info("Getting cpm quantiles") + mean_colname = self.get_colname(config.MEAN_COLNAME) self.stat_lf = self.stat_lf.with_columns( - (pl.col(self.get_colname(config.MEAN_COLNAME)).rank() / pl.col(config.MEAN_COLNAME).count() * self.NB_QUANTILES) + ( + pl.col(mean_colname).rank() + / pl.col(mean_colname).count() + * self.NB_QUANTILES + ) .floor() .cast(pl.Int8) # we want the only value = NB_QUANTILES to be NB_QUANTILES - 1 @@ -201,7 +202,6 @@ def get_quantile_intervals(self): .alias(self.get_colname(config.EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME)) ) - def compute_statistics(self) -> pl.LazyFrame: logger.info("Computing statistics and stability score") # getting expression statistics @@ -212,11 +212,9 @@ def compute_statistics(self) -> pl.LazyFrame: self.compute_ratio_zeros() # getting quantile intervals self.get_quantile_intervals() - return self.stat_lf - ##################################################### ##################################################### # FUNCTIONS @@ -231,9 +229,7 @@ def parse_args(): parser.add_argument( "--counts", type=Path, dest="count_file", required=True, help="Count file" ) - parser.add_argument( - "--platform", type=str, help="Platform name" - ) + parser.add_argument("--platform", type=str, help="Platform name") return parser.parse_args() @@ -242,12 +238,11 @@ def get_counts(file: Path) -> pl.LazyFrame: return pl.scan_parquet(file).sort(config.ENSEMBL_GENE_ID_COLNAME, descending=False) -def export_data( - stat_lf: pl.LazyFrame, platform: str -): +def export_data(stat_lf: pl.LazyFrame, platform: str): """Export gene expression data to CSV files.""" - logger.info(f"Exporting statistics for all genes to: {ALL_GENES_RESULT_OUTFILENAME}") - stat_lf.collect().write_csv(ALL_GENES_RESULT_OUTFILENAME) + outfile = f"{platform}.{ALL_GENES_RESULT_OUTFILE_SUFFIX}" + logger.info(f"Exporting statistics for all genes to: {outfile}") + stat_lf.collect().write_csv(outfile) logger.info("Done") @@ -269,7 +264,7 @@ def main(): stat_lf = gene_stat.compute_statistics() # exporting computed data - export_data(stat_lf, args.platform ) + export_data(stat_lf, args.platform) if __name__ == "__main__": diff --git a/bin/config.py b/bin/config.py index 0d5e8ccc..2e5331ca 100644 --- a/bin/config.py +++ b/bin/config.py @@ -1,7 +1,6 @@ - # general column names ENSEMBL_GENE_ID_COLNAME = "ensembl_gene_id" -RANK_COLNAME = "Rank" +RANK_COLNAME = "rank" # base statistics VARIATION_COEFFICIENT_COLNAME = "variation_coefficient" @@ -40,7 +39,5 @@ "genorm": GENORM_M_MEASURE_COLNAME, "std": STANDARD_DEVIATION_COLNAME, "cv": VARIATION_COEFFICIENT_COLNAME, - "mad": MAD_COLNAME + "mad": MAD_COLNAME, } - - diff --git a/modules/local/aggregate_results/main.nf b/modules/local/aggregate_results/main.nf index 10e2d86e..e44efc1b 100644 --- a/modules/local/aggregate_results/main.nf +++ b/modules/local/aggregate_results/main.nf @@ -10,8 +10,10 @@ process AGGREGATE_RESULTS { input: path count_file path stat_file - path metadata_files, stageAs: "?/*" - path mapping_files, stageAs: "?/*" + path rnaseq_dataset_stat_file, stageAs: "*/*" + path microarray_dataset_stat_file, stageAs: "*/*" + path metadata_files, stageAs: "*/*" + path mapping_files, stageAs: "*/*" output: path 'top_stable_genes_summary.csv', emit: top_stable_genes_summary @@ -22,12 +24,16 @@ process AGGREGATE_RESULTS { tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions script: + def rnaseq_dataset_stat_file_arg = rnaseq_dataset_stat_file ? "--rnaseq $rnaseq_dataset_stat_file" : "" + def microarray_dataset_stat_file_arg = microarray_dataset_stat_file ? "--microarray $microarray_dataset_stat_file" : "" """ aggregate_results.py \\ --counts $count_file \\ --stats $stat_file \\ --metadata "$metadata_files" \\ - --mappings "$mapping_files" + --mappings "$mapping_files" \\ + $rnaseq_dataset_stat_file_arg \\ + $microarray_dataset_stat_file_arg \\ """ } diff --git a/modules/local/compute_base_statistics/main.nf b/modules/local/compute_base_statistics/main.nf index 7484622c..1c0b485d 100644 --- a/modules/local/compute_base_statistics/main.nf +++ b/modules/local/compute_base_statistics/main.nf @@ -19,17 +19,19 @@ process COMPUTE_BASE_STATISTICS { val platform output: - path 'stats_all_genes.csv', emit: stats + path '*stats_all_genes.csv', emit: stats tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions script: - if ( platform != 'none' ) { + def args = task.ext.args ?: '' + if ( platform != [] ) { args += " --platform $platform" } """ compute_base_statistics.py \\ - --counts $count_file + --counts $count_file \\ + $args """ } diff --git a/subworkflows/local/base_statistics/main.nf b/subworkflows/local/base_statistics/main.nf index 18dfb5a9..86f6e811 100644 --- a/subworkflows/local/base_statistics/main.nf +++ b/subworkflows/local/base_statistics/main.nf @@ -37,7 +37,7 @@ workflow BASE_STATISTICS { COMPUTE_BASE_STATISTICS ( ch_all_counts, - "none" + [] ) emit: diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 95e2f301..9069abf3 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -135,6 +135,8 @@ workflow STABLEEXPRESSION { MERGE_DATA.out.microarray_counts ) + BASE_STATISTICS.out.stats.set { ch_all_datasets_stats } + // ----------------------------------------------------------------- // GET CANDIDATES AS REFERENCE GENE AND COMPUTES VARIOUS STABILITY VALUES // ----------------------------------------------------------------- @@ -142,10 +144,10 @@ workflow STABLEEXPRESSION { STABILITY_SCORING ( ch_all_counts, ch_whole_design, - BASE_STATISTICS.out.stats + ch_all_datasets_stats ) - STABILITY_SCORING.out.summary_statistics.set { ch_candidate_gene_stats_with_scores } + STABILITY_SCORING.out.summary_statistics.set { ch_stats_all_genes_with_scores } // ----------------------------------------------------------------- // AGGREGATE ALL RESULTS FOR MULTIQC @@ -153,7 +155,9 @@ workflow STABLEEXPRESSION { AGGREGATE_RESULTS ( ch_all_counts, - ch_candidate_gene_stats_with_scores, + ch_stats_all_genes_with_scores, + BASE_STATISTICS.out.rnaseq_stats.ifEmpty( [] ), + BASE_STATISTICS.out.microarray_stats.ifEmpty( [] ), MERGE_DATA.out.whole_gene_metadata, MERGE_DATA.out.whole_gene_id_mapping ) @@ -187,7 +191,7 @@ workflow STABLEEXPRESSION { DASH_APP( ch_all_counts, ch_whole_design, - ch_candidate_gene_stats_with_scores, + ch_stats_all_genes_with_scores, ch_all_genes_statistics ) From 6d6b324b908010b595c16d8547f4d278dbd638dd Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Tue, 28 Oct 2025 11:12:58 +0100 Subject: [PATCH 101/258] Update data_management.py --- .../dash_app/app/src/utils/data_management.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/modules/local/dash_app/app/src/utils/data_management.py b/modules/local/dash_app/app/src/utils/data_management.py index 0b7aaaec..fa5f41a3 100644 --- a/modules/local/dash_app/app/src/utils/data_management.py +++ b/modules/local/dash_app/app/src/utils/data_management.py @@ -8,11 +8,13 @@ @lru_cache(maxsize=None) class DataManager: def __init__(self): - self.all_counts_lf = self.get_all_count_data() - self.grouped_samples = self.get_samples_grouped_by_dataset() - self.candidate_genes_stat_df = self.get_candidate_genes_stat_data() - self.all_gene_stats_df = self.get_all_genes_stat_data() - self.genes = self.get_sorted_genes() + self.all_counts_lf: pl.LazyFrame = self.get_all_count_data() + self.grouped_samples: list[dict] = self.get_samples_grouped_by_dataset() + self.candidate_genes_stat_df: pl.DataFrame = ( + self.get_candidate_genes_stat_data() + ) + self.all_gene_stats_df: pl.DataFrame = self.get_all_genes_stat_data() + self.genes: list[str] = self.get_sorted_genes() @staticmethod def get_all_count_data() -> pl.LazyFrame: @@ -29,8 +31,8 @@ def get_samples_in_count_data(self) -> list[str]: def get_candidate_genes_stat_data(self) -> pl.DataFrame: file = f"{config.DATA_FOLDER}/{config.CANDIDATE_GENES_STAT_FILENAME}" stat_df = pl.read_csv(file) - cols_to_select = ["Rank"] + [ - col for col in stat_df.columns if col not in ["Rank", "is_candidate"] + cols_to_select = ["rank"] + [ + col for col in stat_df.columns if col not in ["rank", "is_candidate"] ] return stat_df.select(cols_to_select) From 401e38782efa4dd2bd0cab7bcc87608f9e7ef361 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Tue, 28 Oct 2025 12:37:37 +0100 Subject: [PATCH 102/258] fix lag with sample and gene lists upload --- modules/local/dash_app/app/app.py | 4 ++-- .../dash_app/app/src/components/settings/genes.py | 6 +++--- .../dash_app/app/src/components/settings/samples.py | 2 +- modules/local/dash_app/app/src/components/tables.py | 4 ++-- .../local/dash_app/app/src/utils/data_management.py | 11 +++++++++-- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/modules/local/dash_app/app/app.py b/modules/local/dash_app/app/app.py index 9db11350..27fc7bf6 100755 --- a/modules/local/dash_app/app/app.py +++ b/modules/local/dash_app/app/app.py @@ -14,8 +14,8 @@ from src.components import top, right_sidebar from src.callbacks import common, genes, samples -# DEBUG = True -DEBUG = False +DEBUG = True +# DEBUG = False # -------------------- SETUP LOGGING -------------------- diff --git a/modules/local/dash_app/app/src/components/settings/genes.py b/modules/local/dash_app/app/src/components/settings/genes.py index c1304801..7e7f0546 100644 --- a/modules/local/dash_app/app/src/components/settings/genes.py +++ b/modules/local/dash_app/app/src/components/settings/genes.py @@ -12,7 +12,7 @@ label=dmc.Text("Genes to display", fw=600, style={"paddingBottom": "5px"}), placeholder="Select genes of interest", nothingFoundMessage="No gene found", - data=data_manager.genes, + data=data_manager.get_sorted_genes(), value=[], w=400, clearable=True, @@ -23,13 +23,13 @@ checkIconPosition="right", hidePickedOptions=True, disabled=False, - persistence=True, + persistence=False, persisted_props=["value"], persistence_type="session", style=style.DROPDOWN, comboboxProps={ "shadow": "md", - "transitionProps": {"transition": "pop", "duration": 200}, + # "transitionProps": {"transition": "pop", "duration": 200}, }, ) ], diff --git a/modules/local/dash_app/app/src/components/settings/samples.py b/modules/local/dash_app/app/src/components/settings/samples.py index 60df69b0..a3c88bfc 100644 --- a/modules/local/dash_app/app/src/components/settings/samples.py +++ b/modules/local/dash_app/app/src/components/settings/samples.py @@ -12,7 +12,7 @@ label="Select list of samples to visualise", placeholder="Select samples", nothingFoundMessage="No samples found", - data=data_manager.grouped_samples, + data=data_manager.get_sorted_samples(), value=[], w=400, clearable=True, diff --git a/modules/local/dash_app/app/src/components/tables.py b/modules/local/dash_app/app/src/components/tables.py index aabe3446..00663171 100644 --- a/modules/local/dash_app/app/src/components/tables.py +++ b/modules/local/dash_app/app/src/components/tables.py @@ -20,7 +20,7 @@ def format_col_name(col: str): ], className="ag-theme-alpine", columnSizeOptions=dict(skipHeader=False, defaultMinWidth=100), - columnSize="autoSizetoFit", + # columnSize="autoSizetoFit", defaultColDef=dict( # type='rightAligned', filter=True, @@ -46,7 +46,7 @@ def format_col_name(col: str): ], className="ag-theme-alpine", columnSizeOptions=dict(skipHeader=False), - columnSize="autoSizetoFit", + # columnSize="autoSizetoFit", defaultColDef=dict( # type='rightAligned', filter=True, diff --git a/modules/local/dash_app/app/src/utils/data_management.py b/modules/local/dash_app/app/src/utils/data_management.py index fa5f41a3..0f6cb2ce 100644 --- a/modules/local/dash_app/app/src/utils/data_management.py +++ b/modules/local/dash_app/app/src/utils/data_management.py @@ -9,12 +9,10 @@ class DataManager: def __init__(self): self.all_counts_lf: pl.LazyFrame = self.get_all_count_data() - self.grouped_samples: list[dict] = self.get_samples_grouped_by_dataset() self.candidate_genes_stat_df: pl.DataFrame = ( self.get_candidate_genes_stat_data() ) self.all_gene_stats_df: pl.DataFrame = self.get_all_genes_stat_data() - self.genes: list[str] = self.get_sorted_genes() @staticmethod def get_all_count_data() -> pl.LazyFrame: @@ -40,6 +38,12 @@ def get_all_genes_stat_data(self) -> pl.DataFrame: file = f"{config.DATA_FOLDER}/{config.ALL_GENES_STAT_FILENAME}" return pl.read_csv(file) + def get_sorted_samples(self): + design_file = f"{config.DATA_FOLDER}/{config.ALL_DESIGNS_FILENAME}" + design_df = pd.read_csv(design_file) + return design_df["sample"].sort_values().tolist() + + """ def get_samples_grouped_by_dataset(self) -> list[dict]: samples_in_count_data = self.get_samples_in_count_data() samples_grouped_by_dataset = [] @@ -60,6 +64,7 @@ def get_samples_grouped_by_dataset(self) -> list[dict]: samples_grouped_by_dataset.append(batch_condition_samples_dict) return samples_grouped_by_dataset + """ def get_sorted_genes(self) -> list[str]: return ( @@ -72,6 +77,7 @@ def get_sorted_genes(self) -> list[str]: ) def get_gene_counts(self, gene: str) -> pd.Series: + print(f"getting gene counts for {gene}") return ( self.all_counts_lf.filter(pl.col(config.ENSEMBL_GENE_ID_COLNAME) == gene) .collect() @@ -80,6 +86,7 @@ def get_gene_counts(self, gene: str) -> pd.Series: ) def get_sample_counts(self, sample: str) -> pd.Series: + print(f"getting sample counts for {sample}") return ( self.all_counts_lf.select(sample) .drop_nulls() From 777259d3eeda18c0fe05f4cb8e82cea0f0e0fa13 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Tue, 28 Oct 2025 19:40:16 +0100 Subject: [PATCH 103/258] update dash app --- bin/clean_count_data.py | 48 +------------------ .../app/src/components/settings/genes.py | 14 +++--- .../app/src/components/settings/samples.py | 4 +- .../dash_app/app/src/utils/data_management.py | 11 ++--- 4 files changed, 13 insertions(+), 64 deletions(-) diff --git a/bin/clean_count_data.py b/bin/clean_count_data.py index d138999d..828904c3 100755 --- a/bin/clean_count_data.py +++ b/bin/clean_count_data.py @@ -48,52 +48,6 @@ def parse_args(): return parser.parse_args() -def is_valid_lf(lf: pl.LazyFrame, file: Path) -> bool: - """Check if a LazyFrame is valid. - - A LazyFrame is considered valid if it contains at least one row. - """ - try: - return not lf.limit(1).collect().is_empty() - except FileNotFoundError: - # strangely enough we get this error for some files existing but empty - logger.error(f"Could not find file {str(file)}") - return False - except pl.exceptions.NoDataError as err: - logger.error(f"File {str(file)} is empty: {err}") - return False - - -def get_valid_lazy_lfs(files: list[Path]) -> list[pl.LazyFrame]: - """Get a list of valid LazyFrames from a list of files. - - A LazyFrame is considered valid if it contains at least one row. - """ - lf_dict = {file: pl.scan_csv(file) for file in files} - return [lf for file, lf in lf_dict.items() if is_valid_lf(lf, file)] - - -def cast_cols_to_string(lf: pl.LazyFrame) -> pl.LazyFrame: - return lf.select( - [pl.col(column).cast(pl.String) for column in lf.collect_schema().names()] - ) - - -def concat_cast_to_string_and_drop_duplicates(files: list[Path]) -> pl.LazyFrame: - """Concatenate LazyFrames, cast all columns to String, and drop duplicates. - - The first step is to concatenate the LazyFrames. Then, the dataframe is cast - to String to ensure that all columns have the same data type. Finally, duplicate - rows are dropped. - """ - lfs = get_valid_lazy_lfs(files) - lfs = [cast_cols_to_string(lf) for lf in lfs] - concat_lf = pl.concat(lfs) - # dropping duplicates - # casting all columns to String - return concat_lf.unique() - - def get_count_columns(lf: pl.LazyFrame) -> list[str]: """Get all column names except the config.ENSEMBL_GENE_ID_COLNAME column. @@ -144,7 +98,7 @@ def remove_samples_with_low_ks_pvalue( )[config.SAMPLE_COLNAME].to_list() if not valid_samples: - logger.error("No more valid sample to process...") + logger.warning("No more valid sample to process...") sys.exit(101) # filtering the count dataframe to keep only the valid samples diff --git a/modules/local/dash_app/app/src/components/settings/genes.py b/modules/local/dash_app/app/src/components/settings/genes.py index 7e7f0546..54a32d97 100644 --- a/modules/local/dash_app/app/src/components/settings/genes.py +++ b/modules/local/dash_app/app/src/components/settings/genes.py @@ -13,7 +13,7 @@ placeholder="Select genes of interest", nothingFoundMessage="No gene found", data=data_manager.get_sorted_genes(), - value=[], + value=None, w=400, clearable=True, searchable=True, @@ -23,17 +23,17 @@ checkIconPosition="right", hidePickedOptions=True, disabled=False, - persistence=False, + persistence=True, persisted_props=["value"], persistence_type="session", - style=style.DROPDOWN, + # style=style.DROPDOWN, comboboxProps={ "shadow": "md", - # "transitionProps": {"transition": "pop", "duration": 200}, + "transitionProps": {"transition": "pop", "duration": 200}, }, ) ], - align="left", + align="stretch", gap="xl", ) @@ -54,7 +54,7 @@ mb=10, ), ], - align="left", + align="center", gap="xl", ) @@ -103,7 +103,7 @@ mb=35, ), ], - align="left", + align="center", gap="xl", ) diff --git a/modules/local/dash_app/app/src/components/settings/samples.py b/modules/local/dash_app/app/src/components/settings/samples.py index a3c88bfc..4daa5606 100644 --- a/modules/local/dash_app/app/src/components/settings/samples.py +++ b/modules/local/dash_app/app/src/components/settings/samples.py @@ -13,7 +13,7 @@ placeholder="Select samples", nothingFoundMessage="No samples found", data=data_manager.get_sorted_samples(), - value=[], + value=None, w=400, clearable=True, searchable=True, @@ -26,7 +26,7 @@ persistence=True, persisted_props=["value"], persistence_type="session", - style=style.DROPDOWN, + # style=style.DROPDOWN, comboboxProps={ "shadow": "md", "transitionProps": {"transition": "pop", "duration": 200}, diff --git a/modules/local/dash_app/app/src/utils/data_management.py b/modules/local/dash_app/app/src/utils/data_management.py index 0f6cb2ce..a990efb2 100644 --- a/modules/local/dash_app/app/src/utils/data_management.py +++ b/modules/local/dash_app/app/src/utils/data_management.py @@ -19,8 +19,8 @@ def get_all_count_data() -> pl.LazyFrame: file = f"{config.DATA_FOLDER}/{config.ALL_COUNT_FILENAME}" return pl.scan_parquet(file) - def get_samples_in_count_data(self) -> list[str]: - return ( + def get_sorted_samples(self) -> list[str]: + return sorted( self.all_counts_lf.select(pl.exclude(config.ENSEMBL_GENE_ID_COLNAME)) .collect_schema() .names() @@ -38,14 +38,9 @@ def get_all_genes_stat_data(self) -> pl.DataFrame: file = f"{config.DATA_FOLDER}/{config.ALL_GENES_STAT_FILENAME}" return pl.read_csv(file) - def get_sorted_samples(self): - design_file = f"{config.DATA_FOLDER}/{config.ALL_DESIGNS_FILENAME}" - design_df = pd.read_csv(design_file) - return design_df["sample"].sort_values().tolist() - """ def get_samples_grouped_by_dataset(self) -> list[dict]: - samples_in_count_data = self.get_samples_in_count_data() + samples_grouped_by_dataset = [] design_file = f"{config.DATA_FOLDER}/{config.ALL_DESIGNS_FILENAME}" From 8dc490bd3431cc082c407a9e3376001ba006dd2c Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 29 Oct 2025 08:46:16 +0100 Subject: [PATCH 104/258] fix issue with output file naming in compute_base_statistics.py --- bin/compute_base_statistics.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bin/compute_base_statistics.py b/bin/compute_base_statistics.py index 16648baf..86f3b3dd 100755 --- a/bin/compute_base_statistics.py +++ b/bin/compute_base_statistics.py @@ -238,9 +238,13 @@ def get_counts(file: Path) -> pl.LazyFrame: return pl.scan_parquet(file).sort(config.ENSEMBL_GENE_ID_COLNAME, descending=False) -def export_data(stat_lf: pl.LazyFrame, platform: str): +def export_data(stat_lf: pl.LazyFrame, platform: str | None): """Export gene expression data to CSV files.""" - outfile = f"{platform}.{ALL_GENES_RESULT_OUTFILE_SUFFIX}" + outfile = ( + f"{platform}.{ALL_GENES_RESULT_OUTFILE_SUFFIX}" + if platform + else ALL_GENES_RESULT_OUTFILE_SUFFIX + ) logger.info(f"Exporting statistics for all genes to: {outfile}") stat_lf.collect().write_csv(outfile) logger.info("Done") From 4ad94e448e34c42fcba7f1254a0b3a641cc1418d Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 29 Oct 2025 08:47:26 +0100 Subject: [PATCH 105/258] undo filtering out genes having zero counts --- bin/compute_stability_scores.py | 7 +------ bin/get_candidate_genes.py | 19 +++++++++++++++---- modules/local/get_candidate_genes/main.nf | 4 +++- nextflow.config | 1 + nextflow_schema.json | 9 +++++++++ subworkflows/local/stability_scoring/main.nf | 3 ++- 6 files changed, 31 insertions(+), 12 deletions(-) diff --git a/bin/compute_stability_scores.py b/bin/compute_stability_scores.py index 0e054f40..44eeba9c 100755 --- a/bin/compute_stability_scores.py +++ b/bin/compute_stability_scores.py @@ -119,7 +119,6 @@ def compute_stability_score(self) -> pl.LazyFrame: ) # add stability score column self.df = self.df.with_columns(expr.alias(config.STABILITY_SCORE_COLNAME)) - print(self.df) def get_statistics_with_stability_scores(self) -> pl.DataFrame: return ( @@ -227,11 +226,7 @@ def main(): # sort genes according to the metrics present in the dataframe stability_scorer = StabilityScorer(lf.collect(), args.stability_score_weights) scored_df = stability_scorer.get_statistics_with_stability_scores() - print( - scored_df.filter(pl.col(config.STABILITY_SCORE_COLNAME) == 0).select( - [config.VARIATION_COEFFICIENT_COLNAME] - ) - ) + # exporting computed data export_data(scored_df) diff --git a/bin/get_candidate_genes.py b/bin/get_candidate_genes.py index 55874ae9..0d4d0898 100755 --- a/bin/get_candidate_genes.py +++ b/bin/get_candidate_genes.py @@ -57,6 +57,13 @@ def parse_args(): required=True, help="Number of top stable genes to show", ) + parser.add_argument( + "--min-pct-quantile-expr-level", + type=float, + dest="min_pct_quantile_expr_level", + required=True, + help="Minimum percentage of quantile expression level", + ) return parser.parse_args() @@ -86,12 +93,16 @@ def get_best_candidates( ) +""" def filter_out_genes_with_zero_counts(stat_lf: pl.LazyFrame) -> pl.LazyFrame: # keep only genes that show no zero count (ie. count > 0 for all samples) return stat_lf.filter(pl.col(config.RATIO_ZEROS_COLNAME) == 0) +""" -def filter_out_low_expression_genes(stat_lf: pl.LazyFrame) -> pl.LazyFrame: +def filter_out_low_expression_genes( + stat_lf: pl.LazyFrame, min_pct_quantile_expr_level: float +) -> pl.LazyFrame: max_quantile = ( stat_lf.select(config.EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME) .max() @@ -100,7 +111,7 @@ def filter_out_low_expression_genes(stat_lf: pl.LazyFrame) -> pl.LazyFrame: ) return stat_lf.filter( pl.col(config.EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME) - >= max_quantile * FRACTION_LOWER_QUANTILES_TO_EXCLUDE + >= max_quantile * min_pct_quantile_expr_level ) @@ -126,8 +137,8 @@ def main(): stat_lf = get_stats(args.stat_file) # first basic filters - stat_lf = filter_out_low_expression_genes(stat_lf) - stat_lf = filter_out_genes_with_zero_counts(stat_lf) + stat_lf = filter_out_low_expression_genes(stat_lf, args.min_pct_quantile_expr_level) + # stat_lf = filter_out_genes_with_zero_counts(stat_lf) # get base candidate genes based on the chosen statistical descriptor (std, mad, ...) best_candidates = get_best_candidates( diff --git a/modules/local/get_candidate_genes/main.nf b/modules/local/get_candidate_genes/main.nf index fa657ed3..a9ce645b 100644 --- a/modules/local/get_candidate_genes/main.nf +++ b/modules/local/get_candidate_genes/main.nf @@ -12,6 +12,7 @@ process GET_CANDIDATE_GENES { path stat_file val candidate_selection_descriptor val nb_top_stable_genes + val min_pct_quantile_expr_level output: path 'candidate_counts.parquet', emit: counts @@ -24,7 +25,8 @@ process GET_CANDIDATE_GENES { --counts $count_file \\ --stats $stat_file \\ --candidate_selection_descriptor $candidate_selection_descriptor \\ - --nb-top-stable-genes $nb_top_stable_genes + --nb-top-stable-genes $nb_top_stable_genes \\ + --min-pct-quantile-expr-level $min_pct_quantile_expr_level """ } diff --git a/nextflow.config b/nextflow.config index c0ff8bdf..386305ca 100644 --- a/nextflow.config +++ b/nextflow.config @@ -45,6 +45,7 @@ params { quantile_normalisation_target_distribution = 'uniform' nb_top_gene_candidates = 5000 ks_pvalue_threshold = 0 + min_expr_threshold = 0.2 // stability scoring run_genorm = false diff --git a/nextflow_schema.json b/nextflow_schema.json index 40c79a5f..da4f5f52 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -231,6 +231,15 @@ "maximum": 1, "default": 0, "help_text": "P-value threshold for the Kolmogorov-Smirnov test of samples counts against a uniform distribution. Samples showing a p-value equal or below this threshold are considered not uniform and will therefore not be considered for computation of the stability score. Examples: `0`, `'0.05'`, `'1E-27'`. Provide a negative value to disable this filter. By default, all samples showing a pvalue of 0 will be discarded." + }, + "min_expr_threshold": { + "type": "number", + "description": "Minimum percentage of quantile expression level", + "fa_icon": "fas fa-chart-bar", + "minimum": 0, + "maximum": 1, + "default": 0.2, + "help_text": "To avoid genes with low expression levels from being considered for stability scoring, a threshold is set up. For example, a value of 0.2 means that the pipeline will reject genes showing an average expression level in the bottom 20% of all genes." } } }, diff --git a/subworkflows/local/stability_scoring/main.nf b/subworkflows/local/stability_scoring/main.nf index 20c4bd65..caa6f74e 100644 --- a/subworkflows/local/stability_scoring/main.nf +++ b/subworkflows/local/stability_scoring/main.nf @@ -26,7 +26,8 @@ workflow STABILITY_SCORING { ch_counts, ch_stats, params.candidate_selection_descriptor, - params.nb_top_gene_candidates + params.nb_top_gene_candidates, + params.min_expr_threshold ) GET_CANDIDATE_GENES.out.counts.set { ch_candidate_gene_counts } From abd02bfb748b73e99ffc6b902c4fe333c15cb60a Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 29 Oct 2025 08:59:32 +0100 Subject: [PATCH 106/258] change dash_app containers following issue with python 3.14; now ignores error if dash app cannot start; increased timeout to 60 --- galaxy/README.md | 4 +- galaxy/build/static/boilerplate.xml | 98 +++++++++++++++++- galaxy/tool/nf_core_stableexpression.xml | 120 ++++++++++++++++++++--- modules/local/dash_app/main.nf | 29 +++--- 4 files changed, 220 insertions(+), 31 deletions(-) diff --git a/galaxy/README.md b/galaxy/README.md index 6cc6217d..98ad5d5f 100644 --- a/galaxy/README.md +++ b/galaxy/README.md @@ -84,11 +84,11 @@ You can test the behaviour of your tool by providing different inputs and check To lint your tool: ``` -tool/lint.sh +test/lint.sh ``` To test your tool: ``` -tool/test.sh +test/test.sh ``` diff --git a/galaxy/build/static/boilerplate.xml b/galaxy/build/static/boilerplate.xml index 5e603aa8..15dcb8ed 100644 --- a/galaxy/build/static/boilerplate.xml +++ b/galaxy/build/static/boilerplate.xml @@ -10,12 +10,41 @@ VERSION="PIPELINE_VERSION"; echo "$VERSION" ]]> INPUTS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @misc{nf-core/stableexpression, - author = {}, - year = {}, + author = {Coen, Olivier}, + year = {2025}, title = {nf-core/stableexpression}, publisher = {GitHub}, journal = {GitHub repository}, - url = {https://github.com/nf-core/stableexpression}, + url = {https://github.com/OlivierCoen/stableexpression}, } diff --git a/galaxy/tool/nf_core_stableexpression.xml b/galaxy/tool/nf_core_stableexpression.xml index 5b1b0bb2..874e8bf1 100644 --- a/galaxy/tool/nf_core_stableexpression.xml +++ b/galaxy/tool/nf_core_stableexpression.xml @@ -1,21 +1,50 @@ This pipeline is dedicated to finding the most stable genes across count datasets - nextflow - apptainer - openjdk + nextflow + apptainer + openjdk Uniform +
    @@ -162,10 +198,72 @@ VERSION="1.0dev"; echo "$VERSION" - + + + ^\d+(\.\d+)?,\d+(\.\d+)?,\d+(\.\d+)?,\d+(\.\d+)?$ +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @misc{nf-core/stableexpression, - author = {}, - year = {}, + author = {Coen, Olivier}, + year = {2025}, title = {nf-core/stableexpression}, publisher = {GitHub}, journal = {GitHub repository}, - url = {https://github.com/nf-core/stableexpression}, + url = {https://github.com/OlivierCoen/stableexpression}, } diff --git a/modules/local/dash_app/main.nf b/modules/local/dash_app/main.nf index dc09a3de..cef4b552 100644 --- a/modules/local/dash_app/main.nf +++ b/modules/local/dash_app/main.nf @@ -4,13 +4,16 @@ process DASH_APP { conda "${moduleDir}/app/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? - 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/b3/b39ecd56e298b0ba94bed41bb36d67b0a2bc24634bc53baff9773dcc3d422c01/data': - 'community.wave.seqera.io/library/dash-ag-grid_dash-extensions_dash-iconify_dash-mantine-components_pruned:138d9ff01702db68' }" + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/4e/4eec747f2063edcc2d1b64e3b84a6b154fde1b9cd9d698446321b4a535432272/data': + 'community.wave.seqera.io/library/dash-ag-grid_dash-extensions_dash-iconify_dash-mantine-components_pruned:7cf6396dd8cd850e' }" errorStrategy { if (task.exitStatus == 100) { log.warn("Could not start the Dash application.") - return 'finish' // finishes started processes but reports error + return 'ignore' // only report errors but ignores it + } else { + log.warn("Could not start the Dash application due to unhandled error.") + return 'ignore' // ignore anyway } } @@ -22,15 +25,15 @@ process DASH_APP { output: path("*"), emit: app - //tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions - //tuple val("${task.process}"), val('dash'), eval('python3 -c "import dash; print(dash.__version__)"'), topic: versions - //tuple val("${task.process}"), val('dash-ag-grid'), eval('python3 -c "import dash_ag_grid; print(dash_ag_grid.__version__)"'), topic: versions - //tuple val("${task.process}"), val('dash-extensions'), eval('python3 -c "import dash_extensions; print(dash_extensions.__version__)"'), topic: versions - //tuple val("${task.process}"), val('dash-mantine-components'), eval('python3 -c "import dash_mantine_components; print(dash_mantine_components.__version__)"'), topic: versions - //tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions - //tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions - //tuple val("${task.process}"), val('pyarrow'), eval('python3 -c "import pyarrow; print(pyarrow.__version__)"'), topic: versions - //tuple val("${task.process}"), val('scipy'), eval('python3 -c "import scipy; print(scipy.__version__)"'), topic: versions + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions + tuple val("${task.process}"), val('dash'), eval('python3 -c "import dash; print(dash.__version__)"'), topic: versions + tuple val("${task.process}"), val('dash-ag-grid'), eval('python3 -c "import dash_ag_grid; print(dash_ag_grid.__version__)"'), topic: versions + tuple val("${task.process}"), val('dash-extensions'), eval('python3 -c "import dash_extensions; print(dash_extensions.__version__)"'), topic: versions + tuple val("${task.process}"), val('dash-mantine-components'), eval('python3 -c "import dash_mantine_components; print(dash_mantine_components.__version__)"'), topic: versions + tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions + tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions + tuple val("${task.process}"), val('pyarrow'), eval('python3 -c "import pyarrow; print(pyarrow.__version__)"'), topic: versions + tuple val("${task.process}"), val('scipy'), eval('python3 -c "import scipy; print(scipy.__version__)"'), topic: versions script: """ @@ -40,7 +43,7 @@ process DASH_APP { # trying to launch the app # if the resulting exit code is not 124 (exit code of timeout) then there is an error - timeout 10 python app.py || exit_code=\$?; [ "\$exit_code" -eq 124 ] && exit 0 || exit 100 + timeout 60 python app.py || exit_code=\$?; [ "\$exit_code" -eq 124 ] && exit 0 || exit 100 """ } From cacd420e84ad7c29a81a6ef06c4be75bd80a3d2d Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 29 Oct 2025 10:47:45 +0100 Subject: [PATCH 107/258] fixed issue with dash app versions and resource allocation --- modules/local/dash_app/main.nf | 33 +++++++++------- subworkflows/local/multiqc/main.nf | 28 ++++++++----- .../main.nf | 39 +++++++------------ workflows/stableexpression.nf | 29 ++++++++------ 4 files changed, 68 insertions(+), 61 deletions(-) diff --git a/modules/local/dash_app/main.nf b/modules/local/dash_app/main.nf index cef4b552..70249f23 100644 --- a/modules/local/dash_app/main.nf +++ b/modules/local/dash_app/main.nf @@ -1,6 +1,6 @@ process DASH_APP { - label 'process_single' + label 'process_high' conda "${moduleDir}/app/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? @@ -13,7 +13,7 @@ process DASH_APP { return 'ignore' // only report errors but ignores it } else { log.warn("Could not start the Dash application due to unhandled error.") - return 'ignore' // ignore anyway + return 'terminate' // ignore anyway } } @@ -24,16 +24,8 @@ process DASH_APP { path all_genes_stats output: - path("*"), emit: app - tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions - tuple val("${task.process}"), val('dash'), eval('python3 -c "import dash; print(dash.__version__)"'), topic: versions - tuple val("${task.process}"), val('dash-ag-grid'), eval('python3 -c "import dash_ag_grid; print(dash_ag_grid.__version__)"'), topic: versions - tuple val("${task.process}"), val('dash-extensions'), eval('python3 -c "import dash_extensions; print(dash_extensions.__version__)"'), topic: versions - tuple val("${task.process}"), val('dash-mantine-components'), eval('python3 -c "import dash_mantine_components; print(dash_mantine_components.__version__)"'), topic: versions - tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions - tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions - tuple val("${task.process}"), val('pyarrow'), eval('python3 -c "import pyarrow; print(pyarrow.__version__)"'), topic: versions - tuple val("${task.process}"), val('scipy'), eval('python3 -c "import scipy; print(scipy.__version__)"'), topic: versions + path("*"), emit: app + path "versions.yml", emit: versions script: """ @@ -41,9 +33,24 @@ process DASH_APP { mv ${all_counts} ${whole_design} ${top_stable_genes_summary} ${all_genes_stats} data/ cp -r ${moduleDir}/app/* . + # as of Nextflow version 25.04.8, having these versions sent to the versions topic channel + # results in ERROR ~ No such file or directory: /.command.env + cat <<-END_VERSIONS > versions.yml + "${task.process}": + python: \$( python3 --version | sed "s/Python //" ) + dash: \$( python3 -c "import dash; print(dash.__version__)" ) + dash-extensions: \$( python3 -c "import dash_extensions; print(dash_extensions.__version__)" ) + dash-mantine-components: \$( python3 -c "import dash_mantine_components; print(dash_mantine_components.__version__)" ) + dash-ag-grid: \$( python3 -c "import dash_ag_grid; print(dash_ag_grid.__version__)" ) + polars: \$( python3 -c "import polars; print(polars.__version__)" ) + pandas: \$( python3 -c "import pandas; print(pandas.__version__)" ) + pyarrow: \$( python3 -c "import pyarrow; print(pyarrow.__version__)" ) + scipy: \$( python3 -c "import scipy; print(scipy.__version__)" ) + END_VERSIONS + # trying to launch the app # if the resulting exit code is not 124 (exit code of timeout) then there is an error - timeout 60 python app.py || exit_code=\$?; [ "\$exit_code" -eq 124 ] && exit 0 || exit 100 + timeout 20 python app.py || exit_code=\$?; [ "\$exit_code" -eq 124 ] && exit 0 || exit 100 """ } diff --git a/subworkflows/local/multiqc/main.nf b/subworkflows/local/multiqc/main.nf index 7a5d0b9e..f4c37be8 100644 --- a/subworkflows/local/multiqc/main.nf +++ b/subworkflows/local/multiqc/main.nf @@ -1,8 +1,9 @@ include { MULTIQC } from '../../../modules/nf-core/multiqc' -include { customSoftwareVersionsToYAML } from '../utils_nfcore_stableexpression_pipeline' +include { formatVersionsToYAML } from '../utils_nfcore_stableexpression_pipeline' include { methodsDescriptionText } from '../utils_nfcore_stableexpression_pipeline' include { paramsSummaryMultiqc } from '../../nf-core/utils_nfcore_pipeline' +include { softwareVersionsToYAML } from '../../nf-core/utils_nfcore_pipeline' include { paramsSummaryMap } from 'plugin/nf-schema' /* @@ -15,20 +16,27 @@ workflow MULTIQC_WORKFLOW { take: ch_multiqc_files + ch_versions main: - // + // ------------------------------------------------------------------------------------ + // VERSIONS + // ------------------------------------------------------------------------------------ + + // Collate and save software versions obtained from topic channels + // TODO: use the nf-core functions when they are adapted to channel topics + // Collate and save software versions - // + formatVersionsToYAML ( Channel.topic('versions') ) + .mix ( softwareVersionsToYAML( ch_versions ) ) // mix with versions obtained from emit outputs + .collectFile(storeDir: "${params.outdir}/pipeline_info", name: 'software_mqc_versions.yml', sort: true, newLine: true) + .set { ch_collated_versions } - ch_collated_versions = customSoftwareVersionsToYAML( Channel.topic('versions') ) - .collectFile( - storeDir: "${params.outdir}/pipeline_info", - name: 'nf_core_' + 'stableexpression_software_' + 'mqc_' + 'versions.yml', - sort: true, - newLine: true - ) + + // ------------------------------------------------------------------------------------ + // CONFIG + // ------------------------------------------------------------------------------------ summary_params = paramsSummaryMap( workflow, parameters_schema: "nextflow_schema.json") ch_workflow_summary = Channel.value(paramsSummaryMultiqc(summary_params)) diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index 631c1b9b..01ac44e9 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -286,36 +286,23 @@ def methodsDescriptionText(mqc_methods_yaml) { return description_html.toString() } -// -// Get software versions for pipeline -// temporary replacements of the native processVersionsFromYAML -// -def customProcessVersionsFromYAML(yaml_file) { - def yaml = new org.yaml.snakeyaml.Yaml() - def versions = yaml.load(yaml_file) - return yaml.dumpAsMap(versions).trim() -} - // // Get channel of software versions used in pipeline in YAML format // temporary replacements of the native softwareVersionsToYAML // -def customSoftwareVersionsToYAML(versions) { - return Channel.of(workflowVersionToYAML()) - .concat( - versions - .unique() - .map { - name, tool, version -> [ name.tokenize(':').last(), [ tool, version ] ] - } - .groupTuple() - .map { - processName, toolInfo -> - def toolVersions = toolInfo.collect { tool, version -> " ${tool}: ${version}" }.join('\n') - "${processName}:\n${toolVersions}\n" - } - .map { customProcessVersionsFromYAML(it) } - ) +def formatVersionsToYAML( ch_versions ) { + return ch_versions + .unique() + .map { + name, tool, version -> [ name.tokenize(':').last(), [ tool, version ] ] + } + .groupTuple() + .map { + processName, toolInfo -> + def toolVersions = toolInfo.collect { tool, version -> " ${tool}: ${version}" }.join('\n') + "${processName}:\n${toolVersions}\n" + } + .unique() } diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 9069abf3..22536a62 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -34,6 +34,7 @@ workflow STABLEEXPRESSION { main: + ch_versions = Channel.empty() ch_top_stable_genes_summary = Channel.empty() ch_all_genes_statistics = Channel.empty() @@ -168,6 +169,18 @@ workflow STABLEEXPRESSION { } + // ----------------------------------------------------------------- + // DASH APPLICATION + // ----------------------------------------------------------------- + + DASH_APP( + ch_all_counts, + ch_whole_design, + ch_stats_all_genes_with_scores, + ch_all_genes_statistics + ) + ch_versions = ch_versions.mix ( DASH_APP.out.versions ) + // ----------------------------------------------------------------- // MULTIQC // ----------------------------------------------------------------- @@ -180,21 +193,13 @@ workflow STABLEEXPRESSION { .mix( Channel.topic('filtered_eatlas_experiment_metadata').collect() ) .set { ch_multiqc_files } - MULTIQC_WORKFLOW( ch_multiqc_files ) + MULTIQC_WORKFLOW( + ch_multiqc_files, + ch_versions + ) MULTIQC_WORKFLOW.out.report.toList().set { multiqc_report } - // ----------------------------------------------------------------- - // DASH APPLICATION - // ----------------------------------------------------------------- - - DASH_APP( - ch_all_counts, - ch_whole_design, - ch_stats_all_genes_with_scores, - ch_all_genes_statistics - ) - emit: multiqc_report From 78e7fc5000e7b0b6a2c699f24c97e5652e60a879 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 29 Oct 2025 11:52:20 +0100 Subject: [PATCH 108/258] replace stats all genes by a summary table of all genes with all computed stats --- assets/multiqc_config.yml | 33 +------------ bin/aggregate_results.py | 46 ++++--------------- modules/local/aggregate_results/main.nf | 2 +- .../dash_app/app/src/components/tables.py | 32 ++----------- .../local/dash_app/app/src/components/top.py | 14 +----- .../local/dash_app/app/src/utils/config.py | 3 +- .../dash_app/app/src/utils/data_management.py | 15 ++---- modules/local/dash_app/main.nf | 7 ++- workflows/stableexpression.nf | 7 ++- 9 files changed, 27 insertions(+), 132 deletions(-) diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index 6f03bec8..a393747e 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -36,10 +36,8 @@ custom_data: section_name: "Top stable genes - ranked by stability" file_format: "csv" no_violin: true - #sort_rows: false description: | Expression descriptive statistics of all genes, ranked by stability. - Expression was first normalised dataset per dataset, then log2(cpm + 1) transformed (cpm :counts per million) and finally fitted to [0, 1] using quantile normalisation. Genes are sorted by stability score - from the most stable to the least stable. plot_type: "table" pconfig: @@ -311,38 +309,9 @@ custom_data: file_format: "csv" description: | Distribution of descriptive statistics for all genes. - Expression was first computed as log2(cpm + 1), then fitted to [0, 1] using scikit-learn's QuantileTransformer. plot_type: "violin" pconfig: col1_header: "Ensembl Gene ID" - headers: - # colors from https://colorbrewer2.org/#type=diverging&scheme=BrBG&n=8 - standard_deviation: - title: "Standard deviation" - description: | - Standard deviation of the expression across samples - color: "#bf812d" - variation_coefficient: - title: "Coefficient of variation" - description: | - Coefficient of variation (ratio of standard deviation to mean) - color: "#f6e8c3" - mean: - title: "Average" - description: | - Average expression across samples. - Expression was first computed as log2(cpm + 1), then fitted to [0, 1] using scikit-learn's QuantileTransformer. - color: "#01665e" - normfinder_stability_value: - title: "Normfinder stability value " - description: | - Stability value as computed by Normfinder - color: "#01665e" - genorm_m_measure: - title: "Genorm M-measure" - description: | - M-measure as computed by Genorm - color: "#01665e" gene_counts: section_name: "Gene counts" @@ -425,7 +394,7 @@ sp: fn: "*top_stable_genes_transposed_counts*.csv" max_filesize: 50000000 # 50MB gene_statistics: - fn: "*stats_all_genes.csv" + fn: "*all_genes_summary.csv" max_filesize: 50000000 # 50MB gene_counts: fn: "*gene_count_statistics.csv" diff --git a/bin/aggregate_results.py b/bin/aggregate_results.py index 010865bb..d1d6a009 100755 --- a/bin/aggregate_results.py +++ b/bin/aggregate_results.py @@ -13,8 +13,8 @@ logger = logging.getLogger(__name__) # outfile names +ALL_GENE_SUMMARY_OUTFILENAME = "all_genes_summary.csv" TOP_STABLE_GENE_SUMMARY_OUTFILENAME = "top_stable_genes_summary.csv" -ALL_GENES_RESULT_OUTFILENAME = "stats_all_genes.csv" ALL_COUNTS_FILTERED_PARQUET_OUTFILENAME = "all_counts_filtered.parquet" TOP_STABLE_GENES_COUNTS_OUTFILENAME = "top_stable_genes_transposed_counts_filtered.csv" @@ -24,16 +24,6 @@ NB_QUANTILES = 100 NB_TOP_GENES_TO_SHOW_IN_BOX_PLOTS = 100 -ALL_GENES_STATS_COLS = [ - config.ENSEMBL_GENE_ID_COLNAME, - config.MEAN_COLNAME, - config.STANDARD_DEVIATION_COLNAME, - config.VARIATION_COEFFICIENT_COLNAME, - config.MEDIAN_COLNAME, - config.MAD_COLNAME, -] - - ##################################################### ##################################################### # FUNCTIONS @@ -178,19 +168,6 @@ def get_mappings(mapping_files: list[Path]) -> pl.LazyFrame: ) -def format_all_genes_statistics(stat_lf: pl.LazyFrame) -> pl.LazyFrame: - """ - Format the dataframe containing statistics for all genes by selecting the right columns - """ - return stat_lf.select( - [ - column - for column in ALL_GENES_STATS_COLS - if column in stat_lf.collect_schema().names() - ] - ) - - def get_status(quantile_interval: int) -> str: """Return the expression level status of the gene given its quantile interval.""" if NB_QUANTILES - 5 <= quantile_interval: @@ -227,7 +204,7 @@ def get_all_genes_summary( # add gene name, description and original gene IDs to statistics summary stat_summary_lf = join_data_on_gene_id(stat_summary_lf, *lfs) stat_summary_lf = add_expression_level_status(stat_summary_lf) - return stat_summary_lf.head(NB_TOP_STABLE_GENES) + return stat_summary_lf def get_top_stable_genes_counts( @@ -260,22 +237,20 @@ def get_top_stable_genes_counts( def export_data( + all_genes_summary_lf: pl.LazyFrame, top_stable_genes_summary_lf: pl.LazyFrame, - formated_stat_lf: pl.LazyFrame, all_counts_lf: pl.LazyFrame, top_stable_genes_counts_df: pl.DataFrame, ): """Export gene expression data to CSV files.""" + logger.info(f"Exporting statistics of all genes to: {ALL_GENE_SUMMARY_OUTFILENAME}") + all_genes_summary_lf.collect().write_csv(ALL_GENE_SUMMARY_OUTFILENAME) + logger.info( f"Exporting statistics of the top stable genes to: {TOP_STABLE_GENE_SUMMARY_OUTFILENAME}" ) top_stable_genes_summary_lf.collect().write_csv(TOP_STABLE_GENE_SUMMARY_OUTFILENAME) - logger.info( - f"Exporting statistics for all genes to: {ALL_GENES_RESULT_OUTFILENAME}" - ) - formated_stat_lf.collect().write_csv(ALL_GENES_RESULT_OUTFILENAME) - logger.info(f"Exporting all counts to: {ALL_COUNTS_FILTERED_PARQUET_OUTFILENAME}") all_counts_lf.collect().write_parquet(ALL_COUNTS_FILTERED_PARQUET_OUTFILENAME) @@ -313,23 +288,22 @@ def main(): metadata_lf = get_metadata(metadata_files) mapping_lf = get_mappings(mapping_files) - formated_stat_lf = format_all_genes_statistics(all_genes_stat_summary_lf) - additional_data_lfs = [metadata_lf, mapping_lf] + platform_datasets_stat_lfs - top_stable_stat_summary_lf = get_all_genes_summary( + all_genes_summary_lf = get_all_genes_summary( all_genes_stat_summary_lf, *additional_data_lfs ) + top_stable_stat_summary_lf = all_genes_summary_lf.head(NB_TOP_STABLE_GENES) + # reducing dataframe size (it is only used for plotting by MultiQC) count_lf = cast_count_columns_to_float32(count_lf) - top_stable_genes_counts_df = get_top_stable_genes_counts( count_lf, top_stable_stat_summary_lf ) # exporting computed data export_data( + all_genes_summary_lf, top_stable_stat_summary_lf, - formated_stat_lf, count_lf, top_stable_genes_counts_df, ) diff --git a/modules/local/aggregate_results/main.nf b/modules/local/aggregate_results/main.nf index e44efc1b..6f89040f 100644 --- a/modules/local/aggregate_results/main.nf +++ b/modules/local/aggregate_results/main.nf @@ -16,8 +16,8 @@ process AGGREGATE_RESULTS { path mapping_files, stageAs: "*/*" output: + path 'all_genes_summary.csv', emit: all_genes_summary path 'top_stable_genes_summary.csv', emit: top_stable_genes_summary - path 'stats_all_genes.csv', emit: stats_all_genes path 'all_counts_filtered.parquet', emit: all_counts_filtered path 'top_stable_genes_transposed_counts_filtered.csv', emit: top_stable_genes_transposed_counts_filtered tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions diff --git a/modules/local/dash_app/app/src/components/tables.py b/modules/local/dash_app/app/src/components/tables.py index 00663171..3f483e6b 100644 --- a/modules/local/dash_app/app/src/components/tables.py +++ b/modules/local/dash_app/app/src/components/tables.py @@ -12,37 +12,11 @@ def format_col_name(col: str): return col.replace("_", " ").capitalize() -candidate_gene_stats_table = dag.AgGrid( - rowData=data_manager.candidate_genes_stat_df.to_dicts(), +all_genes_stats_table = dag.AgGrid( + rowData=data_manager.all_genes_stat_df.to_dicts(), columnDefs=[ {"field": col, "headerName": format_col_name(col)} - for col in data_manager.candidate_genes_stat_df.columns - ], - className="ag-theme-alpine", - columnSizeOptions=dict(skipHeader=False, defaultMinWidth=100), - # columnSize="autoSizetoFit", - defaultColDef=dict( - # type='rightAligned', - filter=True, - resizable=True, - editable=False, - sortable=True, - ), - dashGridOptions=dict( - pagination=True, - paginationAutoPageSize=True, - enableCellTextSelection=True, - ensureDomOrder=True, - ), - style=style.AG_GRID, - id="candidate-gene-ranking-table", -) - -all_gene_stats_table = dag.AgGrid( - rowData=data_manager.all_gene_stats_df.to_dicts(), - columnDefs=[ - {"field": col, "headerName": format_col_name(col)} - for col in data_manager.all_gene_stats_df.columns + for col in data_manager.all_genes_stat_df.columns ], className="ag-theme-alpine", columnSizeOptions=dict(skipHeader=False), diff --git a/modules/local/dash_app/app/src/components/top.py b/modules/local/dash_app/app/src/components/top.py index 0fe29e87..90612ec9 100755 --- a/modules/local/dash_app/app/src/components/top.py +++ b/modules/local/dash_app/app/src/components/top.py @@ -29,13 +29,6 @@ color="red", style=style.HEADER_TABLIST_ITEM, ), - dmc.TabsTab( - dmc.Text("Reference gene ranking", fw=500), - leftSection=sample_icon, - value="ranking", - color="blue", - style=style.HEADER_TABLIST_ITEM, - ), dmc.TabsTab( dmc.Text("Statistics - all genes", fw=500), leftSection=sample_icon, @@ -61,12 +54,7 @@ value="samples", ), dmc.TabsPanel( - children=[tables.candidate_gene_stats_table], - style=style.TABS_PANEL, - value="ranking", - ), - dmc.TabsPanel( - children=[tables.all_gene_stats_table], + children=[tables.all_genes_stats_table], style=style.TABS_PANEL, value="gene_stats", ), diff --git a/modules/local/dash_app/app/src/utils/config.py b/modules/local/dash_app/app/src/utils/config.py index db94495c..50aaa5fa 100644 --- a/modules/local/dash_app/app/src/utils/config.py +++ b/modules/local/dash_app/app/src/utils/config.py @@ -12,8 +12,7 @@ DATA_FOLDER = "data" ALL_COUNT_FILENAME = "all_counts.parquet" -CANDIDATE_GENES_STAT_FILENAME = "stats_with_scores.csv" -ALL_GENES_STAT_FILENAME = "stats_all_genes.csv" +ALL_GENES_STAT_FILENAME = "all_genes_summary.csv" ALL_DESIGNS_FILENAME = "whole_design.csv" ENSEMBL_GENE_ID_COLNAME = "ensembl_gene_id" diff --git a/modules/local/dash_app/app/src/utils/data_management.py b/modules/local/dash_app/app/src/utils/data_management.py index a990efb2..842bcf2e 100644 --- a/modules/local/dash_app/app/src/utils/data_management.py +++ b/modules/local/dash_app/app/src/utils/data_management.py @@ -9,10 +9,7 @@ class DataManager: def __init__(self): self.all_counts_lf: pl.LazyFrame = self.get_all_count_data() - self.candidate_genes_stat_df: pl.DataFrame = ( - self.get_candidate_genes_stat_data() - ) - self.all_gene_stats_df: pl.DataFrame = self.get_all_genes_stat_data() + self.all_genes_stat_df: pl.DataFrame = self.get_all_genes_stat_data() @staticmethod def get_all_count_data() -> pl.LazyFrame: @@ -26,18 +23,14 @@ def get_sorted_samples(self) -> list[str]: .names() ) - def get_candidate_genes_stat_data(self) -> pl.DataFrame: - file = f"{config.DATA_FOLDER}/{config.CANDIDATE_GENES_STAT_FILENAME}" + def get_all_genes_stat_data(self) -> pl.DataFrame: + file = f"{config.DATA_FOLDER}/{config.ALL_GENES_STAT_FILENAME}" stat_df = pl.read_csv(file) cols_to_select = ["rank"] + [ col for col in stat_df.columns if col not in ["rank", "is_candidate"] ] return stat_df.select(cols_to_select) - def get_all_genes_stat_data(self) -> pl.DataFrame: - file = f"{config.DATA_FOLDER}/{config.ALL_GENES_STAT_FILENAME}" - return pl.read_csv(file) - """ def get_samples_grouped_by_dataset(self) -> list[dict]: @@ -63,7 +56,7 @@ def get_samples_grouped_by_dataset(self) -> list[dict]: def get_sorted_genes(self) -> list[str]: return ( - self.candidate_genes_stat_df.sort( + self.all_genes_stat_df.sort( by=config.STABILITY_SCORE_COLNAME, descending=False ) .select(config.ENSEMBL_GENE_ID_COLNAME) diff --git a/modules/local/dash_app/main.nf b/modules/local/dash_app/main.nf index 70249f23..e472352d 100644 --- a/modules/local/dash_app/main.nf +++ b/modules/local/dash_app/main.nf @@ -13,15 +13,14 @@ process DASH_APP { return 'ignore' // only report errors but ignores it } else { log.warn("Could not start the Dash application due to unhandled error.") - return 'terminate' // ignore anyway + return 'ignore' // ignore anyway } } input: path all_counts path whole_design - path top_stable_genes_summary - path all_genes_stats + path all_genes_summary output: path("*"), emit: app @@ -30,7 +29,7 @@ process DASH_APP { script: """ mkdir -p data - mv ${all_counts} ${whole_design} ${top_stable_genes_summary} ${all_genes_stats} data/ + mv ${all_counts} ${whole_design} ${all_genes_summary} data/ cp -r ${moduleDir}/app/* . # as of Nextflow version 25.04.8, having these versions sent to the versions topic channel diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 22536a62..9b40989d 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -163,8 +163,8 @@ workflow STABLEEXPRESSION { MERGE_DATA.out.whole_gene_id_mapping ) + AGGREGATE_RESULTS.out.all_genes_summary.set { ch_all_genes_summary } AGGREGATE_RESULTS.out.top_stable_genes_summary.set { ch_top_stable_genes_summary } - AGGREGATE_RESULTS.out.stats_all_genes.set { ch_all_genes_statistics } AGGREGATE_RESULTS.out.top_stable_genes_transposed_counts_filtered.set { ch_top_stable_genes_transposed_counts } } @@ -176,8 +176,7 @@ workflow STABLEEXPRESSION { DASH_APP( ch_all_counts, ch_whole_design, - ch_stats_all_genes_with_scores, - ch_all_genes_statistics + ch_all_genes_summary ) ch_versions = ch_versions.mix ( DASH_APP.out.versions ) @@ -187,7 +186,7 @@ workflow STABLEEXPRESSION { Channel.empty() .mix( ch_top_stable_genes_summary.collect() ) - .mix( ch_all_genes_statistics.collect() ) + .mix( ch_all_genes_summary.collect() ) .mix( ch_top_stable_genes_transposed_counts.collect() ) .mix( Channel.topic('all_eatlas_experiment_metadata').collect() ) .mix( Channel.topic('filtered_eatlas_experiment_metadata').collect() ) From 1d8ab0ff7665cb180850ee958c4edbb8fbc447ff Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 29 Oct 2025 17:00:44 +0100 Subject: [PATCH 109/258] improve dash app and allow selecting genes directly from the general table --- assets/multiqc_config.yml | 186 ++++++++++++++++-- modules/local/dash_app/app/assets/style.css | 4 +- .../local/dash_app/app/src/callbacks/genes.py | 34 +++- .../dash_app/app/src/callbacks/samples.py | 16 +- .../app/src/components/settings/genes.py | 2 +- .../app/src/components/settings/samples.py | 7 +- .../dash_app/app/src/components/tables.py | 29 ++- .../dash_app/app/src/utils/data_management.py | 3 +- 8 files changed, 240 insertions(+), 41 deletions(-) diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index a393747e..bfd2b9c6 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -52,6 +52,7 @@ custom_data: title: "Rank" description: | Rank of the gene based on stability score + scale: "RdYlGn-rev" cond_formatting_rules: between_fourth_and_tenth: - eq: 4 @@ -67,6 +68,18 @@ custom_data: - eq: 2 first: - eq: 1 + name: + title: "Ensembl name" + description: | + Gene name as shown in Ensembl (g:Profiler) + description: + title: "Ensembl description" + description: | + Gene description as shown in Ensembl (g:Profiler) + original_gene_ids: + title: "Original gene IDs" + description: | + Original gene IDs as stated in the input (provided or downloaded) datasets stability_score: title: "Stability score" description: | @@ -152,18 +165,6 @@ custom_data: - s_eq: "Low expression" very_low: - s_eq: "Very low expression" - name: - title: "Ensembl name" - description: | - Gene name as shown in Ensembl (g:Profiler) - description: - title: "Ensembl description" - description: | - Gene description as shown in Ensembl (g:Profiler) - original_gene_ids: - title: "Original gene IDs" - description: | - Original gene IDs as stated in the input (provided or downloaded) datasets rnaseq_standard_deviation: title: "Std [RNA-seq only]" description: | @@ -310,8 +311,165 @@ custom_data: description: | Distribution of descriptive statistics for all genes. plot_type: "violin" - pconfig: - col1_header: "Ensembl Gene ID" + headers: + stability_score: + title: "Stability score" + description: | + Final stability score : the lower, the better + format: "{:,.6f}" + color: "rgb(186,43,32)" + variation_coefficient_normalised: + title: "Normalised coefficient of variation" + description: | + Quantile normalised (among candidate genes) coefficient of variation ( std(expression) / mean(expression) ) across all samples. + format: "{:,.6f}" + color: "rgb(64, 122, 22)" + normfinder_stability_value_normalised: + title: "Normalised Normfinder stability value " + description: | + Quantile normalised (among candidate genes) stability value as computed by Normfinder + format: "{:,.6f}" + color: "rgb(64, 122, 22)" + genorm_m_measure_normalised: + title: "Normalised Genorm M-measure" + description: | + Quantile normalised (among candidate genes) M-measure as computed by Genorm + format: "{:,.6f}" + color: "rgb(64, 122, 22)" + median_absolute_deviation_normalised: + title: "Normalised median absolute deviation" + description: | + Quantile normalised (among candidate genes) median absolute deviation of the expression across all samples. + format: "{:,.4f}" + color: "rgb(64, 122, 22)" + standard_deviation: + title: "Standard deviation" + description: | + Standard deviation of the expression across all samples. + format: "{:,.6f}" + color: "rgb(227, 210, 36)" + variation_coefficient: + title: "Coefficient of variation" + description: | + Variation coefficient ( std(expression) / mean(expression) ) across all samples. + format: "{:,.6f}" + color: "rgb(227, 210, 36)" + normfinder_stability_value: + title: "Normfinder stability value " + description: | + Stability value as computed by Normfinder + format: "{:,.6f}" + color: "rgb(227, 210, 36)" + genorm_m_measure: + title: "Genorm M-measure" + description: | + M-measure as computed by Genorm + format: "{:,.6f}" + color: "rgb(227, 210, 36)" + mean: + title: "Average" + description: | + Average expression across all samples. + format: "{:,.4f}" + color: "rgb(85, 23, 166)" + median: + title: "Median" + description: | + Median expression across all samples. + format: "{:,.4f}" + color: "rgb(85, 23, 166)" + median_absolute_deviation: + title: "Median absolute deviation" + description: | + Median absolute deviation of the expression across all samples. + format: "{:,.4f}" + color: "rgb(85, 23, 166)" + rnaseq_standard_deviation: + title: "Std [RNA-seq only]" + description: | + Standard deviation of the expression across RNA-Seq samples. + format: "{:,.4f}" + rnaseq_variation_coefficient: + title: "Var coeff [RNA-seq only]" + description: | + Variation coefficient ( std(expression) / mean(expression) ) across RNA-Seq samples. + format: "{:,.4f}" + rnaseq_mean: + title: "Average [RNA-seq only]" + description: | + Average expression across samples. + format: "{:,.4f}" + rnaseq_median: + title: "Median [RNA-seq only]" + description: | + Median expression across RNA-Seq samples. + format: "{:,.4f}" + rnaseq_median_absolute_deviation: + title: "Mad [RNA-seq only]" + description: | + Median absolute deviation of the expression across RNA-Seq samples. + format: "{:,.4f}" + microarray_standard_deviation: + title: "Std [Microarray only]" + description: | + Standard deviation of the expression across Microarray samples. + format: "{:,.4f}" + microarray_variation_coefficient: + title: "Var coeff [Microarray only]" + description: | + Variation coefficient ( std(expression) / mean(expression) ) across Microarray samples. + format: "{:,.4f}" + microarray_mean: + title: "Average [Microarray only]" + description: | + Average expression across Microarray samples. + format: "{:,.4f}" + microarray_median: + title: "Median [Microarray only]" + description: | + Median expression across Microarray samples. + format: "{:,.4f}" + microarray_median_absolute_deviation: + title: "Mad [Microarray only]" + description: | + Median absolute deviation of the expression across Microarray samples. + format: "{:,.4f}" + ratio_nulls_in_all_samples: + title: "Ratio null values (all samples)" + description: | + Ratio of samples in which the gene is not represented. + ratio_nulls_in_valid_samples: + title: "Ratio null values (valid samples)" + description: | + Ratio of samples in which the gene is not represented, excluding samples with particularly low overall gene count. + ratio_zeros: + title: "Ratio zero values" + description: | + Ratio of samples in which the gene has a zero value. + rnaseq_ratio_nulls_in_all_samples: + title: "Ratio null values (all samples) [RNA-seq only]" + description: | + Ratio of RNA-Seq samples in which the gene is not represented. + rnaseq_ratio_nulls_in_valid_samples: + title: "Ratio null values (valid samples) [RNA-seq only]" + description: | + Ratio of RNA-Seq samples in which the gene is not represented, excluding samples with particularly low overall gene count. + rnaseq_ratio_zeros: + title: "Ratio zero values [RNA-seq only]" + description: | + Ratio of RNA-Seq samples in which the gene has a zero value. + microarray_ratio_nulls_in_all_samples: + title: "Ratio null values (all samples) [Microarray only]" + description: | + Ratio of Microarray samples in which the gene is not represented. + microarray_ratio_nulls_in_valid_samples: + title: "Ratio null values (valid samples) [Microarray only]" + description: | + Ratio of Microarray samples in which the gene is not represented, excluding samples with particularly low overall gene count. + microarray_ratio_zeros: + title: "Ratio zero values [Microarray only]" + description: | + Ratio of Microarray samples in which the gene has a zero value. gene_counts: section_name: "Gene counts" diff --git a/modules/local/dash_app/app/assets/style.css b/modules/local/dash_app/app/assets/style.css index 4cf6c682..f32fc1a6 100755 --- a/modules/local/dash_app/app/assets/style.css +++ b/modules/local/dash_app/app/assets/style.css @@ -4,6 +4,6 @@ transform: translateX(-50%); } -.ag-header-background-color { - color: green !important; +.mantine-Drawer-root { + width: 0.1em !important; } diff --git a/modules/local/dash_app/app/src/callbacks/genes.py b/modules/local/dash_app/app/src/callbacks/genes.py index 057b4297..704196f5 100644 --- a/modules/local/dash_app/app/src/callbacks/genes.py +++ b/modules/local/dash_app/app/src/callbacks/genes.py @@ -1,4 +1,4 @@ -from dash_extensions.enrich import Input, Output, State, callback +from dash_extensions.enrich import Input, Output, State, callback, ctx, Serverside import plotly.graph_objects as go from src.utils.data_management import DataManager @@ -13,14 +13,37 @@ ############################################## +def get_selected_rows(selected_genes: list[str]) -> list[dict]: + return data_manager.all_genes_stat_df.filter( + data_manager.all_genes_stat_df["ensembl_gene_id"].is_in(selected_genes) + ).to_dicts() + + def register_callbacks(): @callback( Output("gene-counts", "data"), + Output("gene-dropdown", "value"), + Output("gene-stats-table", "selectedRows"), Input("gene-dropdown", "value"), + Input("gene-stats-table", "selectedRows"), State("gene-counts", "data"), - prevent_initial_call=True, + # prevent_initial_call=True, ) - def update_gene_stored_data(selected_genes: list[str], stored_data: dict) -> dict: + def update_gene_stored_data( + selected_genes: list[str], table_selected_rows: list[dict], stored_data: dict + ) -> dict: + if ctx.triggered_id == "gene-stats-table": + # updating selected genes + if table_selected_rows is not None: + selected_genes = [row["ensembl_gene_id"] for row in table_selected_rows] + else: + selected_genes = [] + else: + # ctx.triggered_id is None (callback triggered at app launch / refresh) + # or ctx.triggered_id == "gene-dropdown": + # taking the dropdown values as reference (since there is persistence on it) + table_selected_rows = get_selected_rows(selected_genes) + # deleting stored data for genes not anymore in the selected list for stored_gene in list( stored_data.keys() @@ -36,7 +59,8 @@ def update_gene_stored_data(selected_genes: list[str], stored_data: dict) -> dic "counts": gene_data.to_list(), "samples": gene_data.index.to_list(), } - return stored_data + + return Serverside(stored_data), selected_genes, table_selected_rows @callback( Output("gene-graph", "figure"), @@ -47,7 +71,7 @@ def update_gene_stored_data(selected_genes: list[str], stored_data: dict) -> dic Input("gene-graph-boxmean", "value"), Input("gene-graph-display-points", "value"), State("gene-graph", "style"), - prevent_initial_call=True, + # prevent_initial_call=True, ) def update_gene_graph( gene_stored_data: dict, diff --git a/modules/local/dash_app/app/src/callbacks/samples.py b/modules/local/dash_app/app/src/callbacks/samples.py index ce05ca27..1ac296c8 100644 --- a/modules/local/dash_app/app/src/callbacks/samples.py +++ b/modules/local/dash_app/app/src/callbacks/samples.py @@ -1,7 +1,7 @@ import plotly.graph_objects as go import numpy as np from scipy.stats import gaussian_kde -from dash_extensions.enrich import Input, Output, State, callback +from dash_extensions.enrich import Input, Output, State, callback, Serverside from src.utils.data_management import DataManager @@ -20,7 +20,7 @@ def register_callbacks(): Output("sample-counts", "data"), Input("sample-dropdown", "value"), State("sample-counts", "data"), - prevent_initial_call=True, + # prevent_initial_call=True, ) def update_stored_data( sample_dropdown_values: list[str], stored_sample_counts: dict @@ -43,13 +43,14 @@ def update_stored_data( "genes": sample_data.index.to_list(), } - return updated_stored_sample_counts + return Serverside(updated_stored_sample_counts) @callback( Output("sample-graph", "figure"), Output("sample-graph", "style"), Output("sample_stats_display_accordion_control", "disabled"), Output("sample_points_display_accordion_control", "disabled"), + Output("sample_plot_customisation_accordion_control", "disabled"), Input("sample-counts", "data"), Input("curve-type", "value"), Input("sample-graph-jitter", "value"), @@ -57,7 +58,7 @@ def update_stored_data( Input("sample-graph-boxmean", "value"), Input("sample-graph-display-points", "value"), State("sample-graph", "style"), - prevent_initial_call=True, + # prevent_initial_call=True, ) def update_sample_histogram( sample_counts: dict, @@ -70,7 +71,7 @@ def update_sample_histogram( ): if not sample_counts: graph_style["display"] = "none" - return {}, graph_style + return {}, graph_style, True, True, True graph_style["display"] = "block" @@ -78,6 +79,7 @@ def update_sample_histogram( sample_stats_display_ac_disabled = True sample_points_display_ac_disabled = True + sample_plot_customisation_ac_disabled = True # we need to use the reversed order, otherwise the last traced added is at the top of the graph for sample, sample_data in reversed(sample_counts.items()): @@ -111,12 +113,14 @@ def update_sample_histogram( sample_stats_display_ac_disabled = False sample_points_display_ac_disabled = False + sample_plot_customisation_ac_disabled = False - fig.update_xaxes(range=[0, 1]) + fig.update_layout(xaxis=dict(range=[0, 1]), yaxis=dict(ticklabelstandoff=10)) return ( fig, graph_style, sample_stats_display_ac_disabled, sample_points_display_ac_disabled, + sample_plot_customisation_ac_disabled, ) diff --git a/modules/local/dash_app/app/src/components/settings/genes.py b/modules/local/dash_app/app/src/components/settings/genes.py index 54a32d97..0cbe0e41 100644 --- a/modules/local/dash_app/app/src/components/settings/genes.py +++ b/modules/local/dash_app/app/src/components/settings/genes.py @@ -13,7 +13,7 @@ placeholder="Select genes of interest", nothingFoundMessage="No gene found", data=data_manager.get_sorted_genes(), - value=None, + value=[], w=400, clearable=True, searchable=True, diff --git a/modules/local/dash_app/app/src/components/settings/samples.py b/modules/local/dash_app/app/src/components/settings/samples.py index 4daa5606..c7e6a97c 100644 --- a/modules/local/dash_app/app/src/components/settings/samples.py +++ b/modules/local/dash_app/app/src/components/settings/samples.py @@ -13,7 +13,7 @@ placeholder="Select samples", nothingFoundMessage="No samples found", data=data_manager.get_sorted_samples(), - value=None, + value=[], w=400, clearable=True, searchable=True, @@ -141,7 +141,10 @@ ), dmc.AccordionItem( [ - dmc.AccordionControl("Plot customisation"), + dmc.AccordionControl( + "Plot customisation", + id="sample_plot_customisation_accordion_control", + ), dmc.AccordionPanel(sample_graph_plot_type_stack), ], value="sample_plot_customisation", diff --git a/modules/local/dash_app/app/src/components/tables.py b/modules/local/dash_app/app/src/components/tables.py index 3f483e6b..cc68c81e 100644 --- a/modules/local/dash_app/app/src/components/tables.py +++ b/modules/local/dash_app/app/src/components/tables.py @@ -7,22 +7,25 @@ data_manager = DataManager() +NB_GENES_SELECTED_DEFAULT = 10 -def format_col_name(col: str): - return col.replace("_", " ").capitalize() +row_data = data_manager.all_genes_stat_df.to_dicts() +default_selected_rows = data_manager.all_genes_stat_df.head( + NB_GENES_SELECTED_DEFAULT +).to_dicts() +column_defs = [ + {"field": col, "headerName": col.replace("_", " ").capitalize()} + for col in data_manager.all_genes_stat_df.columns +] all_genes_stats_table = dag.AgGrid( - rowData=data_manager.all_genes_stat_df.to_dicts(), - columnDefs=[ - {"field": col, "headerName": format_col_name(col)} - for col in data_manager.all_genes_stat_df.columns - ], + rowData=row_data, + columnDefs=column_defs, className="ag-theme-alpine", - columnSizeOptions=dict(skipHeader=False), + # columnSizeOptions=dict(skipHeader=False), # columnSize="autoSizetoFit", defaultColDef=dict( - # type='rightAligned', filter=True, resizable=True, editable=False, @@ -33,7 +36,15 @@ def format_col_name(col: str): paginationAutoPageSize=True, enableCellTextSelection=True, ensureDomOrder=True, + animateRows=False, + rowSelection=dict(mode="multiRow"), + headerCheckboxSelection=False, + getRowId="params.data.ensembl_gene_id", ), + selectedRows=default_selected_rows, style=style.AG_GRID, + persistence=True, + persistence_type="session", + persisted_props=["selectedRows"], id="gene-stats-table", ) diff --git a/modules/local/dash_app/app/src/utils/data_management.py b/modules/local/dash_app/app/src/utils/data_management.py index 842bcf2e..6e38569c 100644 --- a/modules/local/dash_app/app/src/utils/data_management.py +++ b/modules/local/dash_app/app/src/utils/data_management.py @@ -65,16 +65,15 @@ def get_sorted_genes(self) -> list[str]: ) def get_gene_counts(self, gene: str) -> pd.Series: - print(f"getting gene counts for {gene}") return ( self.all_counts_lf.filter(pl.col(config.ENSEMBL_GENE_ID_COLNAME) == gene) + .select(pl.exclude(config.ENSEMBL_GENE_ID_COLNAME)) .collect() .to_pandas() .iloc[0] ) def get_sample_counts(self, sample: str) -> pd.Series: - print(f"getting sample counts for {sample}") return ( self.all_counts_lf.select(sample) .drop_nulls() From d04bb8b85e77a4437b93a3e53e7a09977050def3 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Thu, 30 Oct 2025 13:43:10 +0100 Subject: [PATCH 110/258] fix micromamba config --- nextflow.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nextflow.config b/nextflow.config index 386305ca..74f3139c 100644 --- a/nextflow.config +++ b/nextflow.config @@ -124,7 +124,7 @@ profiles { apptainer.enabled = false } micromamba { - onda.enabled = true + conda.enabled = true conda.useMicromamba = true docker.enabled = false singularity.enabled = false From 7387c3c1da76d630ee0e22b5b255740cc435df8a Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Thu, 30 Oct 2025 13:56:35 +0100 Subject: [PATCH 111/258] reformat galaxy part to pass lint and test on server --- galaxy/build/build_boilerplate.py | 2 + galaxy/build/build_tool.py | 18 ++++- galaxy/build/formatters/config/base.py | 2 +- .../build/formatters/schema/parameter/base.py | 21 ++++-- .../formatters/schema/parameter/datasets.py | 7 +- galaxy/build/static/boilerplate.xml | 53 +++++++------ galaxy/lint | 6 ++ galaxy/serve | 7 ++ galaxy/test | 15 ++++ galaxy/test/lint.sh | 6 -- galaxy/test/serve.sh | 7 -- galaxy/test/test.sh | 8 -- galaxy/tool/nf_core_stableexpression.xml | 74 +++++++++++-------- 13 files changed, 141 insertions(+), 85 deletions(-) mode change 100644 => 100755 galaxy/build/build_boilerplate.py mode change 100644 => 100755 galaxy/build/build_tool.py create mode 100755 galaxy/lint create mode 100755 galaxy/serve create mode 100755 galaxy/test delete mode 100755 galaxy/test/lint.sh delete mode 100755 galaxy/test/serve.sh delete mode 100755 galaxy/test/test.sh diff --git a/galaxy/build/build_boilerplate.py b/galaxy/build/build_boilerplate.py old mode 100644 new mode 100755 index 3b3b7f82..12c7a21a --- a/galaxy/build/build_boilerplate.py +++ b/galaxy/build/build_boilerplate.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 + import logging from pathlib import Path diff --git a/galaxy/build/build_tool.py b/galaxy/build/build_tool.py old mode 100644 new mode 100755 index 4ec44db0..202d2116 --- a/galaxy/build/build_tool.py +++ b/galaxy/build/build_tool.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 + import logging from pathlib import Path @@ -12,7 +14,7 @@ def main(): logger.info("Formatting config") - package_versions = ConfigFormatter.get_package_versions() + # package_versions = ConfigFormatter.get_package_versions() pipeline_metadata = ConfigFormatter.get_pipeline_metadata() logger.info("Formatting schema") @@ -25,11 +27,19 @@ def main(): with open(tool_boilerplate_file, "r") as fin: static_string = fin.read() + # checking if package versions were filled by the user + for package_version in ["OPENJDK_VERSION"]: + if package_version in static_string: + raise ValueError( + f"You must fill the package version in place of {package_version} before building" + ) + logger.info("Building tool XML file") tool_string = ( - static_string.replace("NEXTFLOW_VERSION", package_versions["nextflow"]) - .replace("APPTAINER_VERSION", package_versions["apptainer"]) - .replace("OPENJDK_VERSION", package_versions["openjdk"]) + static_string + # .replace("NEXTFLOW_VERSION", package_versions["nextflow"]) + # .replace("APPTAINER_VERSION", package_versions["apptainer"]) + # .replace("OPENJDK_VERSION", package_versions["openjdk"]) .replace("PIPELINE_VERSION", pipeline_metadata["version"]) .replace("DESCRIPTION", schema_formatter.pipeline_description) .replace("PARAMETERS", schema_formatter.params_cli) diff --git a/galaxy/build/formatters/config/base.py b/galaxy/build/formatters/config/base.py index 603caeb6..9b31d537 100644 --- a/galaxy/build/formatters/config/base.py +++ b/galaxy/build/formatters/config/base.py @@ -16,7 +16,7 @@ class BaseConfigFormatter: PACKAGES_REPOS: ClassVar[dict] = { "nextflow": "bioconda", "apptainer": "conda-forge", - "openjdk": "conda-forge", + # "openjdk": "conda-forge", } @classmethod diff --git a/galaxy/build/formatters/schema/parameter/base.py b/galaxy/build/formatters/schema/parameter/base.py index 8955e736..fb09787d 100644 --- a/galaxy/build/formatters/schema/parameter/base.py +++ b/galaxy/build/formatters/schema/parameter/base.py @@ -82,12 +82,21 @@ def get_input(self) -> str: if param_type == "string" and self.param_dict.get("format") == "file-path": input_type = "data" # removing extension check as files are renamed in .dat files by Galaxy - """ - if pattern := self.param_dict.get("pattern"): - # TODO: handle multiple extensions - extension = pattern.split(".")[-1].strip("$") - param_format = f' format="{extension}"' - """ + if pattern := self.param_dict.get( + "pattern" + ): # going from something like "^\\S+\\.(csv|yaml)$" to "csv,ya + # getting the extensions part + extension_str = pattern.split(".")[-1] + # removes recursively all leading and traling "(", ")" and "$" + extension_str = extension_str.strip("$()") + # getting list of extensions; removing dat because this extension is specifically made to handle Galaxy filename + extensions = [ext for ext in extension_str.split("|") if ext != "dat"] + formated_extensions_str = ",".join(extensions) + param_format = f' format="{formated_extensions_str}"' + else: + # there is no specific pattern provided in the schema, this means that the format does not matter much + # however, the planemo linter needs a format, so we specify format="data" + param_format = ' format="data"' else: input_type = self.NF_TYPES_TO_GALAXY[param_type] diff --git a/galaxy/build/formatters/schema/parameter/datasets.py b/galaxy/build/formatters/schema/parameter/datasets.py index 19e8b42b..bcdb42cd 100644 --- a/galaxy/build/formatters/schema/parameter/datasets.py +++ b/galaxy/build/formatters/schema/parameter/datasets.py @@ -17,12 +17,15 @@ def get_input(self) -> str: ).replace(self.param, "samplesheet") # changing label input_param_str = re.sub( - r'label="[\s\w]*"', 'format="csv" label="Samplesheet"', input_param_str + r'label="[\s\w]*"', 'label="Samplesheet"', input_param_str ) # adding conditional statement return f""" \t\t\t - + + + + {input_param_str} diff --git a/galaxy/build/static/boilerplate.xml b/galaxy/build/static/boilerplate.xml index 15dcb8ed..1eccca0a 100644 --- a/galaxy/build/static/boilerplate.xml +++ b/galaxy/build/static/boilerplate.xml @@ -3,7 +3,7 @@ nextflow apptainer - openjdk + openjdk - - - - - - +
    + + + + + + + +
    +
    + +
    - + -
    - - +
    + + + + + +
    - + -
    - - - - - +
    + + + + + + + +
    - + - diff --git a/galaxy/lint b/galaxy/lint new file mode 100755 index 00000000..5417d1cb --- /dev/null +++ b/galaxy/lint @@ -0,0 +1,6 @@ +#!/bin/bash + +galaxy_dir="$(dirname $(readlink -f "$0"))" +tool_file="${galaxy_dir}/tool/nf_core_stableexpression.xml" + +planemo lint $tool_file diff --git a/galaxy/serve b/galaxy/serve new file mode 100755 index 00000000..1065a0a2 --- /dev/null +++ b/galaxy/serve @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +galaxy_dir="$(dirname $(readlink -f "$0"))" +tool_dir="${galaxy_dir}/tool" + +planemo serve \ + $tool_dir diff --git a/galaxy/test b/galaxy/test new file mode 100755 index 00000000..05036eb9 --- /dev/null +++ b/galaxy/test @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +galaxy_dir="$(dirname $(readlink -f "$0"))" +tool_file="${galaxy_dir}/tool/nf_core_stableexpression.xml" + +TEST_OUTDIR="test_ouptut" + + +ARGS="$@" + +# add --update_test_data to create output file +planemo test \ + $tool_file \ + --job_output_files $TEST_OUTDIR \ + $ARGS diff --git a/galaxy/test/lint.sh b/galaxy/test/lint.sh deleted file mode 100755 index 99fbbea7..00000000 --- a/galaxy/test/lint.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -galaxy_dir="$(dirname $(dirname $(readlink -f "$0")))" -tool_file="${galaxy_dir}/tool/nf_core_stableexpression.xml" - -planemo lint $tool_file --fail_level error diff --git a/galaxy/test/serve.sh b/galaxy/test/serve.sh deleted file mode 100755 index dc421b15..00000000 --- a/galaxy/test/serve.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -galaxy_dir="$(dirname $(dirname $(readlink -f "$0")))" -tool_dir="${galaxy_dir}/tool" - -planemo serve $tool_dir - diff --git a/galaxy/test/test.sh b/galaxy/test/test.sh deleted file mode 100755 index c0fc9767..00000000 --- a/galaxy/test/test.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -galaxy_dir="$(dirname $(dirname $(readlink -f "$0")))" -tool_file="${galaxy_dir}/tool/nf_core_stableexpression.xml" - -# add --update_test_data to create output file -planemo test $tool_file --update_test_data - diff --git a/galaxy/tool/nf_core_stableexpression.xml b/galaxy/tool/nf_core_stableexpression.xml index 874e8bf1..282a0b87 100644 --- a/galaxy/tool/nf_core_stableexpression.xml +++ b/galaxy/tool/nf_core_stableexpression.xml @@ -2,7 +2,7 @@ This pipeline is dedicated to finding the most stable genes across count datasets nextflow - apptainer + micromamba openjdk ([a-zA-Z]+)[_ ]([a-zA-Z]+) - + + + + - + @@ -158,27 +160,27 @@ VERSION="1.0dev"; echo "$VERSION" ([A-Z0-9-]+,?)+ - + ([A-Z0-9-]+,?)+ - +
    ([A-Z0-9-]+,?)+ - + ([A-Z0-9-]+,?)+ - +
    - - + +
    @@ -212,53 +214,65 @@ VERSION="1.0dev"; echo "$VERSION" - - - - - - +
    + + + + + + + +
    +
    + +
    - + -
    - - +
    + + + + + +
    - + -
    - - - - - +
    + + + + + + + +
    - + - From 71b2868c83638f0f3b9ff16ac9e0348226759fb7 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Thu, 30 Oct 2025 21:47:21 +0100 Subject: [PATCH 112/258] fix segmentation fault issue in get_candidate_genes.py --- bin/get_candidate_genes.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/bin/get_candidate_genes.py b/bin/get_candidate_genes.py index 0d4d0898..87d48217 100755 --- a/bin/get_candidate_genes.py +++ b/bin/get_candidate_genes.py @@ -15,8 +15,6 @@ # outfile names CANDIDATE_COUNTS_OUTFILENAME = "candidate_counts.parquet" -FRACTION_LOWER_QUANTILES_TO_EXCLUDE = 0.2 - ##################################################### ##################################################### @@ -67,19 +65,10 @@ def parse_args(): return parser.parse_args() -def get_counts_for_candidates(file: Path, best_candidates: list[str]) -> pl.LazyFrame: - return pl.scan_parquet(file).filter( - pl.col(config.ENSEMBL_GENE_ID_COLNAME).is_in(best_candidates) - ) - - -def get_stats(file: Path) -> pl.LazyFrame: - return pl.scan_csv(file) - - def get_best_candidates( stat_lf: pl.LazyFrame, candidate_selection_descriptor: str, nb_top_stable_genes: int ) -> list[str]: + logger.info("Getting best candidates") column_for_sorting = config.SCORING_BASE_TO_STABILITY_SCORE_COLUMN[ candidate_selection_descriptor ] @@ -103,6 +92,7 @@ def filter_out_genes_with_zero_counts(stat_lf: pl.LazyFrame) -> pl.LazyFrame: def filter_out_low_expression_genes( stat_lf: pl.LazyFrame, min_pct_quantile_expr_level: float ) -> pl.LazyFrame: + logger.info("Filtering out low expression genes") max_quantile = ( stat_lf.select(config.EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME) .max() @@ -115,12 +105,19 @@ def filter_out_low_expression_genes( ) -def export_data(filtered_count_lf: pl.LazyFrame): +def get_counts_for_candidates(file: Path, best_candidates: list[str]) -> pl.DataFrame: + logger.info("Getting counts for candidate genes") + return pl.read_parquet(file).filter( + pl.col(config.ENSEMBL_GENE_ID_COLNAME).is_in(best_candidates) + ) + + +def export_data(filtered_count_df: pl.DataFrame): """Export gene expression data to CSV files.""" logger.info( f"Exporting counts for candidate genes to: {CANDIDATE_COUNTS_OUTFILENAME}" ) - filtered_count_lf.collect().write_parquet(CANDIDATE_COUNTS_OUTFILENAME) + filtered_count_df.write_parquet(CANDIDATE_COUNTS_OUTFILENAME) logger.info("Done") @@ -134,7 +131,7 @@ def export_data(filtered_count_lf: pl.LazyFrame): def main(): args = parse_args() - stat_lf = get_stats(args.stat_file) + stat_lf = pl.scan_csv(args.stat_file) # first basic filters stat_lf = filter_out_low_expression_genes(stat_lf, args.min_pct_quantile_expr_level) From 9015b142f461a07ae7f65374e469fad463d28c8d Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Thu, 30 Oct 2025 22:48:44 +0100 Subject: [PATCH 113/258] update doc in compute_stability_scores.py --- bin/compute_stability_scores.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/bin/compute_stability_scores.py b/bin/compute_stability_scores.py index 44eeba9c..18b7e80c 100755 --- a/bin/compute_stability_scores.py +++ b/bin/compute_stability_scores.py @@ -23,7 +23,7 @@ class StabilityScorer: N_QUANTILES: ClassVar[int] = 1000 - WEIGHT_FIELDS: ClassVar[list] = [ + WEIGHT_FIELDS: ClassVar[list[str]] = [ config.VARIATION_COEFFICIENT_COLNAME, config.MAD_COLNAME, config.NORMFINDER_STABILITY_VALUE_COLNAME, @@ -56,7 +56,7 @@ def quantile_normalise(data: pl.Series, new_name: str) -> pl.Series: normalised_array = transformer.fit_transform(array) return pl.Series(new_name, normalised_array.ravel()) - def compute_stability_score(self) -> pl.LazyFrame: + def compute_stability_score(self): logger.info("Computing stability score for candidate genes") candidate_df = self.df.filter( @@ -67,17 +67,22 @@ def compute_stability_score(self) -> pl.LazyFrame: normalised_data = {} null_data = {} weight_sum = 0 + # iterate over columns that can participate in stability score calculation for col, weight in self.weights.items(): + # if a column is absent, skip it if col not in self.df.columns: continue data = candidate_df.select(col).to_series() + # for each column present, we quantile normalise the data to have values between 0 and 1 + # and put these normalised data in another column suffixed with "_normalised" normalised_col = f"{col}_normalised" normalised_data[col] = self.quantile_normalise( data, new_name=normalised_col ) # creating a null column with same name null_data[col] = pl.Series(normalised_col, [None] * len(non_candidate_df)) - # if this column is present, add its weight to the sum + # counting the sum of weights corresponding to the columns present + # so that we can normalise the weights afterwards weight_sum += weight # replacing original data with quantile normalised ones @@ -101,9 +106,8 @@ def compute_stability_score(self) -> pl.LazyFrame: pl.col(config.RATIO_NULLS_VALID_SAMPLES_COLNAME) * self.WEIGHT_RATIO_NB_NULLS_TO_SCORING ) - print(self.weights) + for col, weight in self.weights.items(): - print(col, weight) if col not in self.df.columns: logger.warning(f"Column {col} not found in dataframe") continue From 2d1909710e9c8be55fb104c84b05001f4f8978a2 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Thu, 30 Oct 2025 22:56:54 +0100 Subject: [PATCH 114/258] update galaxy files to pass linter and web app UI tests --- galaxy/README.md | 11 ++---- galaxy/dev/nextflow_apptainer.xml | 34 +++++++++++++++++++ galaxy/environment.yml | 10 ++++++ galaxy/serve | 1 + galaxy/tool/nf_core_stableexpression.xml | 3 ++ galaxy/tool/rebuild_samplesheet.py | 8 ++--- .../tool/test-data/microarray.normalised.csv | 2 +- galaxy/tool/test-data/rnaseq.raw.csv | 2 +- 8 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 galaxy/dev/nextflow_apptainer.xml create mode 100644 galaxy/environment.yml diff --git a/galaxy/README.md b/galaxy/README.md index 98ad5d5f..112eb49d 100644 --- a/galaxy/README.md +++ b/galaxy/README.md @@ -2,18 +2,13 @@ ## Setup build / testing environment -NB: You need micromamba installed: - -``` -"${SHELL}" <(curl -L micro.mamba.pm/install.sh) -``` +NB: You need conda installed (micromamba does not work, since the Galaxy installer looks for a venv / conda environment) Create a new environment with python and planemo installed: ``` -micromamba create -n galaxy -c conda-forge python=3.12 -y -micromamba activate galaxy -pip install planemo +conda env create -f environment.yml -y +conda activate planemo ``` ## Build tool XML file diff --git a/galaxy/dev/nextflow_apptainer.xml b/galaxy/dev/nextflow_apptainer.xml new file mode 100644 index 00000000..27f1f851 --- /dev/null +++ b/galaxy/dev/nextflow_apptainer.xml @@ -0,0 +1,34 @@ + + This pipeline is dedicated to finding the most stable genes across count datasets + + nextflow + apptainer + fuse-overlayfs + openjdk + + + results/species.txt + + && zip -r results.zip results + + ]]> + + + + + + + + diff --git a/galaxy/environment.yml b/galaxy/environment.yml new file mode 100644 index 00000000..feae659f --- /dev/null +++ b/galaxy/environment.yml @@ -0,0 +1,10 @@ +name: planemo +channels: + - defaults + - conda-forge + - bioconda + - nodefaults +dependencies: + - python=3.12 + - pip: + - planemo==0.75.33 diff --git a/galaxy/serve b/galaxy/serve index 1065a0a2..ea0e649d 100755 --- a/galaxy/serve +++ b/galaxy/serve @@ -4,4 +4,5 @@ galaxy_dir="$(dirname $(readlink -f "$0"))" tool_dir="${galaxy_dir}/tool" planemo serve \ + --no_cleanup \ $tool_dir diff --git a/galaxy/tool/nf_core_stableexpression.xml b/galaxy/tool/nf_core_stableexpression.xml index 282a0b87..39b39c4e 100644 --- a/galaxy/tool/nf_core_stableexpression.xml +++ b/galaxy/tool/nf_core_stableexpression.xml @@ -3,6 +3,7 @@ nextflow micromamba + fuse-overlayfs openjdk Date: Fri, 31 Oct 2025 08:31:19 +0100 Subject: [PATCH 115/258] update nextflow_schema.json --- nextflow_schema.json | 92 ++++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 45 deletions(-) diff --git a/nextflow_schema.json b/nextflow_schema.json index da4f5f52..3970bfe7 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -14,10 +14,10 @@ "properties": { "species": { "type": "string", - "description": "Species name", + "description": "Scientifc species name (genus and species)", "fa_icon": "fas fa-hippo", "pattern": "([a-zA-Z]+)[_ ]([a-zA-Z]+)", - "help_text": "Genus and species may be separated by ` ` or `_`. Example: `--species 'Arabidopsis thaliana'` or `--species 'homo_sapiens'`" + "help_text": "Genus and species may be separated by ` ` or `_`. Example: `--species 'Arabidopsis thaliana'` or `--species 'homo_sapiens'`. Character case is not important." }, "outdir": { "type": "string", @@ -31,36 +31,35 @@ "format": "file-path", "exists": true, "schema": "assets/schema_datasets.json", - "pattern": "^\\S+\\.(csv|dat)$", - "description": "User count datasets", - "help_text": "Path to CSV file containing information about the input count datasets and their related experimental design. The dataset file should be a comma-separated file with 3 columns, and a header row. See [usage docs](https://nf-co.re/stableexpression/usage#samplesheet-input) for more information. Before running the pipeline, and for each user count dataset, you will need to create a design file with information about the samples in your experiment. Use this parameter to specify its location. Combine with --skip_fetch_eatlas_accessions if you only want to analyse your own count datasets.", + "pattern": "^\\S+\\.(csv|yaml|yml|dat)$", + "description": "Custom datasets (counts + designs)", + "help_text": "Path to CSV / YAML file listing your own count datasets and their related experimental design. This file should be a comma-separated file with 4 columns (`counts`, `design`, `platform` and `normalised`). It must have a header row. Before running the pipeline, and for each count dataset provided by you, a design file with information about the samples in your experiment is required. Combine with both --skip_fetch_eatlas_accessions and --skip_fetch_geo_accessions if you only want to analyse your own count datasets. Otherwise, accessions from Expression Atlas and GEO will be fetched automatically. See [usage docs](https://nf-co.re/stableexpression/usage#samplesheet-input) for more information. ", "fa_icon": "fas fa-file-csv" }, "keywords": { "type": "string", "description": "Keywords used for selecting specific Expression Atlas / GEO accessions", - "default": "", - "fa_icon": "fas fa-highlighter", - "pattern": "([a-zA-Z,]+)", + "fa_icon": "fas fa-font", + "pattern": "(([a-zA-Z,]+))?", "help_text": "Keywords (separated by commas) to use when retrieving specific experiments from Expression Atlas and / or GEO datasets. The pipeline will select all Expression Atlas experiments / GEO datasets that contain the provided keywords in their description of in one of the condition names. Example: `--keywords 'stress,flowering'`. This parameter is unused if both --skip_fetch_eatlas_accessions and --skip_fetch_geo_accessions are set." }, "platform": { "type": "string", "enum": ["rnaseq", "microarray"], "description": "Only download from this platform", - "fa_icon": "fas fa-id-card", - "help_text": "By default, data from all platform are downloaded. If this parameter is specified, a filter is applied to get data from only one specific type of platform. This filter is only used while fetching appropriate Expression atlas / GEO accessions. It will not filter accessions provided directly by the user." + "fa_icon": "fas fa-arrows-alt-h", + "help_text": "By default, data from both RNA-seq and Microarray platforms are downloaded. Setting this parameter applies a filter to get data from only one of the two platforms. This filter is only used while fetching appropriate Expression atlas / GEO accessions. It will not filter accessions provided directly by the user." }, "accessions_only": { "type": "boolean", "description": "Only get accessions from Expression Atlas / GEO and exit.", - "fa_icon": "fas fa-id-card", + "fa_icon": "far fa-stop-circle", "help_text": "Use this option if you only want to get Expression Atlas accessions and skip the rest of the pipeline." }, "download_only": { "type": "boolean", "description": "Only get accessions from Expression Atlas / GEO and download the selected datasets.", - "fa_icon": "fas fa-id-card", + "fa_icon": "far fa-stop-circle", "help_text": "Use this option if you only want to get Expression Atlas / GEO accessions, download the selected data, and skip the rest of the pipeline." }, "email": { @@ -94,15 +93,15 @@ "type": "string", "pattern": "([A-Z0-9-]+,?)+", "description": "Expression Atlas accession(s) to include", - "fa_icon": "fas fa-id-card", - "help_text": "Provide directly in command line Expression Atlas accession(s) (separated by commas) that you want to download. Example: `--eatlas_accessions E-MTAB-552,E-GEOD-61690`. Combine with --skip_fetch_eatlas_accessions if you want only these accessions to be used." + "fa_icon": "fas fa-address-card", + "help_text": "Provide Expression Atlas accession(s) that you want to download. The accessions should be comma-separated. Example: `--eatlas_accessions E-MTAB-552,E-GEOD-61690`. Combine with --skip_fetch_eatlas_accessions if you want only these accessions to be used." }, "eatlas_accessions_file": { "type": "string", "format": "file-path", "exists": true, "description": "File containing Expression Atlas accession(s) to download", - "fa_icon": "fas fa-id-card", + "fa_icon": "fas fa-file", "help_text": "File containing Expression Atlas accession(s) that you want to download. One accession per line. Example: `--eatlas_accessions_file included_accessions.txt`. Combine with --skip_fetch_eatlas_accessions if you want only these accessions to be used." }, "exclude_eatlas_accessions": { @@ -110,14 +109,14 @@ "pattern": "([A-Z0-9-]+,?)+", "description": "Expression Atlas accession(s) to exclude", "fa_icon": "fas fa-id-card", - "help_text": "Provide directly in command line Expression Atlas accessions (separated by commas) that you want to exclude. Example: `--exclude_eatlas_accessions E-MTAB-552,E-GEOD-61690`" + "help_text": "Provide Expression Atlas accession(s) that you want to exclude. The accessions should be comma-separated. Example: `--exclude_eatlas_accessions E-MTAB-552,E-GEOD-61690`" }, "exclude_eatlas_accessions_file": { "type": "string", "format": "file-path", "exists": true, "description": "File containing Expression Atlas accession(s) to exclude", - "fa_icon": "fas fa-id-card", + "fa_icon": "fas fa-file", "help_text": "File containing Expression Atlas accession(s) that you want to exclude. One accession per line. Example: `--exclude_eatlas_accessions_file excluded_accessions.txt`." } } @@ -139,14 +138,14 @@ "pattern": "([A-Z0-9-]+,?)+", "description": "GEO accession(s) to include", "fa_icon": "fas fa-id-card", - "help_text": "Provide directly in command line GEO series accession(s) (separated by commas) that you want to download. Example: `--geo_accessions GSE8165,GSE8161`. Combine with --skip_fetch_geo_accessions if you want only these accessions to be used." + "help_text": "Provide GEO accessions that you want to download. The accessions should be comma-separated. Example: `--geo_accessions GSE8165,GSE8161`. Combine with --skip_fetch_geo_accessions if you want only these accessions to be used." }, "geo_accessions_file": { "type": "string", "format": "file-path", "exists": true, "description": "File containing GEO accession(s) to download", - "fa_icon": "fas fa-id-card", + "fa_icon": "fas fa-file", "help_text": "File containing GEO series accession(s) that you want to download. One accession per line. Example: `--geo_accessions_file included_accessions.txt`. Combine with --skip_fetch_geo_accessions if you want only these accessions to be used." }, "exclude_geo_accessions": { @@ -154,14 +153,14 @@ "pattern": "([A-Z0-9-]+,?)+", "description": "GEO accession(s) to exclude", "fa_icon": "fas fa-id-card", - "help_text": "Provide directly in command line GEO series accessions (separated by commas) that you want to exclude. Example: `--exclude_geo_accessions GSE8165,GSE8161`" + "help_text": "Provide GEO accessions that you want to exclude. The accessions should be comma-separated. Example: `--exclude_geo_accessions GSE8165,GSE8161`" }, "exclude_geo_accessions_file": { "type": "string", "format": "file-path", "exists": true, "description": "File containing GEO accession(s) to exclude", - "fa_icon": "fas fa-id-card", + "fa_icon": "fas fa-file", "help_text": "File containing GEO series accession(s) that you want to exclude. One accession per line. Example: `--exclude_geo_accessions_file excluded_accessions.txt`." } } @@ -187,7 +186,7 @@ "pattern": "^\\S+\\.(csv|dat)$", "description": "Custom gene id mapping file", "help_text": "Path to comma-separated file containing custom gene id mappings. Each row represents a mapping from the original gene ID in your count datasets to the ensembl ID in g:Profiler. The mapping file should be a comma-separated file with 2 columns (original_gene_id and ensembl_gene_id) and a header row.", - "fa_icon": "fas fa-file-csv" + "fa_icon": "fas fa-file" }, "gene_metadata": { "type": "string", @@ -198,7 +197,7 @@ "pattern": "^\\S+\\.(csv|dat)$", "description": "Custom gene metadata file", "help_text": "Path to comma-separated file containing custom gene metadata information. Each row represents a gene and links its ensembl gene ID to its name and description. The metadata file should be a comma-separated file with 3 columns (ensembl_gene_id, name and description) and a header row.", - "fa_icon": "fas fa-file-csv" + "fa_icon": "fas fa-file" } } }, @@ -210,24 +209,24 @@ "properties": { "normalisation_method": { "type": "string", - "description": "Tool to use for normalisation", - "fa_icon": "fas fa-chart-simple", + "description": "Count normalisation method", + "fa_icon": "fas fa-divide", "enum": ["deseq2", "edger"], "default": "deseq2", - "help_text": "Raw RNAseq data must be normalised before further processing. You can select the package used for normalisation." + "help_text": "Raw RNAseq data must be normalised before further processing. You can select the R package used for normalisation." }, "quantile_normalisation_target_distribution": { "type": "string", - "description": "Target distribution to map to during quantile normalisation", - "fa_icon": "fas fa-chart-simple", - "enum": ["normal", "uniform"], + "description": "Target distribution for quantile normalisation", + "fa_icon": "fas fa-chart-bar", + "enum": ["uniform", "normal"], "default": "uniform", - "help_text": "All sample counts get quantile normalised and mapped to a specific distribution so that subsequent can compare them. The pipeline uses scikit-learn's quantile_transform function. You can select the target distribution to map counts to." + "help_text": "In order to compare counts between samples and different datasets, all normalised counts are quantile normalised and mapped to a specific distribution. The pipeline uses scikit-learn's quantile_transform function. You can select the target distribution to map counts to." }, "ks_pvalue_threshold": { "type": "number", "description": "Threshold for KS p-value for considering samples counts as a uniform distribution", - "fa_icon": "fas fa-chart-simple", + "fa_icon": "fas fa-battery-three-quarters", "maximum": 1, "default": 0, "help_text": "P-value threshold for the Kolmogorov-Smirnov test of samples counts against a uniform distribution. Samples showing a p-value equal or below this threshold are considered not uniform and will therefore not be considered for computation of the stability score. Examples: `0`, `'0.05'`, `'1E-27'`. Provide a negative value to disable this filter. By default, all samples showing a pvalue of 0 will be discarded." @@ -235,7 +234,7 @@ "min_expr_threshold": { "type": "number", "description": "Minimum percentage of quantile expression level", - "fa_icon": "fas fa-chart-bar", + "fa_icon": "fas fa-battery-three-quarters", "minimum": 0, "maximum": 1, "default": 0.2, @@ -251,33 +250,33 @@ "properties": { "candidate_selection_descriptor": { "type": "string", - "description": "Statistic descriptor for prior gene candidate selection", - "fa_icon": "fas fa-chart-simple", + "description": "Statistic descriptor for prior gene candidate selection. Either standard deviation (std), coefficient of variation (cv), or median absolute deviation (mad).", + "fa_icon": "far fa-chart-bar", "enum": ["std", "cv", "mad"], "default": "std", "help_text": "Candidate genes are chosen based on a certain statistical descriptor. Set this parameter to modify the descriptor used." }, + "nb_top_gene_candidates": { + "type": "integer", + "description": "Number of candidate genes to keep for stability scoring", + "fa_icon": "fas fa-sort-numeric-up-alt", + "minimum": 1, + "default": 5000, + "help_text": "Number of candidate genes to keep in the final list. These candidates genes are chosen as the ones showing the least standard variation." + }, "run_genorm": { "type": "boolean", "description": "Run Genorm", - "fa_icon": "fas fa-ban", - "help": "Genorm is NOT run by default. To run and get additional information about gene stability, set this parameter." + "fa_icon": "fas fa-check", + "help": "Genorm is NOT run by default. To run and get additional information about gene stability, set this parameter to true." }, "stability_score_weights": { "type": "string", "description": "Weights for stability score calculation", - "fa_icon": "fas fa-chart-simple", + "fa_icon": "fas fa-balance-scale", "default": "0.7,0.1,0.1,0.1", "help_text": "Weights for Standard deviation / Median absolute deviation / Normfinder / Genorm respectively. Must be a comma-separated string. Example: 0.7,0.1,0.1,0.1", "pattern": "^\\d+(\\.\\d+)?,\\d+(\\.\\d+)?,\\d+(\\.\\d+)?,\\d+(\\.\\d+)?$" - }, - "nb_top_gene_candidates": { - "type": "integer", - "description": "Number of candidate genes to keep for stability scoring", - "fa_icon": "fas fa-chart-simple", - "minimum": 1, - "default": 5000, - "help_text": "Number of candidate genes to keep in the final list. These candidates genes are chosen as the ones showing the least standard variation. Default is 1000." } } }, @@ -445,6 +444,9 @@ { "$ref": "#/$defs/expression_atlas_options" }, + { + "$ref": "#/$defs/geo_dataset_options" + }, { "$ref": "#/$defs/idmapping_options" }, From d591b37110a0d8e9633a9c4f90928d0df306c0f6 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Fri, 31 Oct 2025 08:31:57 +0100 Subject: [PATCH 116/258] update galaxy tooling for first publishing of downloadable galaxy tool --- galaxy/build/build_tool.py | 2 +- galaxy/build/static/boilerplate.xml | 18 +++++++++--------- galaxy/lint | 4 +++- galaxy/serve | 6 ++++-- galaxy/test | 4 ++-- galaxy/{tool => tool_shed}/.shed.yml | 2 +- .../tool/nf_core_stableexpression.xml | 16 +++++++--------- .../tool/rebuild_samplesheet.py | 0 .../tool/test_data}/input.csv | 0 .../tool/test_data}/microarray.normalised.csv | 0 .../microarray.normalised.design.csv | 0 .../tool/test_data}/rnaseq.raw.csv | 2 +- .../tool/test_data}/rnaseq.raw.design.csv | 0 13 files changed, 28 insertions(+), 26 deletions(-) rename galaxy/{tool => tool_shed}/.shed.yml (97%) rename galaxy/{ => tool_shed}/tool/nf_core_stableexpression.xml (96%) rename galaxy/{ => tool_shed}/tool/rebuild_samplesheet.py (100%) rename galaxy/{tool/test-data => tool_shed/tool/test_data}/input.csv (100%) rename galaxy/{tool/test-data => tool_shed/tool/test_data}/microarray.normalised.csv (100%) rename galaxy/{tool/test-data => tool_shed/tool/test_data}/microarray.normalised.design.csv (100%) rename galaxy/{tool/test-data => tool_shed/tool/test_data}/rnaseq.raw.csv (75%) rename galaxy/{tool/test-data => tool_shed/tool/test_data}/rnaseq.raw.design.csv (100%) diff --git a/galaxy/build/build_tool.py b/galaxy/build/build_tool.py index 202d2116..9ba1454f 100755 --- a/galaxy/build/build_tool.py +++ b/galaxy/build/build_tool.py @@ -9,7 +9,7 @@ logger = logging.getLogger(__name__) tool_boilerplate_file = Path(__file__).parent / "static/boilerplate.xml" -tool_file = Path(__file__).parents[1] / "tool/nf_core_{}.xml" +tool_file = Path(__file__).parents[1] / "tool_shed/tool/nf_core_{}.xml" def main(): diff --git a/galaxy/build/static/boilerplate.xml b/galaxy/build/static/boilerplate.xml index 1eccca0a..4f1ef82d 100644 --- a/galaxy/build/static/boilerplate.xml +++ b/galaxy/build/static/boilerplate.xml @@ -1,8 +1,8 @@ DESCRIPTION - nextflow - apptainer + nextflow + micromamba openjdk - - - + + +
    @@ -111,9 +111,9 @@ INPUTS - - - + + +
    diff --git a/galaxy/lint b/galaxy/lint index 5417d1cb..dd141d2f 100755 --- a/galaxy/lint +++ b/galaxy/lint @@ -1,6 +1,8 @@ #!/bin/bash galaxy_dir="$(dirname $(readlink -f "$0"))" -tool_file="${galaxy_dir}/tool/nf_core_stableexpression.xml" +tool_file="${galaxy_dir}/tool_shed/tool/nf_core_stableexpression.xml" planemo lint $tool_file + +planemo shed_lint tool_shed/tool --tools diff --git a/galaxy/serve b/galaxy/serve index ea0e649d..019e9ee0 100755 --- a/galaxy/serve +++ b/galaxy/serve @@ -1,8 +1,10 @@ #!/usr/bin/env bash galaxy_dir="$(dirname $(readlink -f "$0"))" -tool_dir="${galaxy_dir}/tool" +tool_dir="${galaxy_dir}/tool_shed/tool" planemo serve \ - --no_cleanup \ $tool_dir + +# add --no_cleanup to keep the pipelines workdirs after a run +# very useful for debugging diff --git a/galaxy/test b/galaxy/test index 05036eb9..2a511baf 100755 --- a/galaxy/test +++ b/galaxy/test @@ -1,9 +1,9 @@ #!/usr/bin/env bash galaxy_dir="$(dirname $(readlink -f "$0"))" -tool_file="${galaxy_dir}/tool/nf_core_stableexpression.xml" +tool_file="${galaxy_dir}/tool_shed/tool/nf_core_stableexpression.xml" -TEST_OUTDIR="test_ouptut" +TEST_OUTDIR="tests/output" ARGS="$@" diff --git a/galaxy/tool/.shed.yml b/galaxy/tool_shed/.shed.yml similarity index 97% rename from galaxy/tool/.shed.yml rename to galaxy/tool_shed/.shed.yml index 8c97037c..1cc55908 100644 --- a/galaxy/tool/.shed.yml +++ b/galaxy/tool_shed/.shed.yml @@ -9,5 +9,5 @@ long_description: | It takes as input a species name (mandatory), keywords for expression atlas search (optional) and / or a CSV input file listing local raw / normalised count datasets (optional). A typical usage is to find the most suitable qPCR housekeeping genes for a specific species (and optionally specific conditions). name: nf_core_stableexpression -owner: olivier_coen +owner: ocoen remote_repository_url: https://github.com/OlivierCoen/stableexpression/ diff --git a/galaxy/tool/nf_core_stableexpression.xml b/galaxy/tool_shed/tool/nf_core_stableexpression.xml similarity index 96% rename from galaxy/tool/nf_core_stableexpression.xml rename to galaxy/tool_shed/tool/nf_core_stableexpression.xml index 39b39c4e..e82d7d1a 100644 --- a/galaxy/tool/nf_core_stableexpression.xml +++ b/galaxy/tool_shed/tool/nf_core_stableexpression.xml @@ -3,7 +3,6 @@ nextflow micromamba - fuse-overlayfs openjdk - - - + + +
    @@ -264,9 +262,9 @@ VERSION="1.0dev"; echo "$VERSION" - - - + + +
    diff --git a/galaxy/tool/rebuild_samplesheet.py b/galaxy/tool_shed/tool/rebuild_samplesheet.py similarity index 100% rename from galaxy/tool/rebuild_samplesheet.py rename to galaxy/tool_shed/tool/rebuild_samplesheet.py diff --git a/galaxy/tool/test-data/input.csv b/galaxy/tool_shed/tool/test_data/input.csv similarity index 100% rename from galaxy/tool/test-data/input.csv rename to galaxy/tool_shed/tool/test_data/input.csv diff --git a/galaxy/tool/test-data/microarray.normalised.csv b/galaxy/tool_shed/tool/test_data/microarray.normalised.csv similarity index 100% rename from galaxy/tool/test-data/microarray.normalised.csv rename to galaxy/tool_shed/tool/test_data/microarray.normalised.csv diff --git a/galaxy/tool/test-data/microarray.normalised.design.csv b/galaxy/tool_shed/tool/test_data/microarray.normalised.design.csv similarity index 100% rename from galaxy/tool/test-data/microarray.normalised.design.csv rename to galaxy/tool_shed/tool/test_data/microarray.normalised.design.csv diff --git a/galaxy/tool/test-data/rnaseq.raw.csv b/galaxy/tool_shed/tool/test_data/rnaseq.raw.csv similarity index 75% rename from galaxy/tool/test-data/rnaseq.raw.csv rename to galaxy/tool_shed/tool/test_data/rnaseq.raw.csv index 4d558cc2..a9a6bdb4 100644 --- a/galaxy/tool/test-data/rnaseq.raw.csv +++ b/galaxy/tool_shed/tool/test_data/rnaseq.raw.csv @@ -1,4 +1,4 @@ -ensembl_gene_id,ESM1528575,ESM1528576,ESM1528579,ESM1528583,ESM1528584,ESM1528585,ESM1528580,ESM1528586,ESM1528582,ESM1528578,ESM1528581,ESM1528577 +,ESM1528575,ESM1528576,ESM1528579,ESM1528583,ESM1528584,ESM1528585,ESM1528580,ESM1528586,ESM1528582,ESM1528578,ESM1528581,ESM1528577 ENSRNA049453121,1,82,8,82,4,68,88,73,46,57,25,22 ENSRNA049453138,68,93,41,84,36,18,28,92,84,85,92,32 ENSRNA049454388,38,10,0,23,11,17,95,57,25,82,10,70 diff --git a/galaxy/tool/test-data/rnaseq.raw.design.csv b/galaxy/tool_shed/tool/test_data/rnaseq.raw.design.csv similarity index 100% rename from galaxy/tool/test-data/rnaseq.raw.design.csv rename to galaxy/tool_shed/tool/test_data/rnaseq.raw.design.csv From ad6e58717d341c21329ff340005e8842e4640547 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Fri, 31 Oct 2025 16:12:33 +0100 Subject: [PATCH 117/258] fix null stability score when normfinder issues null score --- bin/compute_stability_scores.py | 19 ++++++++++++++++--- bin/normfinder.py | 21 +++++++++++++-------- modules/local/normfinder/main.nf | 7 +++++++ 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/bin/compute_stability_scores.py b/bin/compute_stability_scores.py index 18b7e80c..7e80b8b5 100755 --- a/bin/compute_stability_scores.py +++ b/bin/compute_stability_scores.py @@ -56,6 +56,10 @@ def quantile_normalise(data: pl.Series, new_name: str) -> pl.Series: normalised_array = transformer.fit_transform(array) return pl.Series(new_name, normalised_array.ravel()) + @staticmethod + def get_normalsed_col(col: str) -> str: + return f"{col}_normalised" + def compute_stability_score(self): logger.info("Computing stability score for candidate genes") @@ -75,7 +79,7 @@ def compute_stability_score(self): data = candidate_df.select(col).to_series() # for each column present, we quantile normalise the data to have values between 0 and 1 # and put these normalised data in another column suffixed with "_normalised" - normalised_col = f"{col}_normalised" + normalised_col = self.get_normalsed_col(col) normalised_data[col] = self.quantile_normalise( data, new_name=normalised_col ) @@ -111,8 +115,17 @@ def compute_stability_score(self): if col not in self.df.columns: logger.warning(f"Column {col} not found in dataframe") continue - normalised_col = f"{col}_normalised" - stability_scoring_expr += pl.col(normalised_col) * weight / weight_sum + normalised_col = self.get_normalsed_col(col) + # we do not want to include null / nan values in the stability score calculation + # because this would result in a total null / nan value for the stability score + stability_scoring_expr += ( + pl.when( + pl.col(normalised_col).is_not_null() + & pl.col(normalised_col).is_not_nan() + ) + .then(pl.col(normalised_col)) + .otherwise(pl.lit(0)) + ) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/bin/normfinder.py b/bin/normfinder.py index 2464a107..faf4a64a 100755 --- a/bin/normfinder.py +++ b/bin/normfinder.py @@ -8,7 +8,6 @@ from pathlib import Path from tqdm import tqdm from dataclasses import dataclass, field -from typing import ClassVar from statistics import mean import numpy as np from numba import njit, prange @@ -102,7 +101,7 @@ class NormFinder: genes: list[str] = field(init=False) - group_to_samples_dict: dict[str, list] = field(init=False) + group_to_samples_dict: dict[str, list[str]] = field(init=False) n_groups: int = field(init=False) n_genes: int = field(init=False) @@ -326,7 +325,9 @@ def get_unbiased_intragroup_variances(self): mean(group_overall_means), ) - def adjust_for_nb_of_samples_in_groups(self, unbiased_intragroup_variance_df): + def adjust_for_nb_of_samples_in_groups( + self, unbiased_intragroup_variance_df: pl.DataFrame + ): n_samples_list = [ len(samples) for samples in self.group_to_samples_dict.values() ] @@ -335,7 +336,7 @@ def adjust_for_nb_of_samples_in_groups(self, unbiased_intragroup_variance_df): ).select([(pl.col(c) / pl.col("n_samples")).alias(c) for c in self.genes]) def get_unbiased_intergroup_variance( - self, gene_means_in_groups_df, dataset_overall_mean + self, gene_means_in_groups_df: pl.DataFrame, dataset_overall_mean: float ): mean_over_genes = ( gene_means_in_groups_df.mean() @@ -371,7 +372,7 @@ def get_unbiased_intergroup_variance( ) # square to get variance ) - def compute_gamma_factor(self, diff_df, vardiff_df): + def compute_gamma_factor(self, diff_df: pl.DataFrame, vardiff_df: pl.DataFrame): logger.info("Computing gamma factor") first_term = ( diff_df.with_columns( @@ -400,15 +401,19 @@ def compute_gamma_factor(self, diff_df, vardiff_df): .item() ) - return max(first_term - second_term, 0) + return max(first_term - second_term, 0) # set to 0 if negative @staticmethod - def apply_gamma_factor(gamma, diff_df, vardiff_df): + def apply_gamma_factor( + gamma: float, diff_df: pl.DataFrame, vardiff_df: pl.DataFrame + ): difnew = diff_df * gamma / (gamma + vardiff_df) varnew = vardiff_df + gamma * vardiff_df / (gamma + vardiff_df) return difnew, varnew - def apply_shrinkage(self, intergroup_variance_df, group_mean_variance_df): + def apply_shrinkage( + self, intergroup_variance_df: pl.DataFrame, group_mean_variance_df: pl.DataFrame + ): gamma = self.compute_gamma_factor( intergroup_variance_df, group_mean_variance_df ) diff --git a/modules/local/normfinder/main.nf b/modules/local/normfinder/main.nf index 48094ca4..2e0dd1b9 100644 --- a/modules/local/normfinder/main.nf +++ b/modules/local/normfinder/main.nf @@ -2,6 +2,13 @@ process NORMFINDER { label 'process_high' + errorStrategy { + if (task.exitStatus == 100) { + log.warn("Too few genes to run NormFinder.") + return 'ignore' + } + } + conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0e/0e0445114887dd260f1632afe116b1e81e02e1acc74a86adca55099469b490d9/data': From 6882ace167d9d1b3f0d524f4316fa78d25a7d84d Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sat, 1 Nov 2025 14:31:33 +0100 Subject: [PATCH 118/258] place all errorStrategy statements in config files --- conf/modules.config | 1 + conf/modules/cleaning.config | 24 +++++++++++ conf/modules/expression_atlas.config | 40 ++++++++++++++++++- conf/modules/genorm.config | 2 +- conf/modules/geo.config | 39 +++++++++++++++++- conf/modules/id_mapping.config | 2 +- conf/modules/merging.config | 14 +++---- conf/modules/normalisation.config | 32 ++++++++++----- conf/modules/normfinder.config | 22 +++++++++- modules/local/clean_count_data/main.nf | 13 +----- modules/local/compute_base_statistics/main.nf | 11 +++++ modules/local/expressionatlas/getdata/main.nf | 35 ---------------- modules/local/geo/getdata/main.nf | 34 ---------------- modules/local/gprofiler/idmapping/main.nf | 11 ++++- modules/local/merge/designs/main.nf | 24 ----------- modules/local/merge/designs/spec-file.txt | 39 ------------------ .../{merge/counts => merge_counts}/main.nf | 2 +- .../counts => merge_counts}/spec-file.txt | 0 modules/local/normalisation/deseq2/main.nf | 4 -- modules/local/normfinder/main.nf | 7 ---- subworkflows/local/merge_data/main.nf | 6 +-- 21 files changed, 179 insertions(+), 183 deletions(-) create mode 100644 conf/modules/cleaning.config delete mode 100644 modules/local/merge/designs/main.nf delete mode 100644 modules/local/merge/designs/spec-file.txt rename modules/local/{merge/counts => merge_counts}/main.nf (95%) rename modules/local/{merge/counts => merge_counts}/spec-file.txt (100%) diff --git a/conf/modules.config b/conf/modules.config index d34043cf..ed3a4c56 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -23,6 +23,7 @@ includeConfig 'modules/expression_atlas.config' includeConfig 'modules/geo.config' includeConfig 'modules/id_mapping.config' includeConfig 'modules/normalisation.config' +includeConfig 'modules/cleaning.config' includeConfig 'modules/merging.config' includeConfig 'modules/genorm.config' includeConfig 'modules/normfinder.config' diff --git a/conf/modules/cleaning.config b/conf/modules/cleaning.config new file mode 100644 index 00000000..fdc36341 --- /dev/null +++ b/conf/modules/cleaning.config @@ -0,0 +1,24 @@ +process { + + withName: CLEAN_COUNT_DATA { + + errorStrategy = { + if (task.exitStatus == 101) { + return 'ignore' + } else if ( task.exitStatus in ((130..145) + 104 + 175) ) { // override default behaviour to sleep some time before retry + // in case of OOM errors, we wait a bit and try again (2 retries) + if ( task.attempt <= 2) { + sleep(Math.pow(2, task.attempt) * 2000 as long) + return 'retry' + } else { + log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") + return 'ignore' + } + } else { + return 'finish' + } + } + + } + +} diff --git a/conf/modules/expression_atlas.config b/conf/modules/expression_atlas.config index 72c34cbf..13504ed1 100644 --- a/conf/modules/expression_atlas.config +++ b/conf/modules/expression_atlas.config @@ -1,17 +1,53 @@ process { - withName: 'EXPRESSIONATLAS_GETACCESSIONS' { + withName: EXPRESSIONATLAS_GETACCESSIONS { publishDir = [ path: { "${params.outdir}/expression_atlas/accessions/" }, mode: params.publish_dir_mode ] } - withName: 'EXPRESSIONATLAS_GETDATA' { + withName: EXPRESSIONATLAS_GETDATA { + publishDir = [ path: { "${params.outdir}/expression_atlas/datasets/" }, mode: params.publish_dir_mode ] + + maxForks = 8 // limiting to 8 threads at a time to avoid 429 errors with the Expression Atlas API server + + errorStrategy = { + if (task.exitStatus == 100) { + // ignoring accessions that cannot be retrieved from Expression Atlas (the script throws a 100 in this case) + // sometimes, some datasets are transiently unavailable from Expression Atlas: + // we ignore them as there is no point in trying again and again + // they will be available again soon but we can't know when + // for some other files, they are simply unavailable for good... + log.warn("Could not retrieve data for accession ${accession}. This could be a transient network issue or a permission error.") + return 'ignore' + } else if (task.exitStatus == 101) { + // some datasets are not associated with experiment summary + // we ignore them as there they would be useless for us + log.warn("Failure to download whole dataset for accession ${accession}. No experiment summary found.") + return 'ignore' + } else if (task.exitStatus == 102) { + // unhandled error: we print an extra message to warn the user + log.warn("Unhandled error occurred with accession: ${accession}") + return 'ignore' + } else if ( task.exitStatus in ((130..145) + 104 + 175) ) { // override default behaviour to sleep some time before retry + // in case of OOM errors, we wait a bit and try again (2 retries) + if ( task.attempt <= 2) { + sleep(Math.pow(2, task.attempt) * 2000 as long) + return 'retry' + } else { + log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") + return 'ignore' + } + } else { + return 'ignore' + } + } + } } diff --git a/conf/modules/genorm.config b/conf/modules/genorm.config index 8025d038..17bd973b 100644 --- a/conf/modules/genorm.config +++ b/conf/modules/genorm.config @@ -1,6 +1,6 @@ process { - withName: 'COMPUTE_M_MEASURE' { + withName: COMPUTE_M_MEASURE { publishDir = [ path: { "${params.outdir}/stability_scoring/genorm/" }, mode: params.publish_dir_mode diff --git a/conf/modules/geo.config b/conf/modules/geo.config index 70be03ec..4be02c34 100644 --- a/conf/modules/geo.config +++ b/conf/modules/geo.config @@ -1,17 +1,52 @@ process { - withName: 'GEO_GETACCESSIONS' { + withName: GEO_GETACCESSIONS { publishDir = [ path: { "${params.outdir}/geo/accessions/" }, mode: params.publish_dir_mode ] } - withName: 'GEO_GETDATA' { + withName: GEO_GETDATA { + publishDir = [ path: { "${params.outdir}/geo/datasets/" }, mode: params.publish_dir_mode ] + + maxForks = 8 // limiting to 8 threads at a time to avoid 429 errors with the Expression Atlas API server + + errorStrategy = { + if (task.exitStatus == 100) { + // ignoring accessions that cannot be retrieved from GEO + log.warn("Could not retrieve data for accession ${accession}. This could be a transient network issue or a permission error.") + return 'ignore' + } else if (task.exitStatus == 101) { + log.warn("GEO dataset with accession ${accession} contains multiple files.") + return 'ignore' + } else if (task.exitStatus == 110) { + log.warn("GEO dataset for accession ${accession} does not seem normalised.") + return 'ignore' + } else if (task.exitStatus == 111) { + log.warn("GEO dataset for accession ${accession} seems normalised but not log-transformed.") + return 'ignore' + } else if (task.exitStatus == 112) { + log.warn("GEO dataset for accession ${accession} are of unclear origin. Could not infer normalisation state.") + return 'ignore' + } else if ( task.exitStatus in ((130..145) + 104 + 175) ) { // override default behaviour to sleep some time before retry + // in case of OOM errors, we wait a bit and try again (2 retries) + if ( task.attempt <= 2) { + sleep(Math.pow(2, task.attempt) * 2000 as long) + return 'retry' + } else { + log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") + return 'ignore' + } + } else { + return 'ignore' + } + } + } } diff --git a/conf/modules/id_mapping.config b/conf/modules/id_mapping.config index 50e59957..f06c486b 100644 --- a/conf/modules/id_mapping.config +++ b/conf/modules/id_mapping.config @@ -1,6 +1,6 @@ process { - withName: 'GPROFILER_IDMAPPING' { + withName: GPROFILER_IDMAPPING { publishDir = [ path: { "${params.outdir}/idmapping/datasets/" }, mode: params.publish_dir_mode diff --git a/conf/modules/merging.config b/conf/modules/merging.config index ce76a569..7e1b3e8d 100644 --- a/conf/modules/merging.config +++ b/conf/modules/merging.config @@ -1,46 +1,46 @@ process { - withName: 'MERGE_COUNTS' { + withName: MERGE_COUNTS { cpus = 12 maxRetries = 5 } - withName: 'MERGE_RNASEQ_COUNTS' { + withName: MERGE_RNASEQ_COUNTS { publishDir = [ path: { "${params.outdir}/merged_datasets/rnaseq/" }, mode: params.publish_dir_mode ] } - withName: 'MERGE_MICROARRAY_COUNTS' { + withName: MERGE_MICROARRAY_COUNTS { publishDir = [ path: { "${params.outdir}/merged_datasets/microarray/" }, mode: params.publish_dir_mode ] } - withName: 'MERGE_ALL_COUNTS' { + withName: MERGE_ALL_COUNTS { publishDir = [ path: { "${params.outdir}/merged_datasets/all/" }, mode: params.publish_dir_mode ] } - withName: 'COMPUTE_BASE_STATISTICS_FOR_RNASEQ' { + withName: COMPUTE_BASE_STATISTICS_FOR_RNASEQ { publishDir = [ path: { "${params.outdir}/base_statistics/rnaseq/" }, mode: params.publish_dir_mode ] } - withName: 'COMPUTE_BASE_STATISTICS_FOR_MICROARRAY' { + withName: COMPUTE_BASE_STATISTICS_FOR_MICROARRAY { publishDir = [ path: { "${params.outdir}/base_statistics/microarray/" }, mode: params.publish_dir_mode ] } - withName: 'COMPUTE_BASE_STATISTICS' { + withName: COMPUTE_BASE_STATISTICS { publishDir = [ path: { "${params.outdir}/base_statistics/all/" }, mode: params.publish_dir_mode diff --git a/conf/modules/normalisation.config b/conf/modules/normalisation.config index 93b9e197..85e609ed 100644 --- a/conf/modules/normalisation.config +++ b/conf/modules/normalisation.config @@ -1,20 +1,34 @@ process { - withName: 'NORMALISATION_DESEQ2' { - publishDir = [ - path: { "${params.outdir}/normalised/${meta.dataset}/deseq2/" }, - mode: params.publish_dir_mode - ] - } + withName: 'NORMALISATION_DESEQ2|NORMALISATION_EDGER' { - withName: 'NORMALISATION_EDGER' { publishDir = [ - path: { "${params.outdir}/normalised/${meta.dataset}/edger/" }, + path: { "${params.outdir}/normalised/${meta.dataset}/${task.process.tokenize(':')[-1].toLowerCase()}/" }, mode: params.publish_dir_mode ] + + errorStrategy = { + if (task.exitStatus == 100) { + // ignoring cases when the count dataframe gets empty after filtering (the script throws a 100 in this case) + // the subsequent steps will not be run for this dataset + log.warn("No genes left after pre-filtering for dataset ${meta.dataset}.") + return 'ignore' + } else if ( task.exitStatus in ((130..145) + 104 + 175) ) { // override default behaviour to sleep some time before retry + // in case of OOM errors, we wait a bit and try again (2 retries) + if ( task.attempt <= 2) { + sleep(Math.pow(2, task.attempt) * 2000 as long) + return 'retry' + } else { + log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") + return 'ignore' + } + } else { + return 'ignore' + } + } } - withName: 'QUANTILE_NORMALISATION' { + withName: QUANTILE_NORMALISATION { publishDir = [ path: { "${params.outdir}/quantile_normalised/${meta.dataset}/" }, mode: params.publish_dir_mode diff --git a/conf/modules/normfinder.config b/conf/modules/normfinder.config index 3ad9ac5d..b9ffff45 100644 --- a/conf/modules/normfinder.config +++ b/conf/modules/normfinder.config @@ -1,10 +1,30 @@ process { - withName: 'NORMFINDER' { + withName: NORMFINDER { + publishDir = [ path: { "${params.outdir}/stability_scoring/normfinder/" }, mode: params.publish_dir_mode ] + + errorStrategy = { + if (task.exitStatus == 100) { + log.warn("Too few genes to run NormFinder.") + return 'ignore' + } else if ( task.exitStatus in ((130..145) + 104 + 175) ) { // override default behaviour to sleep some time before retry + // in case of OOM errors, we wait a bit and try again (2 retries) + if ( task.attempt <= 2) { + sleep(Math.pow(2, task.attempt) * 2000 as long) + return 'retry' + } else { + log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") + return 'ignore' + } + } else { + return 'ignore' + } + } + } } diff --git a/modules/local/clean_count_data/main.nf b/modules/local/clean_count_data/main.nf index 59c454fd..9fbe4a7b 100644 --- a/modules/local/clean_count_data/main.nf +++ b/modules/local/clean_count_data/main.nf @@ -2,18 +2,7 @@ process CLEAN_COUNT_DATA { label 'process_single' - errorStrategy { - if (task.exitStatus == 101) { - /* - log.warning( - "No more valid sample after checking p-value of Kolmogorow-Smirnoff test against target distribution! " - + "You can try a more flexible approach by setting again the value of the ks_pvalue_threshold parameter. " - + "Provide a negative value to disable this filter." - ) - */ - return 'ignore' - } - } + tag "${meta.dataset}" conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? diff --git a/modules/local/compute_base_statistics/main.nf b/modules/local/compute_base_statistics/main.nf index 1c0b485d..42969691 100644 --- a/modules/local/compute_base_statistics/main.nf +++ b/modules/local/compute_base_statistics/main.nf @@ -6,6 +6,17 @@ process COMPUTE_BASE_STATISTICS { if (task.exitStatus == 100) { log.error("No count could be found before merging datasets! Please check the provided accessions and datasets and run again") return 'terminate' + } else if ( task.exitStatus in ((130..145) + 104 + 175) ) { // override default behaviour to sleep some time before retry + // in case of OOM errors, we wait a bit and try again (2 retries) + if ( task.attempt <= 2) { + sleep(Math.pow(2, task.attempt) * 2000 as long) + return 'retry' + } else { + log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") + return 'ignore' + } + } else { + return 'ignore' } } diff --git a/modules/local/expressionatlas/getdata/main.nf b/modules/local/expressionatlas/getdata/main.nf index 6c5101f1..046eddad 100644 --- a/modules/local/expressionatlas/getdata/main.nf +++ b/modules/local/expressionatlas/getdata/main.nf @@ -2,43 +2,8 @@ process EXPRESSIONATLAS_GETDATA { label 'process_single' - // limiting to 8 threads at a time to avoid 429 errors with the Expression Atlas API server - maxForks 8 - tag "$accession" - errorStrategy { - if (task.exitStatus == 100) { - // ignoring accessions that cannot be retrieved from Expression Atlas (the script throws a 100 in this case) - // sometimes, some datasets are transiently unavailable from Expression Atlas: - // we ignore them as there is no point in trying again and again - // they will be available again soon but we can't know when - // for some other files, they are simply unavailable for good... - log.warn("Could not retrieve data for accession ${accession}. This could be a transient network issue or a permission error.") - return 'ignore' - } else if (task.exitStatus == 101) { - // some datasets are not associated with experiment summary - // we ignore them as there they would be useless for us - log.warn("Failure to download whole dataset for accession ${accession}. No experiment summary found.") - return 'ignore' - } else if (task.exitStatus == 102) { - // unhandled error: we print an extra message to warn the user - log.warn("Unhandled error occurred with accession: ${accession}") - return 'ignore' - } else if (task.exitStatus == 137) { // override default behaviour to sleep some time before retry - // in case of OOM errors, we wait a bit and try again (2 retries) - if ( task.attempt <= 2) { - sleep(Math.pow(2, task.attempt) * 2000 as long) - return 'retry' - } else { - log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") - return 'ignore' - } - } else { - return 'terminate' - } - } - conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/7f/7fd21450c3a3f7df37fa0480170780019e9686be319da1c9e10712f7f17cca26/data': diff --git a/modules/local/geo/getdata/main.nf b/modules/local/geo/getdata/main.nf index d6e5173b..d3994dbe 100644 --- a/modules/local/geo/getdata/main.nf +++ b/modules/local/geo/getdata/main.nf @@ -2,42 +2,8 @@ process GEO_GETDATA { label 'process_single' - // limiting to 8 threads at a time to avoid 429 errors with the Expression Atlas API server - maxForks 8 - tag "$accession" - errorStrategy { - if (task.exitStatus == 100) { - // ignoring accessions that cannot be retrieved from GEO - log.warn("Could not retrieve data for accession ${accession}. This could be a transient network issue or a permission error.") - return 'ignore' - } else if (task.exitStatus == 101) { - log.warn("GEO dataset with accession ${accession} contains multiple files.") - return 'ignore' - } else if (task.exitStatus == 110) { - log.warn("GEO dataset for accession ${accession} does not seem normalised.") - return 'ignore' - } else if (task.exitStatus == 111) { - log.warn("GEO dataset for accession ${accession} seems normalised but not log-transformed.") - return 'ignore' - } else if (task.exitStatus == 112) { - log.warn("GEO dataset for accession ${accession} are of unclear origin. Could not infer normalisation state.") - return 'ignore' - } else if (task.exitStatus == 137) { // override default behaviour to sleep some time before retry - // in case of OOM errors, we wait a bit and try again (2 retries) - if ( task.attempt <= 2) { - sleep(Math.pow(2, task.attempt) * 2000 as long) - return 'retry' - } else { - log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") - return 'ignore' - } - } else { - return 'terminate' - } - } - conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/4c/4cb08d96e62942e7b6288abf2cfd30e813521a022459700e610325a3a7c0b1c8/data': diff --git a/modules/local/gprofiler/idmapping/main.nf b/modules/local/gprofiler/idmapping/main.nf index 237d64e3..01b3c2af 100644 --- a/modules/local/gprofiler/idmapping/main.nf +++ b/modules/local/gprofiler/idmapping/main.nf @@ -20,8 +20,17 @@ process GPROFILER_IDMAPPING { // if the server appears to be down, we stop immediately log.error("gProfiler server appears to be down, stopping pipeline") return 'terminate' + } else if ( task.exitStatus in ((130..145) + 104 + 175) ) { // override default behaviour to sleep some time before retry + // in case of OOM errors, we wait a bit and try again (2 retries) + if ( task.attempt <= 2) { + sleep(Math.pow(2, task.attempt) * 2000 as long) + return 'retry' + } else { + log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") + return 'ignore' + } } else { - return 'terminate' + return 'finish' } } diff --git a/modules/local/merge/designs/main.nf b/modules/local/merge/designs/main.nf deleted file mode 100644 index eec246e3..00000000 --- a/modules/local/merge/designs/main.nf +++ /dev/null @@ -1,24 +0,0 @@ -process MERGE_DESIGNS { - - label 'process_high' - - conda "${moduleDir}/spec-file.txt" - container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? - 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/f1/f1c30725ef181337de8749d5b54eacb1a8e1f97ac5e43fe15ec34a61789a7320/data': - 'community.wave.seqera.io/library/pandas:2.3.2--baef3004955c4a32' }" - - input: - path design_files, stageAs: "?/*" - - output: - path 'whole_design.csv', emit: design - tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions - tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions - - script: - """ - merge_designs.py \\ - --designs "$design_files" - """ - -} diff --git a/modules/local/merge/designs/spec-file.txt b/modules/local/merge/designs/spec-file.txt deleted file mode 100644 index 51228f95..00000000 --- a/modules/local/merge/designs/spec-file.txt +++ /dev/null @@ -1,39 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_5.conda#dcd5ff1940cd38f6df777cac86819d60 -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_5.conda#264fbfba7fb20acf3b29cde153e345ce -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda#74784ee3d225fc3dca89edb635b4e5cc -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda#0be7c6e070c19105f966d3758448d018 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_5.conda#fbd4008644add05032b6764807ee2cba -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_5.conda#0c91408b3dec0b97e8a3c694845bd63b -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_2.conda#dfc5aae7b043d9f56ba99514d5e60625 -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-35_h4a7cf45_openblas.conda#6da7e852c812a84096b68158574398d0 -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-35_h0358290_openblas.conda#8aa3389d36791ecd31602a247b1f3641 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda#4211416ecba1866fab0c6470986c22d6 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-35_h47877c9_openblas.conda#aa0b36b71d44f74686f13b9bfabec891 -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda#0b367fad34931cb79e0d6b7e5c06bb1c -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_5.conda#4e02a49aaa9d5190cb630fa43528fbe6 -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.1-he9a06e4_0.conda#af930c65e9a79a3423d6d36e265cef65 -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda#ffffb341206dd0dab0c36053c048d621 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.13.7-h2b335a9_100_cp313.conda#724dcf9960e933838247971da07fe5cf -https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.3-py313hf6604e3_0.conda#3122d20dc438287e125fb5acff1df170 -https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda#3339e3b65d58accf4ca4fb8748ab16b3 -https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 -https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 -https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 -https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.2-py313h08cd8bf_0.conda#5f4cc42e08d6d862b7b919a3c8959e0b -https://conda.anaconda.org/conda-forge/noarch/pip-25.2-pyh145f28c_0.conda#e7ab34d5a93e0819b62563c78635d937 diff --git a/modules/local/merge/counts/main.nf b/modules/local/merge_counts/main.nf similarity index 95% rename from modules/local/merge/counts/main.nf rename to modules/local/merge_counts/main.nf index 4844b440..e488a01b 100644 --- a/modules/local/merge/counts/main.nf +++ b/modules/local/merge_counts/main.nf @@ -1,6 +1,6 @@ process MERGE_COUNTS { - memory { def calc = (dataset_size / 10000).toInteger() + memory = { def calc = (dataset_size / 10000).toInteger() def result = Math.max(1, calc) // Ensure at least 1 MB def multiplicator = 1 + 0.2 * task.attempt // increase memory usage with each attempt by 20% return 1.MB * result * multiplicator diff --git a/modules/local/merge/counts/spec-file.txt b/modules/local/merge_counts/spec-file.txt similarity index 100% rename from modules/local/merge/counts/spec-file.txt rename to modules/local/merge_counts/spec-file.txt diff --git a/modules/local/normalisation/deseq2/main.nf b/modules/local/normalisation/deseq2/main.nf index f792b91b..60f5deee 100644 --- a/modules/local/normalisation/deseq2/main.nf +++ b/modules/local/normalisation/deseq2/main.nf @@ -4,10 +4,6 @@ process NORMALISATION_DESEQ2 { tag "${meta.dataset}" - // ignoring cases when the count dataframe gets empty after filtering (the script throws a 100 in this case) - // the subsequent steps will not be run for this dataset - errorStrategy { task.exitStatus == 100 ? 'ignore' : 'terminate' } - conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/ce/cef7164b168e74e5db11dcd9acf6172d47ed6753e4814c68f39835d0c6c22f6d/data': diff --git a/modules/local/normfinder/main.nf b/modules/local/normfinder/main.nf index 2e0dd1b9..48094ca4 100644 --- a/modules/local/normfinder/main.nf +++ b/modules/local/normfinder/main.nf @@ -2,13 +2,6 @@ process NORMFINDER { label 'process_high' - errorStrategy { - if (task.exitStatus == 100) { - log.warn("Too few genes to run NormFinder.") - return 'ignore' - } - } - conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0e/0e0445114887dd260f1632afe116b1e81e02e1acc74a86adca55099469b490d9/data': diff --git a/subworkflows/local/merge_data/main.nf b/subworkflows/local/merge_data/main.nf index c2dd3265..e70aaf1e 100644 --- a/subworkflows/local/merge_data/main.nf +++ b/subworkflows/local/merge_data/main.nf @@ -1,6 +1,6 @@ -include { MERGE_COUNTS as MERGE_ALL_COUNTS } from '../../../modules/local/merge/counts' -include { MERGE_COUNTS as MERGE_RNASEQ_COUNTS } from '../../../modules/local/merge/counts' -include { MERGE_COUNTS as MERGE_MICROARRAY_COUNTS } from '../../../modules/local/merge/counts' +include { MERGE_COUNTS as MERGE_ALL_COUNTS } from '../../../modules/local/merge_counts' +include { MERGE_COUNTS as MERGE_RNASEQ_COUNTS } from '../../../modules/local/merge_counts' +include { MERGE_COUNTS as MERGE_MICROARRAY_COUNTS } from '../../../modules/local/merge_counts' include { getWholeDatasetSize } from '../../../subworkflows/local/utils_nfcore_stableexpression_pipeline' From 72a1238521f77b97c67e0ef749a7bde2d95d0093 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sun, 2 Nov 2025 12:16:39 +0100 Subject: [PATCH 119/258] fix issue with platform selection in get_geo_dataset_accessions.py --- bin/get_geo_dataset_accessions.py | 70 +++++++++++++++++++------------ 1 file changed, 43 insertions(+), 27 deletions(-) diff --git a/bin/get_geo_dataset_accessions.py b/bin/get_geo_dataset_accessions.py index 9e02a040..55cc1363 100755 --- a/bin/get_geo_dataset_accessions.py +++ b/bin/get_geo_dataset_accessions.py @@ -416,6 +416,11 @@ def probe_ids_can_be_converted( platform_dict_list = dataset_metadata["platform_metadata"] all_probe_ids = [] + acc = dataset_metadata["accession"] + tmp_file = f"tmp/{acc}.txt" + Path("tmp").mkdir(exist_ok=True) + Path(tmp_file).touch() + for platform_dict in platform_dict_list: # looping until we find data for our species if format_species(platform_dict["taxon"]) != format_species(species): @@ -434,6 +439,8 @@ def probe_ids_can_be_converted( # if at least one ID could be converted can_be_converted = True if mapping_dict else False + + Path(tmp_file).unlink() return dataset_metadata, can_be_converted @@ -624,6 +631,9 @@ def contains_only_rna(molecules_types: list, accession: str) -> bool: def contains_proper_experiment_type( experiment_types: list, accession: str, platform: str ) -> bool: + experiment_types = ( + experiment_types if isinstance(experiment_types, list) else [experiment_types] + ) for experiment_type in experiment_types: # if at least one experiment type is ok, we keep this dataset if GEO_EXPERIMENT_TYPE_TO_PLATFORM.get(experiment_type) == platform: @@ -778,7 +788,7 @@ def main(): # EXCLUDING DATASETS WITH THE WRONG SPECIES # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - logger.info("Excluding wrong species") + logger.info(f"Excluding wrong species for {len(dataset_metadata_list)} datasets") good_species_dataset_metadata_list = [ dataset for dataset in dataset_metadata_list @@ -786,21 +796,21 @@ def main(): ] export_filtered_out_datasets_if_any( - original_dataset_metadata_list=dataset_metadata_list, - filtered_dataset_metadata_list=good_species_dataset_metadata_list, - filtered_out_outfile_name=WRONG_SPECIES_DATASETS_METADATA_OUTFILE_NAME, - filtered_feature="species", + dataset_metadata_list, + good_species_dataset_metadata_list, + WRONG_SPECIES_DATASETS_METADATA_OUTFILE_NAME, + "species", ) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # PARSING METADATA # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - logger.info("Parsing metadata") + logger.info(f"Parsing metadata for {len(dataset_metadata_list)} datasets") augmented_dataset_metadata_list = [] with ( Pool(processes=args.nb_cpus) as p, - tqdm(total=len(dataset_metadata_list)) as pbar, + tqdm(total=len(good_species_dataset_metadata_list)) as pbar, ): for result in p.imap_unordered( parse_metadata, good_species_dataset_metadata_list @@ -815,7 +825,7 @@ def main(): # CHECKING MOLECULE TYPE / PLATFORM TECHNOLOGIES # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - logger.info("Validating datasets") + logger.info(f"Validating {len(augmented_dataset_metadata_list)} datasets") specs_filtered_metadata_list = [ metadata for metadata in augmented_dataset_metadata_list @@ -823,10 +833,10 @@ def main(): ] export_filtered_out_datasets_if_any( - original_dataset_metadata_list=augmented_dataset_metadata_list, - filtered_dataset_metadata_list=specs_filtered_metadata_list, - filtered_out_outfile_name=WRONG_SPECS_DATASETS_METADATA_OUTFILE_NAME, - filtered_feature="molecule type / platform technology", + augmented_dataset_metadata_list, + specs_filtered_metadata_list, + WRONG_SPECS_DATASETS_METADATA_OUTFILE_NAME, + "molecule type / platform technology", ) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -834,7 +844,9 @@ def main(): # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if args.keywords: - logger.info(f"Filtering experiments with keywords {args.keywords}") + logger.info( + f"Filtering experiments with keywords {args.keywords} for {len(specs_filtered_metadata_list)} datasets" + ) func = partial(filter_metadata_with_keywords, keywords=args.keywords) keywords_filtered_metadata_list = [] @@ -850,10 +862,10 @@ def main(): keywords_filtered_metadata_list.append(result) export_filtered_out_datasets_if_any( - original_dataset_metadata_list=specs_filtered_metadata_list, - filtered_dataset_metadata_list=keywords_filtered_metadata_list, - filtered_out_outfile_name=WRONG_KEYWORDS_DATASETS_METADATA_OUTFILE_NAME, - filtered_feature="keywords", + specs_filtered_metadata_list, + keywords_filtered_metadata_list, + WRONG_KEYWORDS_DATASETS_METADATA_OUTFILE_NAME, + "keywords", ) else: @@ -863,7 +875,9 @@ def main(): # GETTING METADATA OF SEQUENCING PLATFORMS # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - logger.info("Getting platform metadata") + logger.info( + f"Getting platform metadata for {len(keywords_filtered_metadata_list)} datasets" + ) platform_augmented_dataset_metadata_list = [] for selected_metadata_chunk_list in tqdm( chunk_list(keywords_filtered_metadata_list, PLATFORM_METADATA_CHUNKSIZE) @@ -873,10 +887,10 @@ def main(): ) export_filtered_out_datasets_if_any( - original_dataset_metadata_list=keywords_filtered_metadata_list, - filtered_dataset_metadata_list=platform_augmented_dataset_metadata_list, - filtered_out_outfile_name=PLATFORM_NOT_AVAILABLE_DATASETS_METADATA_OUTFILE_NAME, - filtered_feature="platform metadata", + keywords_filtered_metadata_list, + platform_augmented_dataset_metadata_list, + PLATFORM_NOT_AVAILABLE_DATASETS_METADATA_OUTFILE_NAME, + "platform metadata", ) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -884,7 +898,9 @@ def main(): # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # this cannot be done in parallel because it requires HTTP requests - logger.info("Checking gene ID mapping issues") + logger.info( + f"Checking gene ID mapping issues for {len(platform_augmented_dataset_metadata_list)} datasets" + ) func = partial(probe_ids_can_be_converted, species=args.species) final_metadata_list = [] @@ -901,10 +917,10 @@ def main(): final_metadata_list.append(metadata) export_filtered_out_datasets_if_any( - original_dataset_metadata_list=platform_augmented_dataset_metadata_list, - filtered_dataset_metadata_list=final_metadata_list, - filtered_out_outfile_name=GENE_ID_MAPPING_ISSUES_DATASETS_METADATA_OUTFILE_NAME, - filtered_feature="gene id mapping", + platform_augmented_dataset_metadata_list, + final_metadata_list, + GENE_ID_MAPPING_ISSUES_DATASETS_METADATA_OUTFILE_NAME, + "gene id mapping", ) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 4eae685833ed5d95394fe0768ae3eeada4a0927a Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sun, 2 Nov 2025 14:27:39 +0100 Subject: [PATCH 120/258] put errorStrategy and other configs back in processes to avoid errors --- bin/get_geo_dataset_accessions.py | 7 -- bin/merge_designs.py | 65 ------------------- conf/modules.config | 4 +- conf/modules/cleaning.config | 24 ------- conf/modules/expression_atlas.config | 34 ---------- conf/modules/geo.config | 35 ---------- conf/modules/merging.config | 5 -- conf/modules/normalisation.config | 21 ------ conf/modules/normfinder.config | 30 --------- .../modules/{genorm.config => scoring.config} | 7 ++ modules/local/clean_count_data/main.nf | 17 +++++ modules/local/expressionatlas/getdata/main.nf | 34 ++++++++++ modules/local/geo/getaccessions/main.nf | 2 +- modules/local/geo/getdata/main.nf | 33 ++++++++++ modules/local/merge_counts/main.nf | 6 +- modules/local/normalisation/deseq2/main.nf | 20 ++++++ modules/local/normalisation/edger/main.nf | 22 ++++++- modules/local/normfinder/main.nf | 18 +++++ 18 files changed, 155 insertions(+), 229 deletions(-) delete mode 100755 bin/merge_designs.py delete mode 100644 conf/modules/cleaning.config delete mode 100644 conf/modules/normfinder.config rename conf/modules/{genorm.config => scoring.config} (52%) diff --git a/bin/get_geo_dataset_accessions.py b/bin/get_geo_dataset_accessions.py index 55cc1363..3f1cf955 100755 --- a/bin/get_geo_dataset_accessions.py +++ b/bin/get_geo_dataset_accessions.py @@ -416,11 +416,6 @@ def probe_ids_can_be_converted( platform_dict_list = dataset_metadata["platform_metadata"] all_probe_ids = [] - acc = dataset_metadata["accession"] - tmp_file = f"tmp/{acc}.txt" - Path("tmp").mkdir(exist_ok=True) - Path(tmp_file).touch() - for platform_dict in platform_dict_list: # looping until we find data for our species if format_species(platform_dict["taxon"]) != format_species(species): @@ -439,8 +434,6 @@ def probe_ids_can_be_converted( # if at least one ID could be converted can_be_converted = True if mapping_dict else False - - Path(tmp_file).unlink() return dataset_metadata, can_be_converted diff --git a/bin/merge_designs.py b/bin/merge_designs.py deleted file mode 100755 index 1bb124a1..00000000 --- a/bin/merge_designs.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python3 - -# Written by Olivier Coen. Released under the MIT license. - -import argparse -import pandas as pd -from pathlib import Path -import logging - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -OUTFILENAME = "whole_design.csv" - - -##################################################### -##################################################### -# FUNCTIONS -##################################################### -##################################################### - - -def parse_args(): - parser = argparse.ArgumentParser( - description="Merge count datasets" - ) - parser.add_argument( - "--designs", type=str, dest="design_files", required=True, help="Design files" - ) - return parser.parse_args() - - -def merge_designs(design_files): - dfs = [pd.read_csv(file) for file in design_files] - return pd.concat(dfs, ignore_index=True) - - -##################################################### -# EXPORT -##################################################### - - -def export_data(design_df: pd.DataFrame ): - logger.info(f"Exporting normalised counts to: {OUTFILENAME}") - design_df.to_csv(OUTFILENAME, index=False, header=True) - - -##################################################### -##################################################### -# MAIN -##################################################### -##################################################### - - -def main(): - args = parse_args() - design_files = [Path(file) for file in args.design_files.split(" ")] - - # putting all designs into a single dataframe - design_df = merge_designs(design_files) - export_data(design_df) - - -if __name__ == "__main__": - main() diff --git a/conf/modules.config b/conf/modules.config index ed3a4c56..3626e0af 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -23,8 +23,6 @@ includeConfig 'modules/expression_atlas.config' includeConfig 'modules/geo.config' includeConfig 'modules/id_mapping.config' includeConfig 'modules/normalisation.config' -includeConfig 'modules/cleaning.config' includeConfig 'modules/merging.config' -includeConfig 'modules/genorm.config' -includeConfig 'modules/normfinder.config' +includeConfig 'modules/scoring.config' includeConfig 'modules/qc.config' diff --git a/conf/modules/cleaning.config b/conf/modules/cleaning.config deleted file mode 100644 index fdc36341..00000000 --- a/conf/modules/cleaning.config +++ /dev/null @@ -1,24 +0,0 @@ -process { - - withName: CLEAN_COUNT_DATA { - - errorStrategy = { - if (task.exitStatus == 101) { - return 'ignore' - } else if ( task.exitStatus in ((130..145) + 104 + 175) ) { // override default behaviour to sleep some time before retry - // in case of OOM errors, we wait a bit and try again (2 retries) - if ( task.attempt <= 2) { - sleep(Math.pow(2, task.attempt) * 2000 as long) - return 'retry' - } else { - log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") - return 'ignore' - } - } else { - return 'finish' - } - } - - } - -} diff --git a/conf/modules/expression_atlas.config b/conf/modules/expression_atlas.config index 13504ed1..454de041 100644 --- a/conf/modules/expression_atlas.config +++ b/conf/modules/expression_atlas.config @@ -14,40 +14,6 @@ process { mode: params.publish_dir_mode ] - maxForks = 8 // limiting to 8 threads at a time to avoid 429 errors with the Expression Atlas API server - - errorStrategy = { - if (task.exitStatus == 100) { - // ignoring accessions that cannot be retrieved from Expression Atlas (the script throws a 100 in this case) - // sometimes, some datasets are transiently unavailable from Expression Atlas: - // we ignore them as there is no point in trying again and again - // they will be available again soon but we can't know when - // for some other files, they are simply unavailable for good... - log.warn("Could not retrieve data for accession ${accession}. This could be a transient network issue or a permission error.") - return 'ignore' - } else if (task.exitStatus == 101) { - // some datasets are not associated with experiment summary - // we ignore them as there they would be useless for us - log.warn("Failure to download whole dataset for accession ${accession}. No experiment summary found.") - return 'ignore' - } else if (task.exitStatus == 102) { - // unhandled error: we print an extra message to warn the user - log.warn("Unhandled error occurred with accession: ${accession}") - return 'ignore' - } else if ( task.exitStatus in ((130..145) + 104 + 175) ) { // override default behaviour to sleep some time before retry - // in case of OOM errors, we wait a bit and try again (2 retries) - if ( task.attempt <= 2) { - sleep(Math.pow(2, task.attempt) * 2000 as long) - return 'retry' - } else { - log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") - return 'ignore' - } - } else { - return 'ignore' - } - } - } } diff --git a/conf/modules/geo.config b/conf/modules/geo.config index 4be02c34..17971c46 100644 --- a/conf/modules/geo.config +++ b/conf/modules/geo.config @@ -8,45 +8,10 @@ process { } withName: GEO_GETDATA { - publishDir = [ path: { "${params.outdir}/geo/datasets/" }, mode: params.publish_dir_mode ] - - maxForks = 8 // limiting to 8 threads at a time to avoid 429 errors with the Expression Atlas API server - - errorStrategy = { - if (task.exitStatus == 100) { - // ignoring accessions that cannot be retrieved from GEO - log.warn("Could not retrieve data for accession ${accession}. This could be a transient network issue or a permission error.") - return 'ignore' - } else if (task.exitStatus == 101) { - log.warn("GEO dataset with accession ${accession} contains multiple files.") - return 'ignore' - } else if (task.exitStatus == 110) { - log.warn("GEO dataset for accession ${accession} does not seem normalised.") - return 'ignore' - } else if (task.exitStatus == 111) { - log.warn("GEO dataset for accession ${accession} seems normalised but not log-transformed.") - return 'ignore' - } else if (task.exitStatus == 112) { - log.warn("GEO dataset for accession ${accession} are of unclear origin. Could not infer normalisation state.") - return 'ignore' - } else if ( task.exitStatus in ((130..145) + 104 + 175) ) { // override default behaviour to sleep some time before retry - // in case of OOM errors, we wait a bit and try again (2 retries) - if ( task.attempt <= 2) { - sleep(Math.pow(2, task.attempt) * 2000 as long) - return 'retry' - } else { - log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") - return 'ignore' - } - } else { - return 'ignore' - } - } - } } diff --git a/conf/modules/merging.config b/conf/modules/merging.config index 7e1b3e8d..831c633a 100644 --- a/conf/modules/merging.config +++ b/conf/modules/merging.config @@ -1,10 +1,5 @@ process { - withName: MERGE_COUNTS { - cpus = 12 - maxRetries = 5 - } - withName: MERGE_RNASEQ_COUNTS { publishDir = [ path: { "${params.outdir}/merged_datasets/rnaseq/" }, diff --git a/conf/modules/normalisation.config b/conf/modules/normalisation.config index 85e609ed..b9e4321c 100644 --- a/conf/modules/normalisation.config +++ b/conf/modules/normalisation.config @@ -1,31 +1,10 @@ process { withName: 'NORMALISATION_DESEQ2|NORMALISATION_EDGER' { - publishDir = [ path: { "${params.outdir}/normalised/${meta.dataset}/${task.process.tokenize(':')[-1].toLowerCase()}/" }, mode: params.publish_dir_mode ] - - errorStrategy = { - if (task.exitStatus == 100) { - // ignoring cases when the count dataframe gets empty after filtering (the script throws a 100 in this case) - // the subsequent steps will not be run for this dataset - log.warn("No genes left after pre-filtering for dataset ${meta.dataset}.") - return 'ignore' - } else if ( task.exitStatus in ((130..145) + 104 + 175) ) { // override default behaviour to sleep some time before retry - // in case of OOM errors, we wait a bit and try again (2 retries) - if ( task.attempt <= 2) { - sleep(Math.pow(2, task.attempt) * 2000 as long) - return 'retry' - } else { - log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") - return 'ignore' - } - } else { - return 'ignore' - } - } } withName: QUANTILE_NORMALISATION { diff --git a/conf/modules/normfinder.config b/conf/modules/normfinder.config deleted file mode 100644 index b9ffff45..00000000 --- a/conf/modules/normfinder.config +++ /dev/null @@ -1,30 +0,0 @@ -process { - - withName: NORMFINDER { - - publishDir = [ - path: { "${params.outdir}/stability_scoring/normfinder/" }, - mode: params.publish_dir_mode - ] - - errorStrategy = { - if (task.exitStatus == 100) { - log.warn("Too few genes to run NormFinder.") - return 'ignore' - } else if ( task.exitStatus in ((130..145) + 104 + 175) ) { // override default behaviour to sleep some time before retry - // in case of OOM errors, we wait a bit and try again (2 retries) - if ( task.attempt <= 2) { - sleep(Math.pow(2, task.attempt) * 2000 as long) - return 'retry' - } else { - log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") - return 'ignore' - } - } else { - return 'ignore' - } - } - - } - -} diff --git a/conf/modules/genorm.config b/conf/modules/scoring.config similarity index 52% rename from conf/modules/genorm.config rename to conf/modules/scoring.config index 17bd973b..c5bfe09e 100644 --- a/conf/modules/genorm.config +++ b/conf/modules/scoring.config @@ -1,5 +1,12 @@ process { + withName: NORMFINDER { + publishDir = [ + path: { "${params.outdir}/stability_scoring/normfinder/" }, + mode: params.publish_dir_mode + ] + } + withName: COMPUTE_M_MEASURE { publishDir = [ path: { "${params.outdir}/stability_scoring/genorm/" }, diff --git a/modules/local/clean_count_data/main.nf b/modules/local/clean_count_data/main.nf index 9fbe4a7b..bd25f3ce 100644 --- a/modules/local/clean_count_data/main.nf +++ b/modules/local/clean_count_data/main.nf @@ -4,6 +4,23 @@ process CLEAN_COUNT_DATA { tag "${meta.dataset}" + errorStrategy { + if (task.exitStatus == 101) { + return 'ignore' + } else if ( task.exitStatus in ((130..145) + 104 + 175) ) { // override default behaviour to sleep some time before retry + // in case of OOM errors, we wait a bit and try again (2 retries) + if ( task.attempt <= 2) { + sleep(Math.pow(2, task.attempt) * 2000 as long) + return 'retry' + } else { + log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") + return 'ignore' + } + } else { + return 'finish' + } + } + conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': diff --git a/modules/local/expressionatlas/getdata/main.nf b/modules/local/expressionatlas/getdata/main.nf index 046eddad..75c2d91b 100644 --- a/modules/local/expressionatlas/getdata/main.nf +++ b/modules/local/expressionatlas/getdata/main.nf @@ -4,6 +4,40 @@ process EXPRESSIONATLAS_GETDATA { tag "$accession" + maxForks 8 // limiting to 8 threads at a time to avoid 429 errors with the Expression Atlas API server + + errorStrategy { + if (task.exitStatus == 100) { + // ignoring accessions that cannot be retrieved from Expression Atlas (the script throws a 100 in this case) + // sometimes, some datasets are transiently unavailable from Expression Atlas: + // we ignore them as there is no point in trying again and again + // they will be available again soon but we can't know when + // for some other files, they are simply unavailable for good... + log.warn("Could not retrieve data for accession ${accession}. This could be a transient network issue or a permission error.") + return 'ignore' + } else if (task.exitStatus == 101) { + // some datasets are not associated with experiment summary + // we ignore them as there they would be useless for us + log.warn("Failure to download whole dataset for accession ${accession}. No experiment summary found.") + return 'ignore' + } else if (task.exitStatus == 102) { + // unhandled error: we print an extra message to warn the user + log.warn("Unhandled error occurred with accession: ${accession}") + return 'ignore' + } else if ( task.exitStatus in ((130..145) + 104 + 175) ) { // override default behaviour to sleep some time before retry + // in case of OOM errors, we wait a bit and try again (2 retries) + if ( task.attempt <= 2) { + sleep(Math.pow(2, task.attempt) * 2000 as long) + return 'retry' + } else { + log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") + return 'ignore' + } + } else { + return 'ignore' + } + } + conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/7f/7fd21450c3a3f7df37fa0480170780019e9686be319da1c9e10712f7f17cca26/data': diff --git a/modules/local/geo/getaccessions/main.nf b/modules/local/geo/getaccessions/main.nf index 6081287c..091eb2ed 100644 --- a/modules/local/geo/getaccessions/main.nf +++ b/modules/local/geo/getaccessions/main.nf @@ -1,5 +1,5 @@ process GEO_GETACCESSIONS { - debug true + label 'process_high_cpus' conda "${moduleDir}/spec-file.txt" diff --git a/modules/local/geo/getdata/main.nf b/modules/local/geo/getdata/main.nf index d3994dbe..bafc68b9 100644 --- a/modules/local/geo/getdata/main.nf +++ b/modules/local/geo/getdata/main.nf @@ -4,6 +4,39 @@ process GEO_GETDATA { tag "$accession" + maxForks 8 // limiting to 8 threads at a time to avoid 429 errors with the Expression Atlas API server + + errorStrategy { + if (task.exitStatus == 100) { + // ignoring accessions that cannot be retrieved from GEO + log.warn("Could not retrieve data for accession ${accession}. This could be a transient network issue or a permission error.") + return 'ignore' + } else if (task.exitStatus == 101) { + log.warn("GEO dataset with accession ${accession} contains multiple files.") + return 'ignore' + } else if (task.exitStatus == 110) { + log.warn("GEO dataset for accession ${accession} does not seem normalised.") + return 'ignore' + } else if (task.exitStatus == 111) { + log.warn("GEO dataset for accession ${accession} seems normalised but not log-transformed.") + return 'ignore' + } else if (task.exitStatus == 112) { + log.warn("GEO dataset for accession ${accession} are of unclear origin. Could not infer normalisation state.") + return 'ignore' + } else if ( task.exitStatus in ((130..145) + 104 + 175) ) { // override default behaviour to sleep some time before retry + // in case of OOM errors, we wait a bit and try again (2 retries) + if ( task.attempt <= 2) { + sleep(Math.pow(2, task.attempt) * 2000 as long) + return 'retry' + } else { + log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") + return 'ignore' + } + } else { + return 'ignore' + } + } + conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/4c/4cb08d96e62942e7b6288abf2cfd30e813521a022459700e610325a3a7c0b1c8/data': diff --git a/modules/local/merge_counts/main.nf b/modules/local/merge_counts/main.nf index e488a01b..204eda7f 100644 --- a/modules/local/merge_counts/main.nf +++ b/modules/local/merge_counts/main.nf @@ -1,6 +1,10 @@ process MERGE_COUNTS { - memory = { def calc = (dataset_size / 10000).toInteger() + label "process_high" + + maxRetries 5 + + memory { def calc = (dataset_size / 10000).toInteger() def result = Math.max(1, calc) // Ensure at least 1 MB def multiplicator = 1 + 0.2 * task.attempt // increase memory usage with each attempt by 20% return 1.MB * result * multiplicator diff --git a/modules/local/normalisation/deseq2/main.nf b/modules/local/normalisation/deseq2/main.nf index 60f5deee..d47f2697 100644 --- a/modules/local/normalisation/deseq2/main.nf +++ b/modules/local/normalisation/deseq2/main.nf @@ -4,6 +4,26 @@ process NORMALISATION_DESEQ2 { tag "${meta.dataset}" + errorStrategy { + if (task.exitStatus == 100) { + // ignoring cases when the count dataframe gets empty after filtering (the script throws a 100 in this case) + // the subsequent steps will not be run for this dataset + log.warn("No genes left after pre-filtering for dataset ${meta.dataset}.") + return 'ignore' + } else if ( task.exitStatus in ((130..145) + 104 + 175) ) { // override default behaviour to sleep some time before retry + // in case of OOM errors, we wait a bit and try again (2 retries) + if ( task.attempt <= 2) { + sleep(Math.pow(2, task.attempt) * 2000 as long) + return 'retry' + } else { + log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") + return 'ignore' + } + } else { + return 'ignore' + } + } + conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/ce/cef7164b168e74e5db11dcd9acf6172d47ed6753e4814c68f39835d0c6c22f6d/data': diff --git a/modules/local/normalisation/edger/main.nf b/modules/local/normalisation/edger/main.nf index da1405ff..ef6b23a0 100644 --- a/modules/local/normalisation/edger/main.nf +++ b/modules/local/normalisation/edger/main.nf @@ -4,9 +4,25 @@ process NORMALISATION_EDGER { tag "${meta.dataset}" - // ignoring cases when the count dataframe gets empty after filtering (the script throws a 100 in this case) - // the subsequent steps will not be run for this dataset - errorStrategy { task.exitStatus == 100 ? 'ignore' : 'terminate' } + errorStrategy { + if (task.exitStatus == 100) { + // ignoring cases when the count dataframe gets empty after filtering (the script throws a 100 in this case) + // the subsequent steps will not be run for this dataset + log.warn("No genes left after pre-filtering for dataset ${meta.dataset}.") + return 'ignore' + } else if ( task.exitStatus in ((130..145) + 104 + 175) ) { // override default behaviour to sleep some time before retry + // in case of OOM errors, we wait a bit and try again (2 retries) + if ( task.attempt <= 2) { + sleep(Math.pow(2, task.attempt) * 2000 as long) + return 'retry' + } else { + log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") + return 'ignore' + } + } else { + return 'ignore' + } + } conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? diff --git a/modules/local/normfinder/main.nf b/modules/local/normfinder/main.nf index 48094ca4..797cadf2 100644 --- a/modules/local/normfinder/main.nf +++ b/modules/local/normfinder/main.nf @@ -2,6 +2,24 @@ process NORMFINDER { label 'process_high' + errorStrategy { + if (task.exitStatus == 100) { + log.warn("Too few genes to run NormFinder.") + return 'ignore' + } else if ( task.exitStatus in ((130..145) + 104 + 175) ) { // override default behaviour to sleep some time before retry + // in case of OOM errors, we wait a bit and try again (2 retries) + if ( task.attempt <= 2) { + sleep(Math.pow(2, task.attempt) * 2000 as long) + return 'retry' + } else { + log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") + return 'ignore' + } + } else { + return 'ignore' + } + } + conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0e/0e0445114887dd260f1632afe116b1e81e02e1acc74a86adca55099469b490d9/data': From b678f7ff49b12e563fe05b04c3a82dfcd542269a Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Mon, 3 Nov 2025 16:13:39 +0100 Subject: [PATCH 121/258] add robust coefficient of variation on median as possible candidate gene selector --- assets/multiqc_config.yml | 123 +++++++----------- bin/compute_base_statistics.py | 5 + bin/compute_stability_scores.py | 4 +- bin/config.py | 10 +- bin/get_candidate_genes.py | 2 +- modules/local/compute_base_statistics/main.nf | 2 +- nextflow.config | 2 +- nextflow_schema.json | 8 +- 8 files changed, 67 insertions(+), 89 deletions(-) diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index bfd2b9c6..fbbe2905 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -86,40 +86,40 @@ custom_data: Final stability score : the lower, the better format: "{:,.6f}" scale: "RdYlGn-rev" - variation_coefficient_normalised: - title: "Normalised coefficient of variation" + coefficient_of_variation_normalised: + title: "Normalised CV" description: | Quantile normalised (among candidate genes) coefficient of variation ( std(expression) / mean(expression) ) across all samples. format: "{:,.6f}" scale: "PRGn-rev" + robust_coefficient_of_variation_median_normalised: + title: "Normalised RCVm" + description: | + Quantile normalised (among candidate genes) robust coefficient of variation on median of the expression across all samples. + format: "{:,.4f}" + scale: "PRGn-rev" normfinder_stability_value_normalised: - title: "Normalised Normfinder stability value " + title: "Normalised Normfinder score" description: | Quantile normalised (among candidate genes) stability value as computed by Normfinder format: "{:,.6f}" scale: "PRGn-rev" genorm_m_measure_normalised: - title: "Normalised Genorm M-measure" + title: "Normalised Genorm score" description: | Quantile normalised (among candidate genes) M-measure as computed by Genorm format: "{:,.6f}" scale: "PRGn-rev" - median_absolute_deviation_normalised: - title: "Normalised median absolute deviation" + coefficient_of_variation: + title: "CV" description: | - Quantile normalised (among candidate genes) median absolute deviation of the expression across all samples. - format: "{:,.4f}" - scale: "PRGn-rev" - standard_deviation: - title: "Standard deviation" - description: | - Standard deviation of the expression across all samples. + Coefficient of variation ( std(expression) / mean(expression) ) across all samples. format: "{:,.6f}" - variation_coefficient: - title: "Coefficient of variation" + robust_coefficient_of_variation_median: + title: "RCVm" description: | - Variation coefficient ( std(expression) / mean(expression) ) across all samples. - format: "{:,.6f}" + Robust coefficient of variation on median of the expression across all samples. + format: "{:,.4f}" normfinder_stability_value: title: "Normfinder stability value " description: | @@ -135,13 +135,18 @@ custom_data: description: | Average expression across all samples. format: "{:,.4f}" + standard_deviation: + title: "Standard deviation" + description: | + Standard deviation of the expression across all samples. + format: "{:,.6f}" median: title: "Median" description: | Median expression across all samples. format: "{:,.4f}" median_absolute_deviation: - title: "Median absolute deviation" + title: "MAD" description: | Median absolute deviation of the expression across all samples. format: "{:,.4f}" @@ -165,20 +170,25 @@ custom_data: - s_eq: "Low expression" very_low: - s_eq: "Very low expression" - rnaseq_standard_deviation: - title: "Std [RNA-seq only]" + rnaseq_coefficient_of_variation: + title: "Var coeff [RNA-seq only]" description: | - Standard deviation of the expression across RNA-Seq samples. + Coefficient of variation ( std(expression) / mean(expression) ) across RNA-Seq samples. format: "{:,.4f}" - rnaseq_variation_coefficient: - title: "Var coeff [RNA-seq only]" + rnaseq_robust_coefficient_of_variation_median: + title: "RCVm [RNA-seq only]" description: | - Variation coefficient ( std(expression) / mean(expression) ) across RNA-Seq samples. + Robust coefficient of variation on median of the expression across RNA-Seq samples. format: "{:,.4f}" rnaseq_mean: title: "Average [RNA-seq only]" description: | - Average expression across samples. + Average expression across RNA-Seq samples. + format: "{:,.4f}" + rnaseq_standard_deviation: + title: "Std [RNA-seq only]" + description: | + Standard deviation of the expression across RNA-Seq samples. format: "{:,.4f}" rnaseq_median: title: "Median [RNA-seq only]" @@ -186,7 +196,7 @@ custom_data: Median expression across RNA-Seq samples. format: "{:,.4f}" rnaseq_median_absolute_deviation: - title: "Mad [RNA-seq only]" + title: "MAD [RNA-seq only]" description: | Median absolute deviation of the expression across RNA-Seq samples. format: "{:,.4f}" @@ -210,28 +220,33 @@ custom_data: - s_eq: "Low expression" very_low: - s_eq: "Very low expression" - microarray_standard_deviation: - title: "Std [Microarray only]" + microarray_coefficient_of_variation: + title: "Var coeff [Microarray only]" description: | - Standard deviation of the expression across Microarray samples. + Coefficient of variation ( std(expression) / mean(expression) ) across Microarray samples. format: "{:,.4f}" - microarray_variation_coefficient: - title: "Var coeff [Microarray only]" + microarray_robust_coefficient_of_variation_median: + title: "RCVm [Microarray only]" description: | - Variation coefficient ( std(expression) / mean(expression) ) across Microarray samples. + Robust coefficient of variation on median of the expression across Microarray samples. format: "{:,.4f}" microarray_mean: title: "Average [Microarray only]" description: | Average expression across Microarray samples. format: "{:,.4f}" + microarray_standard_deviation: + title: "Std [Microarray only]" + description: | + Standard deviation of the expression across Microarray samples. + format: "{:,.4f}" microarray_median: title: "Median [Microarray only]" description: | Median expression across Microarray samples. format: "{:,.4f}" microarray_median_absolute_deviation: - title: "Mad [Microarray only]" + title: "MAD [Microarray only]" description: | Median absolute deviation of the expression across Microarray samples. format: "{:,.4f}" @@ -314,86 +329,44 @@ custom_data: headers: stability_score: title: "Stability score" - description: | - Final stability score : the lower, the better - format: "{:,.6f}" color: "rgb(186,43,32)" variation_coefficient_normalised: title: "Normalised coefficient of variation" - description: | - Quantile normalised (among candidate genes) coefficient of variation ( std(expression) / mean(expression) ) across all samples. - format: "{:,.6f}" color: "rgb(64, 122, 22)" normfinder_stability_value_normalised: title: "Normalised Normfinder stability value " - description: | - Quantile normalised (among candidate genes) stability value as computed by Normfinder - format: "{:,.6f}" color: "rgb(64, 122, 22)" genorm_m_measure_normalised: title: "Normalised Genorm M-measure" - description: | - Quantile normalised (among candidate genes) M-measure as computed by Genorm - format: "{:,.6f}" color: "rgb(64, 122, 22)" median_absolute_deviation_normalised: title: "Normalised median absolute deviation" - description: | - Quantile normalised (among candidate genes) median absolute deviation of the expression across all samples. - format: "{:,.4f}" color: "rgb(64, 122, 22)" standard_deviation: title: "Standard deviation" - description: | - Standard deviation of the expression across all samples. - format: "{:,.6f}" color: "rgb(227, 210, 36)" variation_coefficient: title: "Coefficient of variation" - description: | - Variation coefficient ( std(expression) / mean(expression) ) across all samples. - format: "{:,.6f}" color: "rgb(227, 210, 36)" normfinder_stability_value: title: "Normfinder stability value " - description: | - Stability value as computed by Normfinder - format: "{:,.6f}" color: "rgb(227, 210, 36)" genorm_m_measure: title: "Genorm M-measure" - description: | - M-measure as computed by Genorm - format: "{:,.6f}" color: "rgb(227, 210, 36)" mean: title: "Average" - description: | - Average expression across all samples. - format: "{:,.4f}" color: "rgb(85, 23, 166)" median: title: "Median" - description: | - Median expression across all samples. - format: "{:,.4f}" color: "rgb(85, 23, 166)" median_absolute_deviation: title: "Median absolute deviation" - description: | - Median absolute deviation of the expression across all samples. - format: "{:,.4f}" color: "rgb(85, 23, 166)" rnaseq_standard_deviation: title: "Std [RNA-seq only]" - description: | - Standard deviation of the expression across RNA-Seq samples. - format: "{:,.4f}" rnaseq_variation_coefficient: title: "Var coeff [RNA-seq only]" - description: | - Variation coefficient ( std(expression) / mean(expression) ) across RNA-Seq samples. - format: "{:,.4f}" rnaseq_mean: title: "Average [RNA-seq only]" description: | diff --git a/bin/compute_base_statistics.py b/bin/compute_base_statistics.py index 86f3b3dd..70df4c06 100755 --- a/bin/compute_base_statistics.py +++ b/bin/compute_base_statistics.py @@ -17,6 +17,8 @@ # outfile names ALL_GENES_RESULT_OUTFILE_SUFFIX = "stats_all_genes.csv" +RCV_MULTIFILER = 1.4826 # see https://pmc.ncbi.nlm.nih.gov/articles/PMC9196089/ + ############################################################################ # POLARS EXTENSIONS @@ -137,6 +139,9 @@ def get_main_statistics(self) -> pl.LazyFrame: (pl.col("std") / pl.col("mean")).alias( self.get_colname(config.VARIATION_COEFFICIENT_COLNAME) ), + (pl.col("mad") / pl.col("median") * RCV_MULTIFILER).alias( + self.get_colname(config.ROBUST_COEFFICIENT_OF_VARIATION_MEDIAN_COLNAME) + ), ) def compute_ratios_null_values(self): diff --git a/bin/compute_stability_scores.py b/bin/compute_stability_scores.py index 7e80b8b5..23d8d4a0 100755 --- a/bin/compute_stability_scores.py +++ b/bin/compute_stability_scores.py @@ -25,7 +25,7 @@ class StabilityScorer: WEIGHT_FIELDS: ClassVar[list[str]] = [ config.VARIATION_COEFFICIENT_COLNAME, - config.MAD_COLNAME, + config.ROBUST_COEFFICIENT_OF_VARIATION_MEDIAN_COLNAME, config.NORMFINDER_STABILITY_VALUE_COLNAME, config.GENORM_M_MEASURE_COLNAME, ] @@ -184,7 +184,7 @@ def parse_args(): dest="stability_score_weights", type=str, required=True, - help="Weights for Standard deviation / Median absolute deviation / Normfinder / Genorm respectively. Must be a comma-separated string. Example: 0.7,0.1,0.1,0.1", + help="Weights for Coefficient of Variation / Robust Coefficient of Variation on Median / Normfinder / Genorm respectively. Must be a comma-separated string. Example: 0.7,0.1,0.1,0.1", ) return parser.parse_args() diff --git a/bin/config.py b/bin/config.py index 2e5331ca..0b51be6e 100644 --- a/bin/config.py +++ b/bin/config.py @@ -3,7 +3,10 @@ RANK_COLNAME = "rank" # base statistics -VARIATION_COEFFICIENT_COLNAME = "variation_coefficient" +VARIATION_COEFFICIENT_COLNAME = "coefficient_of_variation" +ROBUST_COEFFICIENT_OF_VARIATION_MEDIAN_COLNAME = ( + "robust_coefficient_of_variation_median" +) STANDARD_DEVIATION_COLNAME = "standard_deviation" STABILITY_SCORE_COLNAME = "stability_score" MEAN_COLNAME = "mean" @@ -35,9 +38,6 @@ RATIOS_STD_COLNAME = "ratios_stds" SCORING_BASE_TO_STABILITY_SCORE_COLUMN = { - "normfinder": NORMFINDER_STABILITY_VALUE_COLNAME, - "genorm": GENORM_M_MEASURE_COLNAME, - "std": STANDARD_DEVIATION_COLNAME, "cv": VARIATION_COEFFICIENT_COLNAME, - "mad": MAD_COLNAME, + "rcvm": ROBUST_COEFFICIENT_OF_VARIATION_MEDIAN_COLNAME, } diff --git a/bin/get_candidate_genes.py b/bin/get_candidate_genes.py index 87d48217..72763333 100755 --- a/bin/get_candidate_genes.py +++ b/bin/get_candidate_genes.py @@ -137,7 +137,7 @@ def main(): stat_lf = filter_out_low_expression_genes(stat_lf, args.min_pct_quantile_expr_level) # stat_lf = filter_out_genes_with_zero_counts(stat_lf) - # get base candidate genes based on the chosen statistical descriptor (std, mad, ...) + # get base candidate genes based on the chosen statistical descriptor (cv, rcvm) best_candidates = get_best_candidates( stat_lf, args.candidate_selection_descriptor, args.nb_top_stable_genes ) diff --git a/modules/local/compute_base_statistics/main.nf b/modules/local/compute_base_statistics/main.nf index 42969691..995967e3 100644 --- a/modules/local/compute_base_statistics/main.nf +++ b/modules/local/compute_base_statistics/main.nf @@ -16,7 +16,7 @@ process COMPUTE_BASE_STATISTICS { return 'ignore' } } else { - return 'ignore' + return 'terminate' } } diff --git a/nextflow.config b/nextflow.config index 74f3139c..e5842a29 100644 --- a/nextflow.config +++ b/nextflow.config @@ -49,7 +49,7 @@ params { // stability scoring run_genorm = false - candidate_selection_descriptor = "std" + candidate_selection_descriptor = "cv" stability_score_weights = "0.7,0.1,0.1,0.1" // Boilerplate options diff --git a/nextflow_schema.json b/nextflow_schema.json index 3970bfe7..82744e23 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -250,10 +250,10 @@ "properties": { "candidate_selection_descriptor": { "type": "string", - "description": "Statistic descriptor for prior gene candidate selection. Either standard deviation (std), coefficient of variation (cv), or median absolute deviation (mad).", + "description": "Statistic descriptor for prior gene candidate selection. Either coefficient of variation (cv), or robust median absolute deviation (rcvm).", "fa_icon": "far fa-chart-bar", - "enum": ["std", "cv", "mad"], - "default": "std", + "enum": ["cv", "rcvm"], + "default": "cv", "help_text": "Candidate genes are chosen based on a certain statistical descriptor. Set this parameter to modify the descriptor used." }, "nb_top_gene_candidates": { @@ -275,7 +275,7 @@ "description": "Weights for stability score calculation", "fa_icon": "fas fa-balance-scale", "default": "0.7,0.1,0.1,0.1", - "help_text": "Weights for Standard deviation / Median absolute deviation / Normfinder / Genorm respectively. Must be a comma-separated string. Example: 0.7,0.1,0.1,0.1", + "help_text": "Weights for Coefficient of Variation / Robust Coefficient of Variation on Median / Normfinder / Genorm respectively. Must be a comma-separated string. Example: 0.7,0.1,0.1,0.1", "pattern": "^\\d+(\\.\\d+)?,\\d+(\\.\\d+)?,\\d+(\\.\\d+)?,\\d+(\\.\\d+)?$" } } From 7a2c2962eb45c3b4785c6572df0f9495551e7da0 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Mon, 3 Nov 2025 16:22:46 +0100 Subject: [PATCH 122/258] fix issue with genorm envs --- modules/local/genorm/compute_m_measure/main.nf | 3 +-- modules/local/genorm/cross_join/main.nf | 3 +-- modules/local/genorm/expression_ratio/main.nf | 3 +-- modules/local/genorm/make_chunks/main.nf | 3 +-- modules/local/genorm/ratio_standard_variation/main.nf | 3 +-- modules/local/normalisation/deseq2/main.nf | 2 +- modules/local/normalisation/edger/main.nf | 2 +- 7 files changed, 7 insertions(+), 12 deletions(-) diff --git a/modules/local/genorm/compute_m_measure/main.nf b/modules/local/genorm/compute_m_measure/main.nf index c0d6c264..07aba914 100644 --- a/modules/local/genorm/compute_m_measure/main.nf +++ b/modules/local/genorm/compute_m_measure/main.nf @@ -1,9 +1,8 @@ process COMPUTE_M_MEASURE { label 'process_medium' - publishDir "${params.outdir}/genorm/m_measures" - conda "${moduleDir}/environment.yml" + conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" diff --git a/modules/local/genorm/cross_join/main.nf b/modules/local/genorm/cross_join/main.nf index 7d927822..e62281ef 100644 --- a/modules/local/genorm/cross_join/main.nf +++ b/modules/local/genorm/cross_join/main.nf @@ -1,9 +1,8 @@ process CROSS_JOIN { label 'process_low' - publishDir "${params.outdir}/genorm/cross_joins" - conda "${moduleDir}/environment.yml" + conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" diff --git a/modules/local/genorm/expression_ratio/main.nf b/modules/local/genorm/expression_ratio/main.nf index da9bb1f1..5eab94eb 100644 --- a/modules/local/genorm/expression_ratio/main.nf +++ b/modules/local/genorm/expression_ratio/main.nf @@ -1,9 +1,8 @@ process EXPRESSION_RATIO { label 'process_low' - publishDir "${params.outdir}/genorm/expression_ratios" - conda "${moduleDir}/environment.yml" + conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" diff --git a/modules/local/genorm/make_chunks/main.nf b/modules/local/genorm/make_chunks/main.nf index 8cfca9b6..cc959731 100644 --- a/modules/local/genorm/make_chunks/main.nf +++ b/modules/local/genorm/make_chunks/main.nf @@ -1,9 +1,8 @@ process MAKE_CHUNKS { label 'process_medium' - publishDir "${params.outdir}/genorm/chunks" - conda "${moduleDir}/environment.yml" + conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" diff --git a/modules/local/genorm/ratio_standard_variation/main.nf b/modules/local/genorm/ratio_standard_variation/main.nf index 7e2b9de2..60645212 100644 --- a/modules/local/genorm/ratio_standard_variation/main.nf +++ b/modules/local/genorm/ratio_standard_variation/main.nf @@ -1,9 +1,8 @@ process RATIO_STANDARD_VARIATION { label 'process_low' - publishDir "${params.outdir}/genorm/ratio_standard_variations" - conda "${moduleDir}/environment.yml" + conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" diff --git a/modules/local/normalisation/deseq2/main.nf b/modules/local/normalisation/deseq2/main.nf index d47f2697..dee09859 100644 --- a/modules/local/normalisation/deseq2/main.nf +++ b/modules/local/normalisation/deseq2/main.nf @@ -20,7 +20,7 @@ process NORMALISATION_DESEQ2 { return 'ignore' } } else { - return 'ignore' + return 'finish' } } diff --git a/modules/local/normalisation/edger/main.nf b/modules/local/normalisation/edger/main.nf index ef6b23a0..1b46b061 100644 --- a/modules/local/normalisation/edger/main.nf +++ b/modules/local/normalisation/edger/main.nf @@ -20,7 +20,7 @@ process NORMALISATION_EDGER { return 'ignore' } } else { - return 'ignore' + return 'finish' } } From 63aced0cf082f3c7eb44fb103d3bd429f0bbc6a0 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Tue, 4 Nov 2025 17:44:17 +0100 Subject: [PATCH 123/258] remove filter of gene ID mapping check in get_geo_dataset_accessions; add geo outputs to multqc report; improve reporting of eatlas --- assets/multiqc_config.yml | 189 ++++++++++-------- bin/get_eatlas_accessions.py | 35 ++-- bin/get_geo_dataset_accessions.py | 88 ++++---- conf/test.config | 1 - conf/test_eatlas_geo.config | 21 -- .../tool/nf_core_stableexpression.xml | 6 +- .../expressionatlas/getaccessions/main.nf | 8 +- modules/local/geo/getaccessions/main.nf | 15 +- nextflow.config | 4 +- nextflow_schema.json | 7 +- subworkflows/local/data_cleansing/main.nf | 4 +- .../local/expression_normalisation/main.nf | 8 +- workflows/stableexpression.nf | 10 +- 13 files changed, 197 insertions(+), 199 deletions(-) delete mode 100644 conf/test_eatlas_geo.config diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index fbbe2905..3a8f3b4c 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -33,7 +33,7 @@ table_cond_formatting_colours: custom_data: ranked_top_stable_genes_summary: - section_name: "Top stable genes - ranked by stability" + section_name: "Stable genes ranking" file_format: "csv" no_violin: true description: | @@ -308,17 +308,18 @@ custom_data: Ratio of Microarray samples in which the gene has a zero value. expression_distributions_top_stable_genes: - section_name: "Expression distribution of the top stable genes (ranked by stability)" + section_name: "Distribution of the normalised expression of the most stable genes (ranked by stability score)" file_format: "csv" pconfig: sort_samples: false + #xmin: 0 + #xmax: 1 + xlab: Expression + ylab: Gene description: | - Distribution of gene expression across samples for the most stable genes. - - Genes are ranked from the most stable to the least stable. + Distribution of normalised gene expression (between 0 and 1) across samples for the most stable genes. + Only the 100 most stable genes are shown and genes are ranked from the most stable to the least stable. plot_type: "boxplot" - #xlab: Expression - #ylab: Gene gene_statistics: section_name: "Descriptive statistics - All genes" @@ -330,119 +331,105 @@ custom_data: stability_score: title: "Stability score" color: "rgb(186,43,32)" - variation_coefficient_normalised: - title: "Normalised coefficient of variation" + coefficient_of_variation_normalised: + title: "Normalised CV" + color: "rgb(64, 122, 22)" + robust_coefficient_of_variation_median_normalised: + title: "Normalised RCVm" color: "rgb(64, 122, 22)" normfinder_stability_value_normalised: - title: "Normalised Normfinder stability value " + title: "Normalised Normfinder score" color: "rgb(64, 122, 22)" genorm_m_measure_normalised: - title: "Normalised Genorm M-measure" - color: "rgb(64, 122, 22)" - median_absolute_deviation_normalised: - title: "Normalised median absolute deviation" + title: "Normalised Genorm score" color: "rgb(64, 122, 22)" - standard_deviation: - title: "Standard deviation" - color: "rgb(227, 210, 36)" - variation_coefficient: - title: "Coefficient of variation" - color: "rgb(227, 210, 36)" + coefficient_of_variation: + title: "CV" + color: "rgb(26, 167, 178)" + robust_coefficient_of_variation_median: + title: "RCVm" + color: "rgb(26, 167, 178)" normfinder_stability_value: title: "Normfinder stability value " - color: "rgb(227, 210, 36)" + color: "rgb(26, 167, 178)" genorm_m_measure: title: "Genorm M-measure" - color: "rgb(227, 210, 36)" + color: "rgb(26, 167, 178)" mean: title: "Average" - color: "rgb(85, 23, 166)" + color: "rgb(26, 167, 178)" + standard_deviation: + title: "Standard deviation" + color: "rgb(26, 167, 178)" median: title: "Median" - color: "rgb(85, 23, 166)" + color: "rgb(26, 167, 178)" median_absolute_deviation: - title: "Median absolute deviation" - color: "rgb(85, 23, 166)" - rnaseq_standard_deviation: - title: "Std [RNA-seq only]" - rnaseq_variation_coefficient: + title: "MAD" + color: "rgb(26, 167, 178)" + rnaseq_coefficient_of_variation: title: "Var coeff [RNA-seq only]" + color: "rgb(140, 50, 76)" + rnaseq_robust_coefficient_of_variation_median: + title: "RCVm [RNA-seq only]" + color: "rgb(140, 50, 76)" rnaseq_mean: title: "Average [RNA-seq only]" - description: | - Average expression across samples. - format: "{:,.4f}" + color: "rgb(140, 50, 76)" + rnaseq_standard_deviation: + title: "Std [RNA-seq only]" + color: "rgb(140, 50, 76)" rnaseq_median: title: "Median [RNA-seq only]" - description: | - Median expression across RNA-Seq samples. - format: "{:,.4f}" + color: "rgb(140, 50, 76)" rnaseq_median_absolute_deviation: - title: "Mad [RNA-seq only]" - description: | - Median absolute deviation of the expression across RNA-Seq samples. - format: "{:,.4f}" - microarray_standard_deviation: - title: "Std [Microarray only]" - description: | - Standard deviation of the expression across Microarray samples. - format: "{:,.4f}" - microarray_variation_coefficient: + title: "MAD [RNA-seq only]" + color: "rgb(140, 50, 76)" + microarray_coefficient_of_variation: title: "Var coeff [Microarray only]" - description: | - Variation coefficient ( std(expression) / mean(expression) ) across Microarray samples. - format: "{:,.4f}" + color: "rgb(27, 83, 73)" + microarray_robust_coefficient_of_variation_median: + title: "RCVm [Microarray only]" + color: "rgb(27, 83, 73)" microarray_mean: title: "Average [Microarray only]" - description: | - Average expression across Microarray samples. - format: "{:,.4f}" + color: "rgb(27, 83, 73)" + microarray_standard_deviation: + title: "Std [Microarray only]" + color: "rgb(27, 83, 73)" microarray_median: title: "Median [Microarray only]" - description: | - Median expression across Microarray samples. - format: "{:,.4f}" + color: "rgb(27, 83, 73)" microarray_median_absolute_deviation: - title: "Mad [Microarray only]" - description: | - Median absolute deviation of the expression across Microarray samples. - format: "{:,.4f}" + title: "MAD [Microarray only]" + color: "rgb(27, 83, 73)" ratio_nulls_in_all_samples: title: "Ratio null values (all samples)" - description: | - Ratio of samples in which the gene is not represented. + color: "rgb(106, 78, 193)" ratio_nulls_in_valid_samples: title: "Ratio null values (valid samples)" - description: | - Ratio of samples in which the gene is not represented, excluding samples with particularly low overall gene count. + color: "rgb(106, 78, 193)" ratio_zeros: title: "Ratio zero values" - description: | - Ratio of samples in which the gene has a zero value. + color: "rgb(106, 78, 193)" rnaseq_ratio_nulls_in_all_samples: title: "Ratio null values (all samples) [RNA-seq only]" - description: | - Ratio of RNA-Seq samples in which the gene is not represented. + color: "rgb(106, 78, 193)" rnaseq_ratio_nulls_in_valid_samples: title: "Ratio null values (valid samples) [RNA-seq only]" - description: | - Ratio of RNA-Seq samples in which the gene is not represented, excluding samples with particularly low overall gene count. + color: "rgb(106, 78, 193)" rnaseq_ratio_zeros: title: "Ratio zero values [RNA-seq only]" - description: | - Ratio of RNA-Seq samples in which the gene has a zero value. + color: "rgb(106, 78, 193)" microarray_ratio_nulls_in_all_samples: title: "Ratio null values (all samples) [Microarray only]" - description: | - Ratio of Microarray samples in which the gene is not represented. + color: "rgb(106, 78, 193)" microarray_ratio_nulls_in_valid_samples: title: "Ratio null values (valid samples) [Microarray only]" - description: | - Ratio of Microarray samples in which the gene is not represented, excluding samples with particularly low overall gene count. + color: "rgb(106, 78, 193)" microarray_ratio_zeros: title: "Ratio zero values [Microarray only]" - description: | - Ratio of Microarray samples in which the gene has a zero value. + color: "rgb(106, 78, 193)" gene_counts: section_name: "Gene counts" @@ -495,22 +482,52 @@ custom_data: Pvalue of Kolmogorov-Smirnov test to normal / uniform distribution. color: "#bf812d" - eatlas_filtered_experiments_metadata: - section_name: "Expression Atlas metadata - filtered" + eatlas_selected_experiments_metadata: + section_name: "Selected" + parent_id: eatlas + parent_name: "Expression Atlas dataset metadata" + parent_description: "Information about the Expression Atlas datasets processed in the analysis" file_format: "tsv" no_violin: true sort_rows: false description: | - Metadata of Expression Atlas accessions filtered relatively to the provided species (and optionally the provided keywords) + Metadata of selected Expression Atlas datasets corresponding to the provided species (and optionally the provided keywords) plot_type: "table" eatlas_all_experiments_metadata: - section_name: "Expression Atlas metadata - all" + section_name: "All datasets" + parent_id: eatlas + parent_name: "Expression Atlas dataset metadata" + parent_description: "Information about the Expression Atlas datasets processed in the analysis" + file_format: "tsv" + no_violin: true + sort_rows: false + description: | + Metadata of all Expression Atlas datasets corresponding to the provided species + plot_type: "table" + + geo_selected_experiments_metadata: + section_name: "Selected" + parent_id: geo + parent_name: "GEO dataset metadata" + parent_description: "Information about the GEO datasets processed in the analysis" + file_format: "tsv" + no_violin: true + sort_rows: false + description: | + Metadata of selected GEO datasets corresponding to the provided species (and optionally the provided keywords) + plot_type: "table" + + geo_all_experiments_metadata: + section_name: "All datasets" + parent_id: geo + parent_name: "GEO dataset metadata" + parent_description: "Information about the GEO datasets processed in the analysis" file_format: "tsv" no_violin: true sort_rows: false description: | - Metadata of Expression Atlas accessions filtered relatively to the provided species (and optionally the provided keywords) + Metadata of all GEO datasets corresponding to the provided species plot_type: "table" #violin_downsample_after: 10000 @@ -533,7 +550,11 @@ sp: fn: "*skewness_statistics.csv" uniform_distribution_probabilities: fn: "*ks_test_statistics.csv" - eatlas_filtered_experiments_metadata: - fn: "*filtered_experiments.metadata.tsv" + eatlas_selected_experiments_metadata: + fn: "*selected_experiments.metadata.tsv" eatlas_all_experiments_metadata: - fn: "*all_experiments.metadata.tsv" + fn: "*species_experiments.metadata.tsv" + geo_selected_experiments_metadata: + fn: "*geo_selected_datasets.metadata.tsv" + geo_all_experiments_metadata: + fn: "*geo_all_datasets.metadata.tsv" diff --git a/bin/get_eatlas_accessions.py b/bin/get_eatlas_accessions.py index 7e929812..5172f5c5 100755 --- a/bin/get_eatlas_accessions.py +++ b/bin/get_eatlas_accessions.py @@ -24,9 +24,9 @@ ALL_EXP_URL = "https://www.ebi.ac.uk/gxa/json/experiments/" ACCESSION_OUTFILE_NAME = "accessions.txt" -ALL_EXPERIMENTS_METADATA_OUTFILE_NAME = "all_experiments.metadata.tsv" +# ALL_EXPERIMENTS_METADATA_OUTFILE_NAME = "all_experiments.metadata.tsv" SPECIES_EXPERIMENTS_METADATA_OUTFILE_NAME = "species_experiments.metadata.tsv" -FILTERED_EXPERIMENTS_METADATA_OUTFILE_NAME = "filtered_experiments.metadata.tsv" +SELECTED_EXPERIMENTS_METADATA_OUTFILE_NAME = "selected_experiments.metadata.tsv" FILTERED_EXPERIMENTS_WITH_KEYWORDS_OUTFILE_NAME = "filtered_experiments.keywords.yaml" @@ -54,7 +54,7 @@ def parse_args(): "--species", type=str, required=True, - help="Search Expression Atlas for this specific species" + help="Search Expression Atlas for this specific species", ) parser.add_argument( "--keywords", @@ -62,15 +62,10 @@ def parse_args(): nargs="*", help="Keywords to search for in experiment description", ) - parser.add_argument( - "--platform", - type=str, - help="Platform type" - ) + parser.add_argument("--platform", type=str, help="Platform type") return parser.parse_args() - @retry( retry=retry_if_exception_type(ExpressionAtlasNothingFoundError), stop=stop_after_delay(600), @@ -231,8 +226,14 @@ def get_platform_specific_experiments(experiments: list[dict], platform: str): platform_experiments = [] for exp_dict in experiments: if technology_type := exp_dict.get("technologyType"): - parsed_technology_type = technology_type[0] if isinstance(technology_type, list) else technology_type - parsed_platform = parsed_technology_type.lower().split(" ")[0].replace("-", "") + parsed_technology_type = ( + technology_type[0] + if isinstance(technology_type, list) + else technology_type + ) + parsed_platform = ( + parsed_technology_type.lower().split(" ")[0].replace("-", "") + ) if platform == parsed_platform: platform_experiments.append(exp_dict) return platform_experiments @@ -293,8 +294,6 @@ def parse_experiment(exp_dict: dict): } - - def filter_experiment_with_keywords(exp_dict: dict, keywords: list[str]) -> dict | None: all_searchable_fields = [exp_dict["description"]] + exp_dict["properties"] found_keywords = keywords_in_fields(all_searchable_fields, keywords) @@ -348,7 +347,9 @@ def main(): if args.platform: logger.info(f"Getting experiments corresponding to platform {args.platform}") - all_experiments = get_platform_specific_experiments(all_experiments, args.platform) + all_experiments = get_platform_specific_experiments( + all_experiments, args.platform + ) species_experiments = get_species_experiments(all_experiments, species_name) logger.info( @@ -388,12 +389,14 @@ def main(): with open(ACCESSION_OUTFILE_NAME, "w") as fout: fout.writelines([f"{acc}\n" for acc in selected_accessions]) + """ # exporting metadata logger.info( f"Writing metadata of all experiments to {ALL_EXPERIMENTS_METADATA_OUTFILE_NAME}" ) df = pd.DataFrame.from_dict(all_experiments) df.to_csv(ALL_EXPERIMENTS_METADATA_OUTFILE_NAME, sep="\t", index=False, header=True) + """ # exporting metadata logger.info( @@ -406,11 +409,11 @@ def main(): if selected_experiments: logger.info( - f"Writing metadata of filtered experiments to {FILTERED_EXPERIMENTS_METADATA_OUTFILE_NAME}" + f"Writing metadata of filtered experiments to {SELECTED_EXPERIMENTS_METADATA_OUTFILE_NAME}" ) df = pd.DataFrame.from_dict(selected_experiments) df.to_csv( - FILTERED_EXPERIMENTS_METADATA_OUTFILE_NAME, + SELECTED_EXPERIMENTS_METADATA_OUTFILE_NAME, sep="\t", index=False, header=True, diff --git a/bin/get_geo_dataset_accessions.py b/bin/get_geo_dataset_accessions.py index 3f1cf955..9dc09eb7 100755 --- a/bin/get_geo_dataset_accessions.py +++ b/bin/get_geo_dataset_accessions.py @@ -7,9 +7,10 @@ from multiprocessing import Pool from Bio import Entrez from pathlib import Path -from random import sample -import re -import requests + +# from random import sample +# import re +# import requests import pandas as pd import xmltodict from urllib.request import urlretrieve @@ -25,7 +26,7 @@ from requests.exceptions import HTTPError, ConnectionError from natural_language_utils import keywords_in_fields -from gprofiler_utils import convert_ids, chunk_list +# from gprofiler_utils import convert_ids, chunk_list logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -35,27 +36,24 @@ # Entrez.Parser.Parser.directory("/tmp/biopython") ACCESSION_OUTFILE_NAME = "accessions.txt" -SPECIES_DATASETS_OUTFILE_NAME = "species_datasets.metadata.tsv" -WRONG_SPECIES_DATASETS_METADATA_OUTFILE_NAME = "wrong_species_datasets.metadata.tsv" +SPECIES_DATASETS_OUTFILE_NAME = "geo_all_datasets.metadata.tsv" +WRONG_SPECIES_DATASETS_METADATA_OUTFILE_NAME = "geo_wrong_species_datasets.metadata.tsv" WRONG_SPECS_DATASETS_METADATA_OUTFILE_NAME = ( - "wrong_platform_moltype_datasets.metadata.tsv" -) -WRONG_KEYWORDS_DATASETS_METADATA_OUTFILE_NAME = "wrong_keywords_datasets.metadata.tsv" -PLATFORM_NOT_AVAILABLE_DATASETS_METADATA_OUTFILE_NAME = ( - "platform_not_available_datasets.metadata.tsv" + "geo_wrong_platform_moltype_datasets.metadata.tsv" ) -GENE_ID_MAPPING_ISSUES_DATASETS_METADATA_OUTFILE_NAME = ( - "gene_id_mapping_issues_datasets.metadata.tsv" +WRONG_KEYWORDS_DATASETS_METADATA_OUTFILE_NAME = ( + "geo_wrong_keywords_datasets.metadata.tsv" ) -FINAL_DATASETS_METADATA_OUTFILE_NAME = "final_datasets.metadata.tsv" +# PLATFORM_NOT_AVAILABLE_DATASETS_METADATA_OUTFILE_NAME = "platform_not_available_datasets.metadata.tsv" +# GENE_ID_MAPPING_ISSUES_DATASETS_METADATA_OUTFILE_NAME = "gene_id_mapping_issues_datasets.metadata.tsv" +FINAL_DATASETS_METADATA_OUTFILE_NAME = "geo_selected_datasets.metadata.tsv" ENTREZ_QUERY_MAX_RESULTS = 9999 ENTREZ_EMAIL = "stableexpression@nfcore.com" -PLATFORM_METADATA_CHUNKSIZE = 2000 +# PLATFORM_METADATA_CHUNKSIZE = 2000 + +# NCBI_API_BASE_URL = "https://www.ncbi.nlm.nih.gov/geo/query/acc.cgi?view=data&acc={accession}" -NCBI_API_BASE_URL = ( - "https://www.ncbi.nlm.nih.gov/geo/query/acc.cgi?view=data&acc={accession}" -) STOP_RETRY_AFTER_DELAY = 600 @@ -168,6 +166,7 @@ def send_request_to_entrez_esummary(ids: list[str]) -> list[dict]: return Entrez.read(handle) +""" @retry( stop=stop_after_delay(STOP_RETRY_AFTER_DELAY), wait=wait_exponential(multiplier=1, min=1, max=30), @@ -204,6 +203,7 @@ def send_request_to_ncbi_api(accession: str) -> requests.Response | None: ) return response +""" @retry( @@ -378,7 +378,7 @@ def download_platform_datatable(ftp_link: str, platform_accession: str) -> Path return output_file """ - +""" def get_platform_probe_id_samples(platform_accession: str) -> list[str]: response = send_request_to_ncbi_api(platform_accession) if response is None: @@ -435,7 +435,7 @@ def probe_ids_can_be_converted( # if at least one ID could be converted can_be_converted = True if mapping_dict else False return dataset_metadata, can_be_converted - +""" # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # FORMATTING @@ -650,13 +650,14 @@ def contains_transcriptomic_source(library_sources: list, accession: str) -> boo return False -def dataset_is_valid(metadata: dict, platform: str) -> bool: +def dataset_is_valid(metadata: dict, platform: str | None) -> bool: accession = metadata["accession"] # checking platform - if not contains_proper_experiment_type( - metadata["experiment_types"], accession, platform - ): - return False + if platform is not None: + if not contains_proper_experiment_type( + metadata["experiment_types"], accession, platform + ): + return False # checking that library sources fit if not contains_transcriptomic_source( @@ -842,7 +843,7 @@ def main(): ) func = partial(filter_metadata_with_keywords, keywords=args.keywords) - keywords_filtered_metadata_list = [] + final_metadata_list = [] with ( Pool(processes=args.nb_cpus) as p, tqdm(total=len(specs_filtered_metadata_list)) as pbar, @@ -852,18 +853,19 @@ def main(): pbar.refresh() if result is None: continue - keywords_filtered_metadata_list.append(result) + final_metadata_list.append(result) export_filtered_out_datasets_if_any( specs_filtered_metadata_list, - keywords_filtered_metadata_list, + final_metadata_list, WRONG_KEYWORDS_DATASETS_METADATA_OUTFILE_NAME, "keywords", ) else: - keywords_filtered_metadata_list = specs_filtered_metadata_list + final_metadata_list = specs_filtered_metadata_list + """ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # GETTING METADATA OF SEQUENCING PLATFORMS # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -886,6 +888,7 @@ def main(): "platform metadata", ) + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # FILTERING OUT DATASETS FOR WHICH ID MAPPING DOES NOT WORK # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -915,6 +918,7 @@ def main(): GENE_ID_MAPPING_ISSUES_DATASETS_METADATA_OUTFILE_NAME, "gene id mapping", ) + """ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # GETTING ACCESSIONS TO DOWNLOAD @@ -929,22 +933,16 @@ def main(): with open(ACCESSION_OUTFILE_NAME, "w") as fout: fout.writelines([f"{acc}\n" for acc in selected_accessions]) - if final_metadata_list: - logger.info( - f"Writing metadata of selected datasets to {FINAL_DATASETS_METADATA_OUTFILE_NAME}" - ) - df = pd.DataFrame.from_dict(final_metadata_list) - df.to_csv( - FINAL_DATASETS_METADATA_OUTFILE_NAME, - sep="\t", - index=False, - header=True, - ) - else: - msg = f"Could not find experiments for species {args.species}" - if args.keywords: - msg += f" and keywords {args.keywords}" - logger.warning(msg) + logger.info( + f"Writing metadata of selected datasets to {FINAL_DATASETS_METADATA_OUTFILE_NAME}" + ) + df = pd.DataFrame.from_dict(final_metadata_list) + df.to_csv( + FINAL_DATASETS_METADATA_OUTFILE_NAME, + sep="\t", + index=False, + header=True, + ) if __name__ == "__main__": diff --git a/conf/test.config b/conf/test.config index 1778c614..fdf68236 100644 --- a/conf/test.config +++ b/conf/test.config @@ -17,6 +17,5 @@ params { // Input data species = 'beta vulgaris' - keywords = "leaf" outdir = "results/test" } diff --git a/conf/test_eatlas_geo.config b/conf/test_eatlas_geo.config deleted file mode 100644 index 63337e37..00000000 --- a/conf/test_eatlas_geo.config +++ /dev/null @@ -1,21 +0,0 @@ -/* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Nextflow config file for running minimal tests -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Defines input files and everything required to run a fast and simple pipeline test. - It tests the different ways to use the pipeline, with small data - - Use as follows: - nextflow run nf-core/stableexpression -profile test_eatlas_geo, --outdir - ----------------------------------------------------------------------------------------- -*/ - -params { - config_profile_name = 'Test dataset custom gene data profile' - config_profile_description = 'Minimal test dataset with custom gene metadata to check pipeline function' - - // Input data - species = 'solanum tuberosum' - outdir = "results/test_eatlas_geo" -} diff --git a/galaxy/tool_shed/tool/nf_core_stableexpression.xml b/galaxy/tool_shed/tool/nf_core_stableexpression.xml index e82d7d1a..04f8276a 100644 --- a/galaxy/tool_shed/tool/nf_core_stableexpression.xml +++ b/galaxy/tool_shed/tool/nf_core_stableexpression.xml @@ -106,8 +106,8 @@ VERSION="1.0dev"; echo "$VERSION" --gene_metadata "$idmapping_options.gene_metadata" #end if --normalisation_method "$statistical_options.normalisation_method" - #if $statistical_options.quantile_normalisation_target_distribution - --quantile_normalisation_target_distribution "$statistical_options.quantile_normalisation_target_distribution" + #if $statistical_options.quantile_norm_target_distrib + --quantile_norm_target_distrib "$statistical_options.quantile_norm_target_distrib" #end if --ks_pvalue_threshold $statistical_options.ks_pvalue_threshold #if $statistical_options.min_expr_threshold @@ -188,7 +188,7 @@ VERSION="1.0dev"; echo "$VERSION" - + diff --git a/modules/local/expressionatlas/getaccessions/main.nf b/modules/local/expressionatlas/getaccessions/main.nf index e44d2c53..41a01a89 100644 --- a/modules/local/expressionatlas/getaccessions/main.nf +++ b/modules/local/expressionatlas/getaccessions/main.nf @@ -14,10 +14,10 @@ process EXPRESSIONATLAS_GETACCESSIONS { output: path "accessions.txt", emit: accessions - path "all_experiments.metadata.tsv", emit: all_eatlas_experiment_metadata - path "species_experiments.metadata.tsv", topic: species_eatlas_experiment_metadata - path "filtered_experiments.metadata.tsv", optional: true, topic: filtered_eatlas_experiment_metadata - path "filtered_experiments.keywords.yaml", optional: true, topic: filtered_eatlas_experiment_keywords + path "selected_experiments.metadata.tsv", topic: eatlas_selected_datasets + path "species_experiments.metadata.tsv", topic: eatlas_all_datasets + //path "filtered_experiments.metadata.tsv", optional: true, topic: filtered_eatlas_experiment_metadata + //path "filtered_experiments.keywords.yaml", optional: true, topic: filtered_eatlas_experiment_keywords tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('requests'), eval('python3 -c "import requests; print(requests.__version__)"'), topic: versions tuple val("${task.process}"), val('nltk'), eval('python3 -c "import nltk; print(nltk.__version__)"'), topic: versions diff --git a/modules/local/geo/getaccessions/main.nf b/modules/local/geo/getaccessions/main.nf index 091eb2ed..c46ca58f 100644 --- a/modules/local/geo/getaccessions/main.nf +++ b/modules/local/geo/getaccessions/main.nf @@ -16,13 +16,14 @@ process GEO_GETACCESSIONS { output: path "accessions.txt", emit: accessions - path "final_datasets.metadata.tsv", optional: true, emit: final_datasets_metadata - path "wrong_species_datasets.metadata.tsv", optional: true, emit: wrong_species_datasets_metadata - path "wrong_platform_moltype_datasets.metadata.tsv", optional: true, emit: wrong_platform_moltype_datasets_metadata - path "wrong_keywords_datasets.metadata.tsv", optional: true, emit: wrong_keywords_datasets_metadata - path "platform_not_available_datasets.metadata.tsv", optional: true, emit: platform_not_available_datasets_metadata - path "gene_id_mapping_issues_datasets.metadata.tsv", optional: true, emit: gene_id_mapping_issues_datasets_metadata - path "species_datasets.metadata.tsv", optional: true, emit: all_datasets_species_metadata + path "geo_selected_datasets.metadata.tsv", topic: geo_selected_datasets + path "geo_all_datasets.metadata.tsv", topic: geo_all_datasets + path "geo_wrong_species_datasets.metadata.tsv", optional: true, topic: geo_wrong_species_datasets + path "geo_wrong_platform_moltype_datasets.metadata.tsv", optional: true, topic: geo_wrong_platform_moltype_datasets + path "geo_wrong_keywords_datasets.metadata.tsv", optional: true, topic: geo_wrong_keywords_datasets + //path "platform_not_available_datasets.metadata.tsv", optional: true, emit: platform_not_available_datasets_metadata + //path "gene_id_mapping_issues_datasets.metadata.tsv", optional: true, emit: gene_id_mapping_issues_datasets_metadata + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('requests'), eval('python3 -c "import requests; print(requests.__version__)"'), topic: versions diff --git a/nextflow.config b/nextflow.config index e5842a29..8590173b 100644 --- a/nextflow.config +++ b/nextflow.config @@ -42,7 +42,7 @@ params { // statistics normalisation_method = 'deseq2' - quantile_normalisation_target_distribution = 'uniform' + quantile_norm_target_distrib = 'uniform' nb_top_gene_candidates = 5000 ks_pvalue_threshold = 0 min_expr_threshold = 0.2 @@ -50,7 +50,7 @@ params { // stability scoring run_genorm = false candidate_selection_descriptor = "cv" - stability_score_weights = "0.7,0.1,0.1,0.1" + stability_score_weights = "0.8,0.1,0.1,0" // Boilerplate options outdir = null diff --git a/nextflow_schema.json b/nextflow_schema.json index 82744e23..4cc1faa3 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -215,7 +215,7 @@ "default": "deseq2", "help_text": "Raw RNAseq data must be normalised before further processing. You can select the R package used for normalisation." }, - "quantile_normalisation_target_distribution": { + "quantile_norm_target_distrib": { "type": "string", "description": "Target distribution for quantile normalisation", "fa_icon": "fas fa-chart-bar", @@ -268,14 +268,13 @@ "type": "boolean", "description": "Run Genorm", "fa_icon": "fas fa-check", - "help": "Genorm is NOT run by default. To run and get additional information about gene stability, set this parameter to true." + "help": "Genorm is not run by default. To run and get additional information about gene stability, set this parameter to true. Moreover, you can integrate the Genorm M-measure in the stability score by modifying the score weights with --stability_score_weights." }, "stability_score_weights": { "type": "string", "description": "Weights for stability score calculation", "fa_icon": "fas fa-balance-scale", - "default": "0.7,0.1,0.1,0.1", - "help_text": "Weights for Coefficient of Variation / Robust Coefficient of Variation on Median / Normfinder / Genorm respectively. Must be a comma-separated string. Example: 0.7,0.1,0.1,0.1", + "help_text": "Weights for Coefficient of Variation / Robust Coefficient of Variation on Median / Normfinder / Genorm respectively. Must be a comma-separated string. Example: 0.8,0.1,0.1,0", "pattern": "^\\d+(\\.\\d+)?,\\d+(\\.\\d+)?,\\d+(\\.\\d+)?,\\d+(\\.\\d+)?$" } } diff --git a/subworkflows/local/data_cleansing/main.nf b/subworkflows/local/data_cleansing/main.nf index 60dc8174..d7de77d7 100644 --- a/subworkflows/local/data_cleansing/main.nf +++ b/subworkflows/local/data_cleansing/main.nf @@ -11,7 +11,7 @@ workflow DATA_CLEANSING { take: ch_quantile_normalised_datasets - quantile_normalisation_target_distribution + quantile_norm_target_distrib ks_pvalue_threshold main: @@ -22,7 +22,7 @@ workflow DATA_CLEANSING { DATASET_STATISTICS( ch_quantile_normalised_datasets, - quantile_normalisation_target_distribution + quantile_norm_target_distrib ) // diff --git a/subworkflows/local/expression_normalisation/main.nf b/subworkflows/local/expression_normalisation/main.nf index 9928c023..144a4db9 100644 --- a/subworkflows/local/expression_normalisation/main.nf +++ b/subworkflows/local/expression_normalisation/main.nf @@ -13,7 +13,7 @@ workflow EXPRESSION_NORMALISATION { take: ch_datasets normalisation_method - quantile_normalisation_target_distribution + quantile_norm_target_distrib main: @@ -50,7 +50,7 @@ workflow EXPRESSION_NORMALISATION { QUANTILE_NORMALISATION ( quant_norm_input, - quantile_normalisation_target_distribution + quantile_norm_target_distrib ) @@ -58,7 +58,3 @@ workflow EXPRESSION_NORMALISATION { normalised_counts = QUANTILE_NORMALISATION.out.counts } - - - - diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 9b40989d..198813cd 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -100,7 +100,7 @@ workflow STABLEEXPRESSION { EXPRESSION_NORMALISATION( ch_counts, params.normalisation_method, - params.quantile_normalisation_target_distribution + params.quantile_norm_target_distrib ) // ----------------------------------------------------------------- @@ -109,7 +109,7 @@ workflow STABLEEXPRESSION { DATA_CLEANSING( EXPRESSION_NORMALISATION.out.normalised_counts, - params.quantile_normalisation_target_distribution, + params.quantile_norm_target_distrib, params.ks_pvalue_threshold ) @@ -188,8 +188,10 @@ workflow STABLEEXPRESSION { .mix( ch_top_stable_genes_summary.collect() ) .mix( ch_all_genes_summary.collect() ) .mix( ch_top_stable_genes_transposed_counts.collect() ) - .mix( Channel.topic('all_eatlas_experiment_metadata').collect() ) - .mix( Channel.topic('filtered_eatlas_experiment_metadata').collect() ) + .mix( Channel.topic('eatlas_all_datasets').collect() ) + .mix( Channel.topic('eatlas_selected_datasets').collect() ) + .mix( Channel.topic('geo_all_datasets').collect() ) + .mix( Channel.topic('geo_selected_datasets').collect() ) .set { ch_multiqc_files } MULTIQC_WORKFLOW( From fb566e1f7407ce47bcf46038a93ddac76eb83d0a Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 5 Nov 2025 11:46:02 +0100 Subject: [PATCH 124/258] improve geo reporting in multiqc --- assets/multiqc_config.yml | 42 +++++++++++++++++++++++++++++++ bin/get_geo_dataset_accessions.py | 6 ++++- workflows/stableexpression.nf | 3 +++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index 3a8f3b4c..bbb9051c 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -530,6 +530,42 @@ custom_data: Metadata of all GEO datasets corresponding to the provided species plot_type: "table" + geo_wrong_species_experiments_metadata: + section_name: "Datasets rejected due to species mismatch" + parent_id: geo + parent_name: "GEO dataset metadata" + parent_description: "Information about the GEO datasets processed in the analysis" + file_format: "tsv" + no_violin: true + sort_rows: false + description: | + Metadata of all GEO datasets which were rejected due to species mismatch + plot_type: "table" + + geo_wrong_platform_moltype_experiments_metadata: + section_name: "Datasets rejected due to platform / molecule type mismatch" + parent_id: geo + parent_name: "GEO dataset metadata" + parent_description: "Information about the GEO datasets processed in the analysis" + file_format: "tsv" + no_violin: true + sort_rows: false + description: | + Metadata of all GEO datasets which were rejected due to platform / molecule type mismatch + plot_type: "table" + + geo_wrong_keywords_experiments_metadata: + section_name: "Datasets rejected due to keywords mismatch" + parent_id: geo + parent_name: "GEO dataset metadata" + parent_description: "Information about the GEO datasets processed in the analysis" + file_format: "tsv" + no_violin: true + sort_rows: false + description: | + Metadata of all GEO datasets which were rejected due to keywords mismatch + plot_type: "table" + #violin_downsample_after: 10000 log_filesize_limit: 10000000000 # 10GB @@ -558,3 +594,9 @@ sp: fn: "*geo_selected_datasets.metadata.tsv" geo_all_experiments_metadata: fn: "*geo_all_datasets.metadata.tsv" + geo_wrong_species_experiments_metadata: + fn: "*geo_wrong_species_datasets.metadata.tsv" + geo_wrong_platform_moltype_experiments_metadata: + fn: "*geo_wrong_platform_moltype_datasets.metadata.tsv" + geo_wrong_keywords_experiments_metadata: + fn: "*geo_wrong_keywords_datasets.metadata.tsv" diff --git a/bin/get_geo_dataset_accessions.py b/bin/get_geo_dataset_accessions.py index 9dc09eb7..3513e43d 100755 --- a/bin/get_geo_dataset_accessions.py +++ b/bin/get_geo_dataset_accessions.py @@ -748,7 +748,11 @@ def main(): logger.info( f"Writing metadata of all experiments for species {args.species} to {SPECIES_DATASETS_OUTFILE_NAME}" ) - df = pd.DataFrame.from_dict(dataset_metadata_list) + formated_dataset_metadata_list = [ + {k: v for k, v in r.items() if k not in ["Item", "Id"]} + for r in dataset_metadata_list + ] + df = pd.DataFrame.from_dict(formated_dataset_metadata_list) df.to_csv(SPECIES_DATASETS_OUTFILE_NAME, sep="\t", index=False, header=True) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 198813cd..988a7dad 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -192,6 +192,9 @@ workflow STABLEEXPRESSION { .mix( Channel.topic('eatlas_selected_datasets').collect() ) .mix( Channel.topic('geo_all_datasets').collect() ) .mix( Channel.topic('geo_selected_datasets').collect() ) + .mix( Channel.topic('geo_wrong_species_datasets').collect() ) + .mix( Channel.topic('geo_wrong_platform_moltype_datasets').collect() ) + .mix( Channel.topic('geo_wrong_keywords_datasets').collect() ) .set { ch_multiqc_files } MULTIQC_WORKFLOW( From b410219e4f5098be99b52f4c38064054abf99fb1 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 5 Nov 2025 11:46:31 +0100 Subject: [PATCH 125/258] delete unused test confs; add nex nf-tests for pipeline --- conf/test_dataset_custom_mapping.config | 25 -------- conf/test_eatlas_only_with_keywords.config | 31 ---------- conf/test_full.config | 3 +- conf/test_ignore_errors.config | 24 -------- conf/test_one_accession.config | 30 ---------- conf/test_one_accession_low_gene_count.config | 30 ---------- conf/test_run_normfinder_genorm.config | 34 ----------- tests/default.nf.test | 57 ++++++++++++++++++- 8 files changed, 58 insertions(+), 176 deletions(-) delete mode 100644 conf/test_dataset_custom_mapping.config delete mode 100644 conf/test_eatlas_only_with_keywords.config delete mode 100644 conf/test_ignore_errors.config delete mode 100644 conf/test_one_accession.config delete mode 100644 conf/test_one_accession_low_gene_count.config delete mode 100644 conf/test_run_normfinder_genorm.config diff --git a/conf/test_dataset_custom_mapping.config b/conf/test_dataset_custom_mapping.config deleted file mode 100644 index aa4fefa2..00000000 --- a/conf/test_dataset_custom_mapping.config +++ /dev/null @@ -1,25 +0,0 @@ -/* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Nextflow config file for running minimal tests -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Defines input files and everything required to run a fast and simple pipeline test. - It tests the different ways to use the pipeline, with small data - - Use as follows: - nextflow run nf-core/stableexpression -profile test_dataset_custom_mapping, --outdir - ----------------------------------------------------------------------------------------- -*/ - -params { - config_profile_name = 'Test dataset custom gene data profile' - config_profile_description = 'Minimal test dataset with custom gene metadata to check pipeline function' - - // Input data - species = 'solanum tuberosum' - datasets = "tests/test_data/input_datasets/input.csv" - skip_gprofiler = true - gene_id_mapping = "tests/test_data/input_datasets/mapping.csv" - gene_metadata = "tests/test_data/input_datasets/metadata.csv" - outdir = "results/test_dataset_custom_mapping" -} diff --git a/conf/test_eatlas_only_with_keywords.config b/conf/test_eatlas_only_with_keywords.config deleted file mode 100644 index 29621a26..00000000 --- a/conf/test_eatlas_only_with_keywords.config +++ /dev/null @@ -1,31 +0,0 @@ -/* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Nextflow config file for running minimal tests -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Defines input files and everything required to run a fast and simple pipeline test. - It tests the different ways to use the pipeline, with small data - - Use as follows: - nextflow run nf-core/stableexpression -profile test, --outdir - ----------------------------------------------------------------------------------------- -*/ - -process { - resourceLimits = [ - cpus: 4, - memory: '15.GB', - time: '1.h' - ] -} - -params { - config_profile_name = 'Test profile' - config_profile_description = 'Minimal test dataset to check pipeline function' - - // Input data - species = 'solanum tuberosum' - keywords = "potato,stress" - skip_fetch_geo_accessions = true - outdir = "results/test" -} diff --git a/conf/test_full.config b/conf/test_full.config index 43f1a9ef..c6b599d7 100644 --- a/conf/test_full.config +++ b/conf/test_full.config @@ -16,6 +16,7 @@ params { config_profile_description = 'Full test dataset to check pipeline function' // Input data - species = 'arabidopsis thaliana' + species = 'solanum_tuberosum' outdir = "results/test_full" + run_genorm = true } diff --git a/conf/test_ignore_errors.config b/conf/test_ignore_errors.config deleted file mode 100644 index 96f1950d..00000000 --- a/conf/test_ignore_errors.config +++ /dev/null @@ -1,24 +0,0 @@ -/* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Nextflow config file for running minimal tests -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Defines input files and everything required to run a fast and simple pipeline test. - It tests the different ways to use the pipeline, with small data - - Use as follows: - nextflow run nf-core/stableexpression -profile test_dataset, --outdir - ----------------------------------------------------------------------------------------- -*/ - -params { - config_profile_name = 'Test dataset profile' - config_profile_description = 'Minimal test dataset to check pipeline function' - - // Input data - species = 'beta vulgaris' - keywords = "leaf" - datasets = "tests/test_data/input_datasets/input.csv" - outdir = "results/test_dataset" - // quant_norm_target_distrib = "uniform" -} diff --git a/conf/test_one_accession.config b/conf/test_one_accession.config deleted file mode 100644 index bb6e2ec5..00000000 --- a/conf/test_one_accession.config +++ /dev/null @@ -1,30 +0,0 @@ -/* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Nextflow config file for running minimal tests -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Defines input files and everything required to run a fast and simple pipeline test. - It tests the different ways to use the pipeline, with small data - - Use as follows: - nextflow run nf-core/stableexpression -profile test_one_accession, --outdir - ----------------------------------------------------------------------------------------- -*/ - -process { - resourceLimits = [ - cpus: 4, - memory: '15.GB', - time: '1.h' - ] -} - -params { - config_profile_name = 'Test profile' - config_profile_description = 'Minimal test dataset to check pipeline function with only one accession to fetch from Expression Atlas.' - - // Input data - species = 'solanum tuberosum' - eatlas_accessions = "E-MTAB-552" - outdir = "results/test_one_accession" -} diff --git a/conf/test_one_accession_low_gene_count.config b/conf/test_one_accession_low_gene_count.config deleted file mode 100644 index fa36442f..00000000 --- a/conf/test_one_accession_low_gene_count.config +++ /dev/null @@ -1,30 +0,0 @@ -/* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Nextflow config file for running minimal tests -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Defines input files and everything required to run a fast and simple pipeline test. - It tests the different ways to use the pipeline, with small data - - Use as follows: - nextflow run nf-core/stableexpression -profile test_one_accession_low_gene_count, --outdir - ----------------------------------------------------------------------------------------- -*/ - -process { - resourceLimits = [ - cpus: 4, - memory: '15.GB', - time: '1.h' - ] -} - -params { - config_profile_name = 'Test profile' - config_profile_description = 'Minimal test dataset to check pipeline function with only one accession to fetch from Expression Atlas. This accession shows a very low gene count.' - - // Input data - species = 'arabidopsis thaliana' - eatlas_accessions = "E-GEOD-51720" - outdir = "results/test_one_accession_low_gene_count" -} diff --git a/conf/test_run_normfinder_genorm.config b/conf/test_run_normfinder_genorm.config deleted file mode 100644 index efc56522..00000000 --- a/conf/test_run_normfinder_genorm.config +++ /dev/null @@ -1,34 +0,0 @@ -/* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Nextflow config file for running minimal tests -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Defines input files and everything required to run a fast and simple pipeline test. - It tests the different ways to use the pipeline, with small data - - Use as follows: - nextflow run nf-core/stableexpression -profile test, --outdir - ----------------------------------------------------------------------------------------- -*/ - -process { - resourceLimits = [ - cpus: 4, - memory: '15.GB', - time: '1.h' - ] -} - -params { - config_profile_name = 'Test dataset profile' - config_profile_description = 'Minimal test dataset to check pipeline function' - - // Input data - species = 'solanum tuberosum' - eatlas_accessions = "E-MTAB-7711" - skip_fetch_eatlas_accessions = true - skip_fetch_geo_accessions = true - run_normfinder = true - run_genorm = true - outdir = "results/test" -} diff --git a/tests/default.nf.test b/tests/default.nf.test index ffe84fb2..c72a3773 100644 --- a/tests/default.nf.test +++ b/tests/default.nf.test @@ -143,7 +143,6 @@ nextflow_pipeline { eatlas_accessions = "E-MTAB-7711" skip_fetch_eatlas_accessions = true skip_fetch_geo_accessions = true - run_normfinder = true run_genorm = true outdir = "$outputDir" } @@ -187,4 +186,60 @@ nextflow_pipeline { ) } } + + test("-profile test_dataset_custom_mapping") { + + when { + params { + species = 'solanum tuberosum' + datasets = "${projectDir}/tests/test_data/input_datasets/input.csv" + skip_gprofiler = true + gene_id_mapping = "${projectDir}/tests/test_data/input_datasets/mapping.csv" + gene_metadata = "${projectDir}/tests/test_data/input_datasets/metadata.csv" + outdir = "$outputDir" + } + } + + then { + def stable_name = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}']) + def stable_path = getAllFilesFromDir(params.outdir, ignoreFile: 'tests/.nftignore') + assertAll( + { assert workflow.success}, + { assert snapshot( + removeNextflowVersion("$outputDir/pipeline_info/nf_core_stableexpression_software_mqc_versions.yml"), + stable_name, + stable_path + ).match() } + ) + } + } + + test("-profile test_full") { + + when { + params { + species = 'solanum_tuberosum' + run_genorm = true + outdir = "$outputDir" + } + } + + then { + // stable_name: All files + folders in ${params.outdir}/ with a stable name + def stable_name = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}']) + // stable_path: All files in ${params.outdir}/ with stable content + def stable_path = getAllFilesFromDir(params.outdir, ignoreFile: 'tests/.nftignore') + assertAll( + { assert workflow.success}, + { 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_stableexpression_software_mqc_versions.yml"), + // All stable path name, with a relative path + stable_name, + // All files with stable contents + stable_path + ).match() } + ) + } + } } From 5b70dd1c0c9ad7746190e36d5b069b7b4fffdd25 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 5 Nov 2025 11:47:10 +0100 Subject: [PATCH 126/258] delete errorStrategy gstatement for eatlas getdata; write to file failture reason --- ...t_eatlas_data.R => download_eatlas_data.R} | 18 +++++----- modules/local/expressionatlas/getdata/main.nf | 35 ++----------------- 2 files changed, 12 insertions(+), 41 deletions(-) rename bin/{get_eatlas_data.R => download_eatlas_data.R} (90%) diff --git a/bin/get_eatlas_data.R b/bin/download_eatlas_data.R similarity index 90% rename from bin/get_eatlas_data.R rename to bin/download_eatlas_data.R index 7ff6d83b..092a26a7 100755 --- a/bin/get_eatlas_data.R +++ b/bin/download_eatlas_data.R @@ -7,6 +7,8 @@ suppressPackageStartupMessages(library("ExpressionAtlas")) library(ExpressionAtlas) library(optparse) +FAILURE_REASON_FILE <- "failure_reason.txt" + ##################################################### ##################################################### @@ -42,7 +44,7 @@ download_expression_atlas_data_with_retries <- function(accession, max_retries = # if the accession os not valid, we stop immediately (useless to keep going) if (grepl("does not look like an ArrayExpress/BioStudies experiment accession.", w$message)) { warning(w$message) - quit(save = "no", status = 100) # quit & ignore process + write("EXPERIMENT NOT FOUND", file = FAILURE_REASON_FILE) } # else, retrying @@ -56,13 +58,13 @@ download_expression_atlas_data_with_retries <- function(accession, max_retries = if (grepl("550 Requested action not taken; file unavailable", w$message)) { warning(w$message) - quit(save = "no", status = 101) # quit & ignore process + write("EXPERIMENT SUMMARY NOT FOUND", file = FAILURE_REASON_FILE) } else if (grepl("Failure when receiving data from the peer", w$message)) { warning(w$message) - quit(save = "no", status = 100) # quit & ignore process + write("EXPERIMENT NOT FOUND", file = FAILURE_REASON_FILE) } else { warning("Unhandled warning: ", w$message) - quit(save = "no", status = 102) # quit & stop workflow + write("UNKNOWN ERROR", file = FAILURE_REASON_FILE) } } @@ -78,10 +80,10 @@ download_expression_atlas_data_with_retries <- function(accession, max_retries = if (grepl("Download appeared successful but no experiment summary object was found", e$message)) { warning(e$message) - quit(save = "no", status = 101) # quit & ignore process + write("EXPERIMENT SUMMARY NOT FOUND", file = FAILURE_REASON_FILE) } else { warning("Unhandled error: ", e$message) - quit(save = "no", status = 102) # quit & stop workflow + write("UNKNOWN ERROR", file = FAILURE_REASON_FILE) } } @@ -169,7 +171,7 @@ process_data <- function(atlas_data, accession) { } else if ( startsWith(data_type, 'A-') ) { # typically: A-AFFY- or A-GEOD- result <- get_one_colour_microarray_data(data) } else { - stop(paste('ERROR: Unknown data type:', data_type)) + write(cat("UNKNOWN DATA TYPE: ", data_type), file = FAILURE_REASON_FILE) } }, error = function(e) { @@ -207,7 +209,7 @@ cat(paste("Getting data for accession", args$accession, "\n")) accession <- trimws(args$accession) if (startsWith(accession, "E-PROT")) { warning("Ignoring the ", accession, " experiment.") - quit(save = "no", status = 100) # quit & ignore process + write("PROTEOME ACCESSIONS NOT HANDLED", file = FAILURE_REASON_FILE) } # searching and downloading expression atlas data diff --git a/modules/local/expressionatlas/getdata/main.nf b/modules/local/expressionatlas/getdata/main.nf index 75c2d91b..30efcd73 100644 --- a/modules/local/expressionatlas/getdata/main.nf +++ b/modules/local/expressionatlas/getdata/main.nf @@ -6,38 +6,6 @@ process EXPRESSIONATLAS_GETDATA { maxForks 8 // limiting to 8 threads at a time to avoid 429 errors with the Expression Atlas API server - errorStrategy { - if (task.exitStatus == 100) { - // ignoring accessions that cannot be retrieved from Expression Atlas (the script throws a 100 in this case) - // sometimes, some datasets are transiently unavailable from Expression Atlas: - // we ignore them as there is no point in trying again and again - // they will be available again soon but we can't know when - // for some other files, they are simply unavailable for good... - log.warn("Could not retrieve data for accession ${accession}. This could be a transient network issue or a permission error.") - return 'ignore' - } else if (task.exitStatus == 101) { - // some datasets are not associated with experiment summary - // we ignore them as there they would be useless for us - log.warn("Failure to download whole dataset for accession ${accession}. No experiment summary found.") - return 'ignore' - } else if (task.exitStatus == 102) { - // unhandled error: we print an extra message to warn the user - log.warn("Unhandled error occurred with accession: ${accession}") - return 'ignore' - } else if ( task.exitStatus in ((130..145) + 104 + 175) ) { // override default behaviour to sleep some time before retry - // in case of OOM errors, we wait a bit and try again (2 retries) - if ( task.attempt <= 2) { - sleep(Math.pow(2, task.attempt) * 2000 as long) - return 'retry' - } else { - log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") - return 'ignore' - } - } else { - return 'ignore' - } - } - conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/7f/7fd21450c3a3f7df37fa0480170780019e9686be319da1c9e10712f7f17cca26/data': @@ -49,12 +17,13 @@ process EXPRESSIONATLAS_GETDATA { output: path "*.design.csv", optional: true, emit: design path "*.counts.csv", optional: true, emit: counts + path "failure_reason.txt", optional: true, topic: eatlas_failure_reason tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //"'), topic: versions tuple val("${task.process}"), val('ExpressionAtlas'), eval('Rscript -e "cat(as.character(packageVersion(\'ExpressionAtlas\')))"'), topic: versions script: """ - get_eatlas_data.R --accession $accession + download_eatlas_data.R --accession $accession """ stub: From 4388e307e94252030c3319cd4b6d598c98c3815c Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 5 Nov 2025 12:22:42 +0100 Subject: [PATCH 127/258] fix issue with accessions_only and download_only params and add corresponding pipeline tests --- tests/default.nf.test | 58 +++++++++++++++++++++++++++++++++++ workflows/stableexpression.nf | 40 ++++++++++-------------- 2 files changed, 75 insertions(+), 23 deletions(-) diff --git a/tests/default.nf.test b/tests/default.nf.test index c72a3773..85581225 100644 --- a/tests/default.nf.test +++ b/tests/default.nf.test @@ -33,6 +33,64 @@ nextflow_pipeline { } } + test("-profile test_accessions_only") { + + when { + params { + species = 'beta vulgaris' + accessions_only = true + outdir = "$outputDir" + } + } + + then { + // stable_name: All files + folders in ${params.outdir}/ with a stable name + def stable_name = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}']) + // stable_path: All files in ${params.outdir}/ with stable content + def stable_path = getAllFilesFromDir(params.outdir, ignoreFile: 'tests/.nftignore') + assertAll( + { assert workflow.success}, + { 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_stableexpression_software_mqc_versions.yml"), + // All stable path name, with a relative path + stable_name, + // All files with stable contents + stable_path + ).match() } + ) + } + } + + test("-profile test_download_only") { + + when { + params { + species = 'beta vulgaris' + download_only = true + outdir = "$outputDir" + } + } + + then { + // stable_name: All files + folders in ${params.outdir}/ with a stable name + def stable_name = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}']) + // stable_path: All files in ${params.outdir}/ with stable content + def stable_path = getAllFilesFromDir(params.outdir, ignoreFile: 'tests/.nftignore') + assertAll( + { assert workflow.success}, + { 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_stableexpression_software_mqc_versions.yml"), + // All stable path name, with a relative path + stable_name, + // All files with stable contents + stable_path + ).match() } + ) + } + } + test("-profile test_one_rnaseq_one_microarray") { when { diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 988a7dad..62099720 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -35,6 +35,7 @@ workflow STABLEEXPRESSION { main: ch_versions = Channel.empty() + ch_multiqc_files = Channel.empty() ch_top_stable_genes_summary = Channel.empty() ch_all_genes_statistics = Channel.empty() @@ -167,36 +168,29 @@ workflow STABLEEXPRESSION { AGGREGATE_RESULTS.out.top_stable_genes_summary.set { ch_top_stable_genes_summary } AGGREGATE_RESULTS.out.top_stable_genes_transposed_counts_filtered.set { ch_top_stable_genes_transposed_counts } - } + // ----------------------------------------------------------------- + // DASH APPLICATION + // ----------------------------------------------------------------- - // ----------------------------------------------------------------- - // DASH APPLICATION - // ----------------------------------------------------------------- + DASH_APP( + ch_all_counts, + ch_whole_design, + ch_all_genes_summary + ) + ch_versions = ch_versions.mix ( DASH_APP.out.versions ) - DASH_APP( - ch_all_counts, - ch_whole_design, - ch_all_genes_summary - ) - ch_versions = ch_versions.mix ( DASH_APP.out.versions ) + ch_multiqc_files + .mix( ch_top_stable_genes_summary.collect() ) + .mix( ch_all_genes_summary.collect() ) + .mix( ch_top_stable_genes_transposed_counts.collect() ) + .set { ch_multiqc_files } + + } // ----------------------------------------------------------------- // MULTIQC // ----------------------------------------------------------------- - Channel.empty() - .mix( ch_top_stable_genes_summary.collect() ) - .mix( ch_all_genes_summary.collect() ) - .mix( ch_top_stable_genes_transposed_counts.collect() ) - .mix( Channel.topic('eatlas_all_datasets').collect() ) - .mix( Channel.topic('eatlas_selected_datasets').collect() ) - .mix( Channel.topic('geo_all_datasets').collect() ) - .mix( Channel.topic('geo_selected_datasets').collect() ) - .mix( Channel.topic('geo_wrong_species_datasets').collect() ) - .mix( Channel.topic('geo_wrong_platform_moltype_datasets').collect() ) - .mix( Channel.topic('geo_wrong_keywords_datasets').collect() ) - .set { ch_multiqc_files } - MULTIQC_WORKFLOW( ch_multiqc_files, ch_versions From 387126f65179d677f209f74c0036105df703e88c Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 5 Nov 2025 19:33:58 +0100 Subject: [PATCH 128/258] refactor eatlas and geo to display rejected datasets clearly in multiqc; display failures and warnings of eatlas and geo downloads in multiqc --- assets/multiqc_config.yml | 79 +++-- bin/download_eatlas_data.R | 7 +- bin/download_geo_data.R | 16 +- bin/get_geo_dataset_accessions.py | 318 ++++++------------ bin/map_ids_to_ensembl.py | 8 +- modules/local/expressionatlas/getdata/main.nf | 3 +- modules/local/geo/getaccessions/main.nf | 7 +- modules/local/geo/getdata/main.nf | 33 +- modules/local/gprofiler/idmapping/main.nf | 33 +- subworkflows/local/multiqc/main.nf | 70 ++++ 10 files changed, 252 insertions(+), 322 deletions(-) diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index bbb9051c..57abef96 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -485,11 +485,10 @@ custom_data: eatlas_selected_experiments_metadata: section_name: "Selected" parent_id: eatlas - parent_name: "Expression Atlas dataset metadata" + parent_name: "Expression Atlas datasets" parent_description: "Information about the Expression Atlas datasets processed in the analysis" file_format: "tsv" no_violin: true - sort_rows: false description: | Metadata of selected Expression Atlas datasets corresponding to the provided species (and optionally the provided keywords) plot_type: "table" @@ -497,15 +496,36 @@ custom_data: eatlas_all_experiments_metadata: section_name: "All datasets" parent_id: eatlas - parent_name: "Expression Atlas dataset metadata" + parent_name: "Expression Atlas datasets" parent_description: "Information about the Expression Atlas datasets processed in the analysis" file_format: "tsv" no_violin: true - sort_rows: false description: | Metadata of all Expression Atlas datasets corresponding to the provided species plot_type: "table" + eatlas_failure_reasons: + section_name: "Failure reasons" + parent_id: eatlas + parent_name: "Expression Atlas datasets" + parent_description: "Information about the Expression Atlas datasets processed in the analysis" + file_format: "csv" + no_violin: true + description: | + Reasons of failure during download of Expression Atlas datasets + plot_type: "table" + + eatlas_warning_reasons: + section_name: "Warnings" + parent_id: eatlas + parent_name: "Expression Atlas datasets" + parent_description: "Information about the Expression Atlas datasets processed in the analysis" + file_format: "csv" + no_violin: true + description: | + Warnings during download of Expression Atlas datasets + plot_type: "table" + geo_selected_experiments_metadata: section_name: "Selected" parent_id: geo @@ -513,7 +533,6 @@ custom_data: parent_description: "Information about the GEO datasets processed in the analysis" file_format: "tsv" no_violin: true - sort_rows: false description: | Metadata of selected GEO datasets corresponding to the provided species (and optionally the provided keywords) plot_type: "table" @@ -521,49 +540,45 @@ custom_data: geo_all_experiments_metadata: section_name: "All datasets" parent_id: geo - parent_name: "GEO dataset metadata" + parent_name: "GEO datasets" parent_description: "Information about the GEO datasets processed in the analysis" file_format: "tsv" no_violin: true - sort_rows: false description: | Metadata of all GEO datasets corresponding to the provided species plot_type: "table" - geo_wrong_species_experiments_metadata: - section_name: "Datasets rejected due to species mismatch" + geo_rejected_experiments_metadata: + section_name: "Rejected GEO datasets" parent_id: geo - parent_name: "GEO dataset metadata" + parent_name: "GEO datasets" parent_description: "Information about the GEO datasets processed in the analysis" file_format: "tsv" no_violin: true - sort_rows: false description: | - Metadata of all GEO datasets which were rejected due to species mismatch + Metadata of all GEO datasets which were rejected plot_type: "table" - geo_wrong_platform_moltype_experiments_metadata: - section_name: "Datasets rejected due to platform / molecule type mismatch" + geo_failure_reasons: + section_name: "Failure reasons" parent_id: geo - parent_name: "GEO dataset metadata" + parent_name: "GEO datasets" parent_description: "Information about the GEO datasets processed in the analysis" - file_format: "tsv" + file_format: "csv" no_violin: true - sort_rows: false description: | - Metadata of all GEO datasets which were rejected due to platform / molecule type mismatch + Reasons of failure during download of GEO datasets plot_type: "table" - geo_wrong_keywords_experiments_metadata: - section_name: "Datasets rejected due to keywords mismatch" + geo_warning_reasons: + section_name: "Warnings" parent_id: geo - parent_name: "GEO dataset metadata" + parent_name: "GEO datasets" parent_description: "Information about the GEO datasets processed in the analysis" - file_format: "tsv" + file_format: "csv" no_violin: true - sort_rows: false description: | - Metadata of all GEO datasets which were rejected due to keywords mismatch + Warnings during download of GEO datasets plot_type: "table" #violin_downsample_after: 10000 @@ -590,13 +605,17 @@ sp: fn: "*selected_experiments.metadata.tsv" eatlas_all_experiments_metadata: fn: "*species_experiments.metadata.tsv" + eatlas_failure_reasons: + fn: "*eatlas_failure_reasons.csv" + eatlas_warning_reasons: + fn: "*eatlas_warning_reasons.csv" geo_selected_experiments_metadata: fn: "*geo_selected_datasets.metadata.tsv" geo_all_experiments_metadata: fn: "*geo_all_datasets.metadata.tsv" - geo_wrong_species_experiments_metadata: - fn: "*geo_wrong_species_datasets.metadata.tsv" - geo_wrong_platform_moltype_experiments_metadata: - fn: "*geo_wrong_platform_moltype_datasets.metadata.tsv" - geo_wrong_keywords_experiments_metadata: - fn: "*geo_wrong_keywords_datasets.metadata.tsv" + geo_rejected_experiments_metadata: + fn: "*geo_rejected_datasets.metadata.tsv" + geo_failure_reasons: + fn: "*geo_failure_reasons.csv" + geo_warning_reasons: + fn: "*geo_warning_reasons.csv" diff --git a/bin/download_eatlas_data.R b/bin/download_eatlas_data.R index 092a26a7..5f50edec 100755 --- a/bin/download_eatlas_data.R +++ b/bin/download_eatlas_data.R @@ -8,6 +8,7 @@ library(ExpressionAtlas) library(optparse) FAILURE_REASON_FILE <- "failure_reason.txt" +WARNING_REASON_FILE <- "warning_reason.txt" ##################################################### @@ -171,12 +172,12 @@ process_data <- function(atlas_data, accession) { } else if ( startsWith(data_type, 'A-') ) { # typically: A-AFFY- or A-GEOD- result <- get_one_colour_microarray_data(data) } else { - write(cat("UNKNOWN DATA TYPE: ", data_type), file = FAILURE_REASON_FILE) + write(paste("UNKNOWN DATA TYPE:", data_type), file = FAILURE_REASON_FILE) } }, error = function(e) { - print(paste("Caught an error: ", e$message)) - print(paste('ERROR: Could not get assay data for experiment ID', accession, 'and data type', data_type)) + warning(paste("Caught an error: ", e$message)) + write(paste('ERROR: COULD NOT GET ASSAY DATA FOR EXPERIMENT ID', accession, 'AND DATA TYPE', data_type), file = WARNING_REASON_FILE) skip_iteration <<- TRUE }) diff --git a/bin/download_geo_data.R b/bin/download_geo_data.R index e9036655..212811b2 100755 --- a/bin/download_geo_data.R +++ b/bin/download_geo_data.R @@ -10,6 +10,9 @@ library(dplyr) options(error = traceback) +FAILURE_REASON_FILE <- "failure_reason.txt" +WARNING_REASON_FILE <- "warning_reason.txt" + ##################################################### ##################################################### # FUNCTIONS @@ -116,7 +119,7 @@ download_geo_data_with_retries <- function(accession, species, max_retries = 3, } else { warning("Unhandled error: ", e$message) - quit(save = "no", status = 100) # quit & stop workflow + write("EXPERIMENT NOT FOUND", file = FAILURE_REASON_FILE) } }) @@ -139,13 +142,13 @@ check_microarray_normalisation <- function(df) { message("Normalized, log2 scale (e.g. RMA, quantile)") } else if (all_integers) { message("Raw probe intensities (unnormalized CEL-like data)") - #quit(save = "no", status = 110) + write("RAW PROBE INTENSITIES FOUND", file = WARNING_REASON_FILE) } else if (value_range[2] > 1000) { message("Normalized but not log-transformed (e.g. MAS5, raw intensities)") - #quit(save = "no", status = 111) + write("PARSED INTENSITIES: NORMALIZED BUT NOT LOG-TRANSFORMED", file = WARNING_REASON_FILE) } else { message("Unclear data origin, check GEO metadata") - #quit(save = "no", status = 112) + write("UNCLEAR DATA ORIGIN: CHECK GEO METADATA", file = WARNING_REASON_FILE) } } @@ -175,7 +178,7 @@ process_data <- function(atlas_data, accession, species) { if ( length(names(geo_data)) > 1 ) { warning("Multiple data files were found") - quit(save = "no", status = 101) # quit & ignore process + write("EXPERIMENT CONTAINS MULTIPLE FILES", file = FAILURE_REASON_FILE) } file <- names(geo_data)[[ 1 ]] @@ -244,6 +247,3 @@ species <- format_species_name(args$species) geo_data <- download_geo_data_with_retries(args$accession, species) process_data(geo_data, args$accession, args$species) - - - diff --git a/bin/get_geo_dataset_accessions.py b/bin/get_geo_dataset_accessions.py index 3513e43d..bfdcd639 100755 --- a/bin/get_geo_dataset_accessions.py +++ b/bin/get_geo_dataset_accessions.py @@ -37,24 +37,18 @@ ACCESSION_OUTFILE_NAME = "accessions.txt" SPECIES_DATASETS_OUTFILE_NAME = "geo_all_datasets.metadata.tsv" -WRONG_SPECIES_DATASETS_METADATA_OUTFILE_NAME = "geo_wrong_species_datasets.metadata.tsv" -WRONG_SPECS_DATASETS_METADATA_OUTFILE_NAME = ( - "geo_wrong_platform_moltype_datasets.metadata.tsv" -) -WRONG_KEYWORDS_DATASETS_METADATA_OUTFILE_NAME = ( - "geo_wrong_keywords_datasets.metadata.tsv" -) +REJECTED_DATASETS_OUTFILE_NAME = "geo_rejected_datasets.metadata.tsv" +# WRONG_SPECS_DATASETS_METADATA_OUTFILE_NAME = "geo_wrong_platform_moltype_datasets.metadata.tsv" +# WRONG_KEYWORDS_DATASETS_METADATA_OUTFILE_NAME = "geo_wrong_keywords_datasets.metadata.tsv" # PLATFORM_NOT_AVAILABLE_DATASETS_METADATA_OUTFILE_NAME = "platform_not_available_datasets.metadata.tsv" # GENE_ID_MAPPING_ISSUES_DATASETS_METADATA_OUTFILE_NAME = "gene_id_mapping_issues_datasets.metadata.tsv" -FINAL_DATASETS_METADATA_OUTFILE_NAME = "geo_selected_datasets.metadata.tsv" +SELECTED_DATASETS_OUTFILE_NAME = "geo_selected_datasets.metadata.tsv" ENTREZ_QUERY_MAX_RESULTS = 9999 ENTREZ_EMAIL = "stableexpression@nfcore.com" # PLATFORM_METADATA_CHUNKSIZE = 2000 # NCBI_API_BASE_URL = "https://www.ncbi.nlm.nih.gov/geo/query/acc.cgi?view=data&acc={accession}" - - STOP_RETRY_AFTER_DELAY = 600 NB_PROBE_IDS_TO_PARSE = 1000 @@ -530,6 +524,7 @@ def parse_interesting_metadata( return { "accession": dataset_metadata["Accession"], + "taxon": dataset_metadata["taxon"], "platform_accessions": platform_accessions, "summary": dataset_metadata["summary"], "title": dataset_metadata["title"], @@ -590,136 +585,116 @@ def exclude_unwanted_accessions( return datasets_to_keep -def species_is_ok(dataset: dict, species: str) -> bool: - accession = dataset["Accession"] - # we want datasets only specific to the species we are interested in - parsed_species_list = dataset["taxon"].split("; ") - if not parsed_species_list: - logger.warning(f"Accession {accession} rejected: Could not detect species.") - return False +def check_species_issues(parsed_species_list: list, species: str) -> dict: # trying to find our species in the list of species parsed for parsed_species in parsed_species_list: if format_species(parsed_species) == format_species(species): - if len(parsed_species_list) > 1: - logger.info( - f"Accession {accession}: multiple species detected = {parsed_species_list}" - ) - return True - logger.warning( - f"Accession {accession} rejected: Found wrong species = {parsed_species_list}" - ) - return False + return {} + return {"parsed_species": parsed_species_list} -def contains_only_rna(molecules_types: list, accession: str) -> bool: +def check_molecule_type_issues(molecules_types: list) -> dict: # we want only GEO series that contain only RNA molecules # for other series, they should be superseries contained other series that are being parsed too # so anyway, this would lead in duplicates if all(["rna" in molecule_type.lower() for molecule_type in molecules_types]): - return True - logger.info(f"Accession {accession} rejected: Molecule type(s) = {molecules_types}") - return False + return {} + return {"molecule_types": molecules_types} -def contains_proper_experiment_type( - experiment_types: list, accession: str, platform: str -) -> bool: +def check_experiment_type_issues(experiment_types: list | str, platform: str) -> dict: experiment_types = ( experiment_types if isinstance(experiment_types, list) else [experiment_types] ) for experiment_type in experiment_types: # if at least one experiment type is ok, we keep this dataset if GEO_EXPERIMENT_TYPE_TO_PLATFORM.get(experiment_type) == platform: - return True - logger.info( - f"Accession {accession} rejected: Experiment type(s) = {experiment_types}" - ) - return False + return {} + return {"experiment_types": experiment_types} -def contains_transcriptomic_source(library_sources: list, accession: str) -> bool: +def check_source_issues(library_sources: list) -> dict: # if we have no data about library sources, we just cannot infer if not library_sources: - return True + return {} + if len(library_sources) == 1 and library_sources[0] in ALLOWED_LIBRARY_SOURCES: + return {} # TODO: see how to process series with multiple library sources - if len(library_sources) > 1: - return False - if library_sources[0] in ALLOWED_LIBRARY_SOURCES: - return True - logger.warning(f"Accession {accession} rejected: Source(s) = {library_sources}") - return False + return {"library_sources": library_sources} + + +def search_keywords(dataset: dict, keywords: list[str]) -> tuple[list, dict]: + accession = dataset["accession"] + all_searchable_fields = ( + [dataset["summary"], dataset["title"]] + + dataset["sample_characteristics"] + + dataset["sample_descriptions"] + + dataset["sample_titles"] + ) + found_keywords = keywords_in_fields(all_searchable_fields, keywords) + # only returning experiments if found keywords + if found_keywords: + dataset["found_keywords"] = list(set(found_keywords)) + logger.info(f"Found keywords: {found_keywords} in accession {accession}") + return found_keywords, {} + else: + return [], {"accession": accession, "keywords_found": False} + + +def check_dataset( + dataset: dict, species: str, platform: str | None, keywords: list[str] | None +) -> tuple[list, dict]: + accession = dataset["accession"] + parsed_species_list = dataset["taxon"].split("; ") + experiment_types = dataset["experiment_types"] + library_sources = dataset["sample_library_sources"] + molecules_types = dataset["sample_molecule_types"] + # checking species + issue_dict = check_species_issues(parsed_species_list, species) -def dataset_is_valid(metadata: dict, platform: str | None) -> bool: - accession = metadata["accession"] # checking platform if platform is not None: - if not contains_proper_experiment_type( - metadata["experiment_types"], accession, platform - ): - return False + platform_issue_dict = check_experiment_type_issues(experiment_types, platform) + issue_dict |= platform_issue_dict # checking that library sources fit - if not contains_transcriptomic_source( - metadata["sample_library_sources"], accession - ): - return False + transcriptomic_issue_dict = check_source_issues(library_sources) + issue_dict |= transcriptomic_issue_dict # checking that all molecule types are RNA - molecules_types = metadata["sample_molecule_types"] - if not contains_only_rna(molecules_types, accession): - return False - - return True + moltype_issue_dict = check_molecule_type_issues(molecules_types) + issue_dict |= moltype_issue_dict + found_keywords = [] + if keywords: + found_keywords, keyword_issue_dict = search_keywords(dataset, keywords) + issue_dict |= keyword_issue_dict -def filter_metadata_with_keywords(metadata: dict, keywords: list[str]) -> dict | None: - all_searchable_fields = ( - [metadata["summary"], metadata["title"]] - + metadata["sample_characteristics"] - + metadata["sample_descriptions"] - + metadata["sample_titles"] - ) - found_keywords = keywords_in_fields(all_searchable_fields, keywords) - # only returning experiments if found keywords - if found_keywords: - metadata["found_keywords"] = list(set(found_keywords)) - logger.info( - f"Found keywords: {found_keywords} in accession {metadata['accession']}" - ) - return metadata + if issue_dict: + rejection_dict = {"accession": accession, "reasons": issue_dict} else: - return None + rejection_dict = {} + + return found_keywords, rejection_dict -def export_filtered_out_datasets_if_any( - original_dataset_metadata_list: list[dict], - filtered_dataset_metadata_list: list[dict], - filtered_out_outfile_name: str, - filtered_feature: str, +def export_dataset_metadatas( + datasets: list[dict], output_file: str, clean_columns: bool = True ): - # checking if all datasets were ok - filtered_out_dataset_metadata_list = [ - dataset - for dataset in original_dataset_metadata_list - if dataset not in filtered_dataset_metadata_list - ] - if filtered_out_dataset_metadata_list: - logger.warning( - f"{len(filtered_out_dataset_metadata_list)} dataset(s) did not have the correct {filtered_feature}!" - ) - logger.info( - f"Writing metadata of datasets corresponding to the wrong {filtered_feature} to {filtered_out_outfile_name}" - ) - df = pd.DataFrame.from_dict(filtered_out_dataset_metadata_list) + if datasets: + df = pd.DataFrame.from_dict(datasets) + # cleaning columns so that MultiQC can parse them + if clean_columns: + for col in df.columns: + df[col] = df[col].astype(str).str.replace("\n", "") + df[col] = df[col].astype(str).str.replace("\t", "") df.to_csv( - filtered_out_outfile_name, + output_file, sep="\t", index=False, header=True, ) - else: - logger.info(f"All datasets had the correct {filtered_feature}") ################################################################## @@ -732,28 +707,13 @@ def export_filtered_out_datasets_if_any( def main(): args = parse_args() - selected_accessions = [] - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # PARSING GEO DATASETS # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ logger.info(f"Getting datasets corresponding to species {args.species}") - dataset_metadata_list = fetch_geo_datasets_for_species(args.species) - logger.info( - f"Found {len(dataset_metadata_list)} datasets for species {args.species}" - ) - - if dataset_metadata_list: - logger.info( - f"Writing metadata of all experiments for species {args.species} to {SPECIES_DATASETS_OUTFILE_NAME}" - ) - formated_dataset_metadata_list = [ - {k: v for k, v in r.items() if k not in ["Item", "Id"]} - for r in dataset_metadata_list - ] - df = pd.DataFrame.from_dict(formated_dataset_metadata_list) - df.to_csv(SPECIES_DATASETS_OUTFILE_NAME, sep="\t", index=False, header=True) + datasets = fetch_geo_datasets_for_species(args.species) + logger.info(f"Found {len(datasets)} datasets for species {args.species}") # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # FOR DEV PURPOSES / TESTING: RESTRICT TO SPECIFIC ACCESSIONS @@ -762,12 +722,8 @@ def main(): if args.accessions: logger.info(f"Keeping only accessions {args.accessions}") dev_accessions = args.accessions.split(",") - dataset_metadata_list = [ - d for d in dataset_metadata_list if d["Accession"] in dev_accessions - ] - logger.info( - f"Kept {len(dataset_metadata_list)} datasets for dev / testing purposes" - ) + datasets = [d for d in datasets if d["Accession"] in dev_accessions] + logger.info(f"Kept {len(datasets)} datasets for dev / testing purposes") # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # EXCLUDING UNWANTED ACCESSIONS @@ -775,99 +731,45 @@ def main(): if args.excluded_accessions_file: logger.info("Excluding unwanted datasets") - dataset_metadata_list = exclude_unwanted_accessions( - dataset_metadata_list, args.excluded_accessions_file - ) + datasets = exclude_unwanted_accessions(datasets, args.excluded_accessions_file) logger.info( - f"{len(dataset_metadata_list)} datasets remaining after excluding unwanted accessions" + f"{len(datasets)} datasets remaining after excluding unwanted accessions" ) - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # EXCLUDING DATASETS WITH THE WRONG SPECIES - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - logger.info(f"Excluding wrong species for {len(dataset_metadata_list)} datasets") - good_species_dataset_metadata_list = [ - dataset - for dataset in dataset_metadata_list - if species_is_ok(dataset, args.species) - ] - - export_filtered_out_datasets_if_any( - dataset_metadata_list, - good_species_dataset_metadata_list, - WRONG_SPECIES_DATASETS_METADATA_OUTFILE_NAME, - "species", - ) - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # PARSING METADATA # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - logger.info(f"Parsing metadata for {len(dataset_metadata_list)} datasets") - augmented_dataset_metadata_list = [] + logger.info(f"Parsing metadata for {len(datasets)} datasets") + augmented_datasets = [] with ( Pool(processes=args.nb_cpus) as p, - tqdm(total=len(good_species_dataset_metadata_list)) as pbar, + tqdm(total=len(datasets)) as pbar, ): - for result in p.imap_unordered( - parse_metadata, good_species_dataset_metadata_list - ): + for result in p.imap_unordered(parse_metadata, datasets): pbar.update() pbar.refresh() if result is None: continue - augmented_dataset_metadata_list.append(result) - - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # CHECKING MOLECULE TYPE / PLATFORM TECHNOLOGIES - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - logger.info(f"Validating {len(augmented_dataset_metadata_list)} datasets") - specs_filtered_metadata_list = [ - metadata - for metadata in augmented_dataset_metadata_list - if dataset_is_valid(metadata, args.platform) - ] - - export_filtered_out_datasets_if_any( - augmented_dataset_metadata_list, - specs_filtered_metadata_list, - WRONG_SPECS_DATASETS_METADATA_OUTFILE_NAME, - "molecule type / platform technology", - ) + augmented_datasets.append(result) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # FILTERING WITH KEYWORDS + # CHECKING DATASET METADATA # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - if args.keywords: - logger.info( - f"Filtering experiments with keywords {args.keywords} for {len(specs_filtered_metadata_list)} datasets" + logger.info(f"Validating {len(augmented_datasets)} datasets") + selected_datasets = [] + rejected_datasets = [] + for dataset in tqdm(augmented_datasets): + found_keywords, rejection_dict = check_dataset( + dataset, args.species, args.platform, args.keywords ) - func = partial(filter_metadata_with_keywords, keywords=args.keywords) - - final_metadata_list = [] - with ( - Pool(processes=args.nb_cpus) as p, - tqdm(total=len(specs_filtered_metadata_list)) as pbar, - ): - for result in p.imap_unordered(func, specs_filtered_metadata_list): - pbar.update() - pbar.refresh() - if result is None: - continue - final_metadata_list.append(result) - - export_filtered_out_datasets_if_any( - specs_filtered_metadata_list, - final_metadata_list, - WRONG_KEYWORDS_DATASETS_METADATA_OUTFILE_NAME, - "keywords", - ) - - else: - final_metadata_list = specs_filtered_metadata_list + if rejection_dict: + rejected_datasets.append(rejection_dict) + else: + if found_keywords: + dataset["found_keywords"] = found_keywords + selected_datasets.append(dataset) """ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -925,27 +827,25 @@ def main(): """ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # GETTING ACCESSIONS TO DOWNLOAD + # EXPORTING ACCESSIONS # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - logger.info(f"Kept {len(final_metadata_list)} datasets") + logger.info(f"Kept {len(selected_datasets)} datasets") # getting accessions of selected experiments - selected_accessions = [metadata["accession"] for metadata in final_metadata_list] - + selected_accessions = [metadata["accession"] for metadata in selected_datasets] # exporting list of accessions logger.info(f"Writing accessions to {ACCESSION_OUTFILE_NAME}") with open(ACCESSION_OUTFILE_NAME, "w") as fout: fout.writelines([f"{acc}\n" for acc in selected_accessions]) - logger.info( - f"Writing metadata of selected datasets to {FINAL_DATASETS_METADATA_OUTFILE_NAME}" - ) - df = pd.DataFrame.from_dict(final_metadata_list) - df.to_csv( - FINAL_DATASETS_METADATA_OUTFILE_NAME, - sep="\t", - index=False, - header=True, + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # EXPORTING DATASETS + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + export_dataset_metadatas(augmented_datasets, SPECIES_DATASETS_OUTFILE_NAME) + export_dataset_metadatas(selected_datasets, SELECTED_DATASETS_OUTFILE_NAME) + export_dataset_metadatas( + rejected_datasets, REJECTED_DATASETS_OUTFILE_NAME, clean_columns=False ) diff --git a/bin/map_ids_to_ensembl.py b/bin/map_ids_to_ensembl.py index b8f0ec2a..0afbb884 100755 --- a/bin/map_ids_to_ensembl.py +++ b/bin/map_ids_to_ensembl.py @@ -72,8 +72,8 @@ def main(): ############################################################# df = pd.read_csv(count_file, header=0, index_col=0) if df.empty: - logger.error("Count file is empty! Aborting ID mapping...") - sys.exit(100) + logger.warning("Count file is empty! Aborting ID mapping...") + sys.exit(0) df.index = df.index.astype(str) gene_ids = df.index.tolist() @@ -106,13 +106,13 @@ def main(): # if mapping dict is empty if not mapping_dict: - logger.error( + logger.warning( f"No mapping found for gene names in count file {count_file.name} " f"and for species {args.species}! " f"Example of gene names found in the provided dataframe: {df.index[:5].tolist()}" f"Count file is empty! Aborting ID mapping..." ) - sys.exit(101) + sys.exit(0) #############################################################" # MAPPING GENE IDS IN DATAFRAME diff --git a/modules/local/expressionatlas/getdata/main.nf b/modules/local/expressionatlas/getdata/main.nf index 30efcd73..e4327b4e 100644 --- a/modules/local/expressionatlas/getdata/main.nf +++ b/modules/local/expressionatlas/getdata/main.nf @@ -17,7 +17,8 @@ process EXPRESSIONATLAS_GETDATA { output: path "*.design.csv", optional: true, emit: design path "*.counts.csv", optional: true, emit: counts - path "failure_reason.txt", optional: true, topic: eatlas_failure_reason + tuple val(accession), path("failure_reason.txt"), optional: true, topic: eatlas_failure_reason + tuple val(accession), path("warning_reason.txt"), optional: true, topic: eatlas_warning_reason tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //"'), topic: versions tuple val("${task.process}"), val('ExpressionAtlas'), eval('Rscript -e "cat(as.character(packageVersion(\'ExpressionAtlas\')))"'), topic: versions diff --git a/modules/local/geo/getaccessions/main.nf b/modules/local/geo/getaccessions/main.nf index c46ca58f..8423d228 100644 --- a/modules/local/geo/getaccessions/main.nf +++ b/modules/local/geo/getaccessions/main.nf @@ -18,12 +18,7 @@ process GEO_GETACCESSIONS { path "accessions.txt", emit: accessions path "geo_selected_datasets.metadata.tsv", topic: geo_selected_datasets path "geo_all_datasets.metadata.tsv", topic: geo_all_datasets - path "geo_wrong_species_datasets.metadata.tsv", optional: true, topic: geo_wrong_species_datasets - path "geo_wrong_platform_moltype_datasets.metadata.tsv", optional: true, topic: geo_wrong_platform_moltype_datasets - path "geo_wrong_keywords_datasets.metadata.tsv", optional: true, topic: geo_wrong_keywords_datasets - //path "platform_not_available_datasets.metadata.tsv", optional: true, emit: platform_not_available_datasets_metadata - //path "gene_id_mapping_issues_datasets.metadata.tsv", optional: true, emit: gene_id_mapping_issues_datasets_metadata - + path "geo_rejected_datasets.metadata.tsv", optional: true, topic: geo_rejected_datasets tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('requests'), eval('python3 -c "import requests; print(requests.__version__)"'), topic: versions diff --git a/modules/local/geo/getdata/main.nf b/modules/local/geo/getdata/main.nf index bafc68b9..9add5b60 100644 --- a/modules/local/geo/getdata/main.nf +++ b/modules/local/geo/getdata/main.nf @@ -6,37 +6,6 @@ process GEO_GETDATA { maxForks 8 // limiting to 8 threads at a time to avoid 429 errors with the Expression Atlas API server - errorStrategy { - if (task.exitStatus == 100) { - // ignoring accessions that cannot be retrieved from GEO - log.warn("Could not retrieve data for accession ${accession}. This could be a transient network issue or a permission error.") - return 'ignore' - } else if (task.exitStatus == 101) { - log.warn("GEO dataset with accession ${accession} contains multiple files.") - return 'ignore' - } else if (task.exitStatus == 110) { - log.warn("GEO dataset for accession ${accession} does not seem normalised.") - return 'ignore' - } else if (task.exitStatus == 111) { - log.warn("GEO dataset for accession ${accession} seems normalised but not log-transformed.") - return 'ignore' - } else if (task.exitStatus == 112) { - log.warn("GEO dataset for accession ${accession} are of unclear origin. Could not infer normalisation state.") - return 'ignore' - } else if ( task.exitStatus in ((130..145) + 104 + 175) ) { // override default behaviour to sleep some time before retry - // in case of OOM errors, we wait a bit and try again (2 retries) - if ( task.attempt <= 2) { - sleep(Math.pow(2, task.attempt) * 2000 as long) - return 'retry' - } else { - log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") - return 'ignore' - } - } else { - return 'ignore' - } - } - conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/4c/4cb08d96e62942e7b6288abf2cfd30e813521a022459700e610325a3a7c0b1c8/data': @@ -49,6 +18,8 @@ process GEO_GETDATA { output: path "*.design.csv", optional: true, emit: design path "*.counts.csv", optional: true, emit: counts + tuple val(accession), path("failure_reason.txt"), optional: true, topic: geo_failure_reason + tuple val(accession), path("warning_reason.txt"), optional: true, topic: geo_warning_reason tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //"'), topic: versions tuple val("${task.process}"), val('GEOquery'), eval('Rscript -e "cat(as.character(packageVersion(\'GEOquery\')))"'), topic: versions tuple val("${task.process}"), val('dplyr'), eval('Rscript -e "cat(as.character(packageVersion(\'dplyr\')))"'), topic: versions diff --git a/modules/local/gprofiler/idmapping/main.nf b/modules/local/gprofiler/idmapping/main.nf index 01b3c2af..d7500a95 100644 --- a/modules/local/gprofiler/idmapping/main.nf +++ b/modules/local/gprofiler/idmapping/main.nf @@ -7,33 +7,6 @@ process GPROFILER_IDMAPPING { // limiting to 8 threads at a time to avoid 429 errors with the G Profiler API server maxForks 8 - errorStrategy { - if (task.exitStatus == 100) { - // ignoring cases when the count dataframe is empty - log.warn("Count file is empty for dataset ${meta.dataset}.") - return 'ignore' - } else if (task.exitStatus == 101) { - // likewise, when no mapping could be found, we do not want to continue with the subsequent steps for this specific dataset - log.warn("Could not map gene IDs to Ensembl for dataset ${meta.dataset}.") - return 'ignore' - } else if (task.exitStatus == 102) { - // if the server appears to be down, we stop immediately - log.error("gProfiler server appears to be down, stopping pipeline") - return 'terminate' - } else if ( task.exitStatus in ((130..145) + 104 + 175) ) { // override default behaviour to sleep some time before retry - // in case of OOM errors, we wait a bit and try again (2 retries) - if ( task.attempt <= 2) { - sleep(Math.pow(2, task.attempt) * 2000 as long) - return 'retry' - } else { - log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") - return 'ignore' - } - } else { - return 'finish' - } - } - conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/5c/5c28c8e613c062828aaee4b950029bc90a1a1aa94d5f61016a588c8ec7be8b65/data': @@ -46,9 +19,9 @@ process GPROFILER_IDMAPPING { val gene_metadata_file output: - tuple val(meta), path('*.renamed.csv'), emit: counts - path('*.metadata.csv'), optional: true, emit: metadata - path('*.mapping.csv'), optional: true, emit: mapping + tuple val(meta), path('*.renamed.csv'), optional: true, emit: counts + path('*.metadata.csv'), optional: true, emit: metadata + path('*.mapping.csv'), optional: true, emit: mapping tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions tuple val("${task.process}"), val('requests'), eval('python3 -c "import requests; print(requests.__version__)"'), topic: versions diff --git a/subworkflows/local/multiqc/main.nf b/subworkflows/local/multiqc/main.nf index f4c37be8..5669ecf2 100644 --- a/subworkflows/local/multiqc/main.nf +++ b/subworkflows/local/multiqc/main.nf @@ -20,6 +20,76 @@ workflow MULTIQC_WORKFLOW { main: + // ------------------------------------------------------------------------------------ + // FAILURE / WARNING REPORTS + // ------------------------------------------------------------------------------------ + + Channel.topic('eatlas_failure_reason') + .map { accession, file -> [ accession, file.readLines()[0] ] } + .collectFile( + name: 'eatlas_failure_reasons.csv', + seed: "Accession,Reason", + newLine: true, + storeDir: "${params.outdir}/errors/" + ) { + item -> "${item[0]},${item[1]}" + } + .set { ch_eatlas_failure_reasons } + + Channel.topic('eatlas_warning_reason') + .map { accession, file -> [ accession, file.readLines()[0] ] } + .collectFile( + name: 'eatlas_warning_reasons.csv', + seed: "Accession,Reason", + newLine: true, + storeDir: "${params.outdir}/warnings/" + ) { + item -> "${item[0]},${item[1]}" + } + .set { ch_eatlas_warning_reasons } + + Channel.topic('geo_failure_reason') + .map { accession, file -> [ accession, file.readLines()[0] ] } + .collectFile( + name: 'geo_failure_reasons.csv', + seed: "Accession,Reason", + newLine: true, + storeDir: "${params.outdir}/errors/" + ) { + item -> "${item[0]},${item[1]}" + } + .set { ch_geo_failure_reasons } + + Channel.topic('geo_warning_reason') + .map { accession, file -> [ accession, file.readLines()[0] ] } + .collectFile( + name: 'geo_warning_reasons.csv', + seed: "Accession,Reason", + newLine: true, + storeDir: "${params.outdir}/warnings/" + ) { + item -> "${item[0]},${item[1]}" + } + .set { ch_geo_warning_reasons } + + + + // ------------------------------------------------------------------------------------ + // MULTIQC FILES + // ------------------------------------------------------------------------------------ + + ch_multiqc_files + .mix( Channel.topic('eatlas_all_datasets').collect() ) + .mix( Channel.topic('eatlas_selected_datasets').collect() ) + .mix( ch_eatlas_failure_reasons ) + .mix( ch_eatlas_warning_reasons ) + .mix( Channel.topic('geo_all_datasets').collect() ) + .mix( Channel.topic('geo_selected_datasets').collect() ) + .mix( Channel.topic('geo_rejected_datasets').collect() ) + .mix( ch_geo_failure_reasons ) + .mix( ch_geo_warning_reasons ) + .set { ch_multiqc_files } + // ------------------------------------------------------------------------------------ // VERSIONS // ------------------------------------------------------------------------------------ From d6d0b3d12f2b3adfb951308a3cccd0b5a4f03ba4 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 5 Nov 2025 20:39:06 +0100 Subject: [PATCH 129/258] add failure report of id mapping in multiqc --- assets/multiqc_config.yml | 13 ++++++++++++ bin/map_ids_to_ensembl.py | 24 +++++++++++++---------- conf/test_full.config | 1 - modules/local/gprofiler/idmapping/main.nf | 7 ++++--- subworkflows/local/multiqc/main.nf | 13 ++++++++++++ tests/default.nf.test | 1 - 6 files changed, 44 insertions(+), 15 deletions(-) diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index 57abef96..32cfccb3 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -581,6 +581,17 @@ custom_data: Warnings during download of GEO datasets plot_type: "table" + id_mapping_failure_reasons: + section_name: "Failure reasons" + parent_id: idmapping + parent_name: "ID mapping" + parent_description: "Information about the ID mapping" + file_format: "tsv" + no_violin: true + description: | + Reasons of failure during ID mapping + plot_type: "table" + #violin_downsample_after: 10000 log_filesize_limit: 10000000000 # 10GB @@ -619,3 +630,5 @@ sp: fn: "*geo_failure_reasons.csv" geo_warning_reasons: fn: "*geo_warning_reasons.csv" + id_mapping_failure_reasons: + fn: "*id_mapping_failure_reasons.tsv" diff --git a/bin/map_ids_to_ensembl.py b/bin/map_ids_to_ensembl.py index 0afbb884..c18b0476 100755 --- a/bin/map_ids_to_ensembl.py +++ b/bin/map_ids_to_ensembl.py @@ -23,6 +23,8 @@ METADATA_FILE_SUFFIX = ".metadata.csv" MAPPING_FILE_SUFFIX = ".mapping.csv" +FAILURE_REASON_FILE = "failure_reason.txt" + ################################################################## # FUNCTIONS ################################################################## @@ -67,12 +69,16 @@ def main(): f"Converting IDs for species {args.species} and count file {count_file.name}..." ) - #############################################################" + ############################################################# # PARSING FILES ############################################################# df = pd.read_csv(count_file, header=0, index_col=0) + if df.empty: - logger.warning("Count file is empty! Aborting ID mapping...") + msg = "COUNT FILE IS EMPTY" + logger.warning(msg) + with open(FAILURE_REASON_FILE, "w") as f: + f.write(msg) sys.exit(0) df.index = df.index.astype(str) @@ -106,15 +112,13 @@ def main(): # if mapping dict is empty if not mapping_dict: - logger.warning( - f"No mapping found for gene names in count file {count_file.name} " - f"and for species {args.species}! " - f"Example of gene names found in the provided dataframe: {df.index[:5].tolist()}" - f"Count file is empty! Aborting ID mapping..." - ) + msg = f"NO MAPPING FOR GENE IDS: {', '.join(df.index[:5].tolist())}, ..." + logger.warning(msg) + with open(FAILURE_REASON_FILE, "w") as f: + f.write(msg) sys.exit(0) - #############################################################" + ############################################################# # MAPPING GENE IDS IN DATAFRAME ############################################################# @@ -132,7 +136,7 @@ def main(): # for now, we just get the mean of values, but this is not ideal df = df.groupby(config.ENSEMBL_GENE_ID_COLNAME, as_index=False).mean() - #############################################################" + ############################################################# # WRITING OUTFILES ############################################################# # writing to output file diff --git a/conf/test_full.config b/conf/test_full.config index c6b599d7..6ed4fd55 100644 --- a/conf/test_full.config +++ b/conf/test_full.config @@ -18,5 +18,4 @@ params { // Input data species = 'solanum_tuberosum' outdir = "results/test_full" - run_genorm = true } diff --git a/modules/local/gprofiler/idmapping/main.nf b/modules/local/gprofiler/idmapping/main.nf index d7500a95..e585821c 100644 --- a/modules/local/gprofiler/idmapping/main.nf +++ b/modules/local/gprofiler/idmapping/main.nf @@ -19,9 +19,10 @@ process GPROFILER_IDMAPPING { val gene_metadata_file output: - tuple val(meta), path('*.renamed.csv'), optional: true, emit: counts - path('*.metadata.csv'), optional: true, emit: metadata - path('*.mapping.csv'), optional: true, emit: mapping + tuple val(meta), path('*.renamed.csv'), optional: true, emit: counts + path('*.metadata.csv'), optional: true, emit: metadata + path('*.mapping.csv'), optional: true, emit: mapping + tuple val(meta.dataset), path("failure_reason.txt"), optional: true, topic: id_mapping_failure_reason tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions tuple val("${task.process}"), val('requests'), eval('python3 -c "import requests; print(requests.__version__)"'), topic: versions diff --git a/subworkflows/local/multiqc/main.nf b/subworkflows/local/multiqc/main.nf index 5669ecf2..0e9e4b62 100644 --- a/subworkflows/local/multiqc/main.nf +++ b/subworkflows/local/multiqc/main.nf @@ -72,6 +72,18 @@ workflow MULTIQC_WORKFLOW { } .set { ch_geo_warning_reasons } + Channel.topic('id_mapping_failure_reason') + .map { accession, file -> [ accession, file.readLines()[0] ] } + .collectFile( + name: 'id_mapping_failure_reasons.tsv', + seed: "Dataset\tReason", + newLine: true, + storeDir: "${params.outdir}/warnings/" + ) { + item -> "${item[0]}\t${item[1]}" + } + .set { ch_id_mapping_failure_reasons } + // ------------------------------------------------------------------------------------ @@ -88,6 +100,7 @@ workflow MULTIQC_WORKFLOW { .mix( Channel.topic('geo_rejected_datasets').collect() ) .mix( ch_geo_failure_reasons ) .mix( ch_geo_warning_reasons ) + .mix( ch_id_mapping_failure_reasons ) .set { ch_multiqc_files } // ------------------------------------------------------------------------------------ diff --git a/tests/default.nf.test b/tests/default.nf.test index 85581225..1521e78e 100644 --- a/tests/default.nf.test +++ b/tests/default.nf.test @@ -277,7 +277,6 @@ nextflow_pipeline { when { params { species = 'solanum_tuberosum' - run_genorm = true outdir = "$outputDir" } } From 07f343e4b20b38045b934d4e755928e946f65c8a Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 5 Nov 2025 21:24:31 +0100 Subject: [PATCH 130/258] fix issue with expression atlas get data files not optional --- bin/get_eatlas_accessions.py | 9 --------- modules/local/expressionatlas/getaccessions/main.nf | 10 +++++----- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/bin/get_eatlas_accessions.py b/bin/get_eatlas_accessions.py index 5172f5c5..48926676 100755 --- a/bin/get_eatlas_accessions.py +++ b/bin/get_eatlas_accessions.py @@ -389,15 +389,6 @@ def main(): with open(ACCESSION_OUTFILE_NAME, "w") as fout: fout.writelines([f"{acc}\n" for acc in selected_accessions]) - """ - # exporting metadata - logger.info( - f"Writing metadata of all experiments to {ALL_EXPERIMENTS_METADATA_OUTFILE_NAME}" - ) - df = pd.DataFrame.from_dict(all_experiments) - df.to_csv(ALL_EXPERIMENTS_METADATA_OUTFILE_NAME, sep="\t", index=False, header=True) - """ - # exporting metadata logger.info( f"Writing metadata of all experiments for species {species_name} to {SPECIES_EXPERIMENTS_METADATA_OUTFILE_NAME}" diff --git a/modules/local/expressionatlas/getaccessions/main.nf b/modules/local/expressionatlas/getaccessions/main.nf index 41a01a89..b3b10dbc 100644 --- a/modules/local/expressionatlas/getaccessions/main.nf +++ b/modules/local/expressionatlas/getaccessions/main.nf @@ -13,11 +13,11 @@ process EXPRESSIONATLAS_GETACCESSIONS { val platform output: - path "accessions.txt", emit: accessions - path "selected_experiments.metadata.tsv", topic: eatlas_selected_datasets - path "species_experiments.metadata.tsv", topic: eatlas_all_datasets - //path "filtered_experiments.metadata.tsv", optional: true, topic: filtered_eatlas_experiment_metadata - //path "filtered_experiments.keywords.yaml", optional: true, topic: filtered_eatlas_experiment_keywords + path "accessions.txt", optional: true, emit: accessions + path "selected_experiments.metadata.tsv", optional: true, topic: eatlas_selected_datasets + path "species_experiments.metadata.tsv", optional: true, topic: eatlas_all_datasets + //path "filtered_experiments.metadata.tsv", optional: true, topic: filtered_eatlas_experiment_metadata + //path "filtered_experiments.keywords.yaml", optional: true, topic: filtered_eatlas_experiment_keywords tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('requests'), eval('python3 -c "import requests; print(requests.__version__)"'), topic: versions tuple val("${task.process}"), val('nltk'), eval('python3 -c "import nltk; print(nltk.__version__)"'), topic: versions From 4d7b2857910b8bb2ee42fda1036df9abe8233e02 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Fri, 7 Nov 2025 15:43:05 +0100 Subject: [PATCH 131/258] fix strict synthax issues --- modules/local/clean_count_data/main.nf | 17 ---------------- modules/local/compute_base_statistics/main.nf | 18 ----------------- modules/local/normalisation/deseq2/main.nf | 20 ------------------- modules/local/normalisation/edger/main.nf | 20 ------------------- modules/local/normfinder/main.nf | 18 ----------------- nextflow.config | 8 -------- 6 files changed, 101 deletions(-) diff --git a/modules/local/clean_count_data/main.nf b/modules/local/clean_count_data/main.nf index bd25f3ce..9fbe4a7b 100644 --- a/modules/local/clean_count_data/main.nf +++ b/modules/local/clean_count_data/main.nf @@ -4,23 +4,6 @@ process CLEAN_COUNT_DATA { tag "${meta.dataset}" - errorStrategy { - if (task.exitStatus == 101) { - return 'ignore' - } else if ( task.exitStatus in ((130..145) + 104 + 175) ) { // override default behaviour to sleep some time before retry - // in case of OOM errors, we wait a bit and try again (2 retries) - if ( task.attempt <= 2) { - sleep(Math.pow(2, task.attempt) * 2000 as long) - return 'retry' - } else { - log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") - return 'ignore' - } - } else { - return 'finish' - } - } - conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': diff --git a/modules/local/compute_base_statistics/main.nf b/modules/local/compute_base_statistics/main.nf index 995967e3..e5287ecd 100644 --- a/modules/local/compute_base_statistics/main.nf +++ b/modules/local/compute_base_statistics/main.nf @@ -2,24 +2,6 @@ process COMPUTE_BASE_STATISTICS { label 'process_medium' - errorStrategy { - if (task.exitStatus == 100) { - log.error("No count could be found before merging datasets! Please check the provided accessions and datasets and run again") - return 'terminate' - } else if ( task.exitStatus in ((130..145) + 104 + 175) ) { // override default behaviour to sleep some time before retry - // in case of OOM errors, we wait a bit and try again (2 retries) - if ( task.attempt <= 2) { - sleep(Math.pow(2, task.attempt) * 2000 as long) - return 'retry' - } else { - log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") - return 'ignore' - } - } else { - return 'terminate' - } - } - conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': diff --git a/modules/local/normalisation/deseq2/main.nf b/modules/local/normalisation/deseq2/main.nf index dee09859..60f5deee 100644 --- a/modules/local/normalisation/deseq2/main.nf +++ b/modules/local/normalisation/deseq2/main.nf @@ -4,26 +4,6 @@ process NORMALISATION_DESEQ2 { tag "${meta.dataset}" - errorStrategy { - if (task.exitStatus == 100) { - // ignoring cases when the count dataframe gets empty after filtering (the script throws a 100 in this case) - // the subsequent steps will not be run for this dataset - log.warn("No genes left after pre-filtering for dataset ${meta.dataset}.") - return 'ignore' - } else if ( task.exitStatus in ((130..145) + 104 + 175) ) { // override default behaviour to sleep some time before retry - // in case of OOM errors, we wait a bit and try again (2 retries) - if ( task.attempt <= 2) { - sleep(Math.pow(2, task.attempt) * 2000 as long) - return 'retry' - } else { - log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") - return 'ignore' - } - } else { - return 'finish' - } - } - conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/ce/cef7164b168e74e5db11dcd9acf6172d47ed6753e4814c68f39835d0c6c22f6d/data': diff --git a/modules/local/normalisation/edger/main.nf b/modules/local/normalisation/edger/main.nf index 1b46b061..bf0fa6aa 100644 --- a/modules/local/normalisation/edger/main.nf +++ b/modules/local/normalisation/edger/main.nf @@ -4,26 +4,6 @@ process NORMALISATION_EDGER { tag "${meta.dataset}" - errorStrategy { - if (task.exitStatus == 100) { - // ignoring cases when the count dataframe gets empty after filtering (the script throws a 100 in this case) - // the subsequent steps will not be run for this dataset - log.warn("No genes left after pre-filtering for dataset ${meta.dataset}.") - return 'ignore' - } else if ( task.exitStatus in ((130..145) + 104 + 175) ) { // override default behaviour to sleep some time before retry - // in case of OOM errors, we wait a bit and try again (2 retries) - if ( task.attempt <= 2) { - sleep(Math.pow(2, task.attempt) * 2000 as long) - return 'retry' - } else { - log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") - return 'ignore' - } - } else { - return 'finish' - } - } - conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/89/89bbc9544e18b624ed6d0a30e701cf8cec63e063cc9b5243e1efde362fe92228/data': diff --git a/modules/local/normfinder/main.nf b/modules/local/normfinder/main.nf index 797cadf2..48094ca4 100644 --- a/modules/local/normfinder/main.nf +++ b/modules/local/normfinder/main.nf @@ -2,24 +2,6 @@ process NORMFINDER { label 'process_high' - errorStrategy { - if (task.exitStatus == 100) { - log.warn("Too few genes to run NormFinder.") - return 'ignore' - } else if ( task.exitStatus in ((130..145) + 104 + 175) ) { // override default behaviour to sleep some time before retry - // in case of OOM errors, we wait a bit and try again (2 retries) - if ( task.attempt <= 2) { - sleep(Math.pow(2, task.attempt) * 2000 as long) - return 'retry' - } else { - log.error("${accession} caused Out of Memory error multiple times. Ignoring this accession.") - return 'ignore' - } - } else { - return 'ignore' - } - } - conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0e/0e0445114887dd260f1632afe116b1e81e02e1acc74a86adca55099469b490d9/data': diff --git a/nextflow.config b/nextflow.config index 8590173b..6bc3f0c5 100644 --- a/nextflow.config +++ b/nextflow.config @@ -225,17 +225,9 @@ profiles { } test { includeConfig 'conf/test.config' } - test_eatlas_only_with_keywords { includeConfig 'conf/test_eatlas_only_with_keywords.config' } - test_run_normfinder_genorm { includeConfig 'conf/test_run_normfinder_genorm.config' } - test_ignore_errors { includeConfig 'conf/test_ignore_errors.config' } - test_eatlas_only { includeConfig 'conf/test_eatlas_only.config' } test_full { includeConfig 'conf/test_full.config' } - test_dataset_custom_mapping { includeConfig 'conf/test_dataset_custom_mapping.config' } - test_one_accession { includeConfig 'conf/test_one_accession.config' } - test_one_accession_low_gene_count { includeConfig 'conf/test_one_accession_low_gene_count.config' } test_local_and_downloaded { includeConfig 'conf/test_local_and_downloaded.config' } test_one_rnaseq_one_microarray { includeConfig 'conf/test_one_rnaseq_one_microarray.config' } - test_eatlas_geo { includeConfig 'conf/test_eatlas_geo.config' } local { includeConfig 'conf/local.config' } } From 441b73aefadd7a76528c1432da8e20538e395bcf Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Fri, 7 Nov 2025 15:43:28 +0100 Subject: [PATCH 132/258] parse geo platform taxon and propagate in meta map --- bin/get_geo_dataset_accessions.py | 114 +++++++++++------- modules/local/geo/getaccessions/main.nf | 6 +- modules/local/geo/getdata/main.nf | 6 +- .../local/expressionatlas_fetchdata/main.nf | 4 +- subworkflows/local/geo_fetchdata/main.nf | 78 +++++++----- .../main.nf | 2 +- workflows/stableexpression.nf | 18 ++- 7 files changed, 136 insertions(+), 92 deletions(-) diff --git a/bin/get_geo_dataset_accessions.py b/bin/get_geo_dataset_accessions.py index bfdcd639..98d5cc32 100755 --- a/bin/get_geo_dataset_accessions.py +++ b/bin/get_geo_dataset_accessions.py @@ -9,8 +9,7 @@ from pathlib import Path # from random import sample -# import re -# import requests +import requests import pandas as pd import xmltodict from urllib.request import urlretrieve @@ -21,12 +20,11 @@ wait_exponential, before_sleep_log, ) -from functools import partial import logging from requests.exceptions import HTTPError, ConnectionError from natural_language_utils import keywords_in_fields -# from gprofiler_utils import convert_ids, chunk_list +from gprofiler_utils import chunk_list logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -35,7 +33,7 @@ # mandatory for running the script in an apptainer container # Entrez.Parser.Parser.directory("/tmp/biopython") -ACCESSION_OUTFILE_NAME = "accessions.txt" +ACCESSION_OUTFILE_NAME = "accessions.tsv" SPECIES_DATASETS_OUTFILE_NAME = "geo_all_datasets.metadata.tsv" REJECTED_DATASETS_OUTFILE_NAME = "geo_rejected_datasets.metadata.tsv" # WRONG_SPECS_DATASETS_METADATA_OUTFILE_NAME = "geo_wrong_platform_moltype_datasets.metadata.tsv" @@ -46,7 +44,7 @@ ENTREZ_QUERY_MAX_RESULTS = 9999 ENTREZ_EMAIL = "stableexpression@nfcore.com" -# PLATFORM_METADATA_CHUNKSIZE = 2000 +PLATFORM_METADATA_CHUNKSIZE = 2000 # NCBI_API_BASE_URL = "https://www.ncbi.nlm.nih.gov/geo/query/acc.cgi?view=data&acc={accession}" STOP_RETRY_AFTER_DELAY = 600 @@ -160,7 +158,6 @@ def send_request_to_entrez_esummary(ids: list[str]) -> list[dict]: return Entrez.read(handle) -""" @retry( stop=stop_after_delay(STOP_RETRY_AFTER_DELAY), wait=wait_exponential(multiplier=1, min=1, max=30), @@ -197,7 +194,6 @@ def send_request_to_ncbi_api(accession: str) -> requests.Response | None: ) return response -""" @retry( @@ -329,38 +325,74 @@ def fetch_geo_platform_data(platform_accessions: list[str]) -> dict: return results -def get_platform_metadata(selected_metadata_chunk_list: list[dict]) -> list[dict]: +def augment_with_platform_metadata( + datasets: list[dict], +) -> tuple[list[dict], list[dict]]: # unique list of platform accessions platform_accessions = list( set( [ platform_accession - for metadata in selected_metadata_chunk_list - for platform_accession in metadata["platform_accessions"] + for dataset in datasets + for platform_accession in dataset["platform_accessions"] ] ) ) # one single request to NCBI for all platform accessions # we extract the platform accessions to allow better parsing afterwards - pltf_acc_to_pltf_metadata = { + acc_to_metadata = { platform_metadata["Accession"]: platform_metadata for platform_metadata in fetch_geo_platform_data(platform_accessions) } # adding the platform metadata to the corresponding metadata + issues = [] augmented_metadata_list = [] - for metadata in selected_metadata_chunk_list: - metadata["platform_metadata"] = [] - for platform_accession in metadata["platform_accessions"]: - # augmenting metadata with platform metadata + for dataset in datasets: + accession = dataset["accession"] + platform_accessions = dataset["platform_accessions"] + dataset["platform_metadata"] = [] + + if not platform_accessions: + issues.append({"accession": accession, "reason": "NO PLATFORM ACCESSIONS"}) + continue + + for platform_accession in platform_accessions: # filtering out cases where the platform metadata is not available - if platform_accession in pltf_acc_to_pltf_metadata: - metadata["platform_metadata"].append( - pltf_acc_to_pltf_metadata[platform_accession] - ) - augmented_metadata_list.append(metadata) + if platform_accession not in acc_to_metadata: + continue + # augmenting metadata with platform metadata + dataset["platform_metadata"].append(acc_to_metadata[platform_accession]) + + # getting list of platform taxon + platforms_taxons = [ + platform_metadata.get("taxon") + for platform_metadata in dataset["platform_metadata"] + if platform_metadata.get("taxon") is not None + ] + + # checking if there is one single platform taxon + # otherwise, checking the dataset + if not platforms_taxons: + logger.warning(f"No taxon found for dataset {accession}") + issues.append({"accession": accession, "reason": "NO PLATFORM TAXON"}) + continue + elif len(platforms_taxons) > 1: + logger.warning( + f"Multiple taxons for dataset {accession}: {platforms_taxons}" + ) + issues.append( + { + "accession": accession, + "reason": f"MULTIPLE PLATFORM TAXONS: {platforms_taxons}", + } + ) + continue - return augmented_metadata_list + dataset["platform_taxon"] = platforms_taxons[0] + augmented_metadata_list.append(dataset) + + return augmented_metadata_list, issues """ @@ -771,30 +803,24 @@ def main(): dataset["found_keywords"] = found_keywords selected_datasets.append(dataset) - """ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # GETTING METADATA OF SEQUENCING PLATFORMS # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - logger.info( - f"Getting platform metadata for {len(keywords_filtered_metadata_list)} datasets" + logger.info(f"Getting platform metadata for {len(selected_datasets)} datasets") + selected_datasets_chunks = chunk_list( + selected_datasets, PLATFORM_METADATA_CHUNKSIZE ) - platform_augmented_dataset_metadata_list = [] - for selected_metadata_chunk_list in tqdm( - chunk_list(keywords_filtered_metadata_list, PLATFORM_METADATA_CHUNKSIZE) - ): - platform_augmented_dataset_metadata_list += get_platform_metadata( - selected_metadata_chunk_list + # resetting selecting datasets + selected_datasets = [] + for selected_datasets_chunk in tqdm(selected_datasets_chunks): + augmented_datasets, issues = augment_with_platform_metadata( + selected_datasets_chunk ) + selected_datasets += augmented_datasets + rejected_datasets += issues - export_filtered_out_datasets_if_any( - keywords_filtered_metadata_list, - platform_augmented_dataset_metadata_list, - PLATFORM_NOT_AVAILABLE_DATASETS_METADATA_OUTFILE_NAME, - "platform metadata", - ) - - + """ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # FILTERING OUT DATASETS FOR WHICH ID MAPPING DOES NOT WORK # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -832,11 +858,11 @@ def main(): logger.info(f"Kept {len(selected_datasets)} datasets") # getting accessions of selected experiments - selected_accessions = [metadata["accession"] for metadata in selected_datasets] - # exporting list of accessions - logger.info(f"Writing accessions to {ACCESSION_OUTFILE_NAME}") - with open(ACCESSION_OUTFILE_NAME, "w") as fout: - fout.writelines([f"{acc}\n" for acc in selected_accessions]) + selected_accessions = [ + {"accession": dataset["accession"], "platform_taxon": dataset["platform_taxon"]} + for dataset in selected_datasets + ] + export_dataset_metadatas(selected_accessions, ACCESSION_OUTFILE_NAME) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # EXPORTING DATASETS diff --git a/modules/local/geo/getaccessions/main.nf b/modules/local/geo/getaccessions/main.nf index 8423d228..f7b7f1e8 100644 --- a/modules/local/geo/getaccessions/main.nf +++ b/modules/local/geo/getaccessions/main.nf @@ -15,9 +15,9 @@ process GEO_GETACCESSIONS { val accessions output: - path "accessions.txt", emit: accessions - path "geo_selected_datasets.metadata.tsv", topic: geo_selected_datasets - path "geo_all_datasets.metadata.tsv", topic: geo_all_datasets + path "accessions.tsv", emit: accessions + path "geo_selected_datasets.metadata.tsv", optional: true, topic: geo_selected_datasets + path "geo_all_datasets.metadata.tsv", optional: true, topic: geo_all_datasets path "geo_rejected_datasets.metadata.tsv", optional: true, topic: geo_rejected_datasets tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions diff --git a/modules/local/geo/getdata/main.nf b/modules/local/geo/getdata/main.nf index 9add5b60..b96e801e 100644 --- a/modules/local/geo/getdata/main.nf +++ b/modules/local/geo/getdata/main.nf @@ -12,12 +12,12 @@ process GEO_GETDATA { 'community.wave.seqera.io/library/bioconductor-geoquery_r-base_r-dplyr_r-optparse:fcd002470b7d6809' }" input: - val accession + tuple val(meta), val(accession) val species output: - path "*.design.csv", optional: true, emit: design - path "*.counts.csv", optional: true, emit: counts + tuple val(meta), path("*.counts.csv"), optional: true, emit: counts + tuple val(meta), path("*.design.csv"), optional: true, emit: design tuple val(accession), path("failure_reason.txt"), optional: true, topic: geo_failure_reason tuple val(accession), path("warning_reason.txt"), optional: true, topic: geo_warning_reason tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //"'), topic: versions diff --git a/subworkflows/local/expressionatlas_fetchdata/main.nf b/subworkflows/local/expressionatlas_fetchdata/main.nf index 2c0e04d3..23dad91d 100644 --- a/subworkflows/local/expressionatlas_fetchdata/main.nf +++ b/subworkflows/local/expressionatlas_fetchdata/main.nf @@ -3,7 +3,7 @@ include { EXPRESSIONATLAS_GETDATA } from '../../../modules/local/ include { addDatasetIdToMetadata } from '../utils_nfcore_stableexpression_pipeline' include { groupFilesByDatasetId } from '../utils_nfcore_stableexpression_pipeline' -include { augmentToMetadata } from '../utils_nfcore_stableexpression_pipeline' +include { augmentMetadata } from '../utils_nfcore_stableexpression_pipeline' /* ======================================================================================== @@ -86,7 +86,7 @@ workflow EXPRESSIONATLAS_FETCHDATA { ch_eatlas_datasets = groupFilesByDatasetId( ch_design, ch_counts ) // adding normalisation state in the meta - augmentToMetadata( ch_eatlas_datasets ) + augmentMetadata( ch_eatlas_datasets ) } diff --git a/subworkflows/local/geo_fetchdata/main.nf b/subworkflows/local/geo_fetchdata/main.nf index 31ee4e32..6594ce5b 100644 --- a/subworkflows/local/geo_fetchdata/main.nf +++ b/subworkflows/local/geo_fetchdata/main.nf @@ -2,7 +2,7 @@ include { GEO_GETACCESSIONS } from '../../../modules/local/geo/getacces include { GEO_GETDATA } from '../../../modules/local/geo/getdata' include { addDatasetIdToMetadata } from '../utils_nfcore_stableexpression_pipeline' include { groupFilesByDatasetId } from '../utils_nfcore_stableexpression_pipeline' -include { augmentToMetadata } from '../utils_nfcore_stableexpression_pipeline' +include { augmentMetadata } from '../utils_nfcore_stableexpression_pipeline' /* ======================================================================================== @@ -13,39 +13,57 @@ include { augmentToMetadata } from '../utils_nfcore_stableexpression_pi workflow GEO_FETCHDATA { take: - ch_species - ch_excluded_accessions + species + ch_eatlas_excluded_accessions main: ch_datasets = Channel.empty() ch_fetched_accessions = Channel.empty() - ch_geo_accessions_file = params.geo_accessions_file ? Channel.fromPath(params.geo_accessions_file, checkIfExists: true) : Channel.empty() + // ------------------------------------------------------------------------------------ + // PREPARE EXCLUDED ACCESSIONS + // ------------------------------------------------------------------------------------ - Channel.fromList( params.geo_accessions.tokenize(',') ) - .mix( ch_geo_accessions_file.splitText() ) - .unique() - .map { acc -> acc.trim() } - .set { ch_input_accessions } + // getting accessions to exclude from GEO + ch_eatlas_excluded_accessions + .filter { accession -> accession.startsWith("E-GEOD-") } + .map { accession -> accession.replace("E-GEOD-", "GSE") } + .set { ch_excluded_eatlas_accessions } - // fetching GEO accessions if applicable - if ( !params.skip_fetch_geo_accessions ) { + // parsing file listing excluded accessions + ch_exclude_geo_accessions_file = params.exclude_geo_accessions_file ? Channel.fromPath(params.exclude_geo_accessions_file, checkIfExists: true) : Channel.empty() + + // getting accessions to exclude and preparing in the right format + Channel.fromList( params.exclude_geo_accessions.tokenize(',') ) + .mix( ch_excluded_eatlas_accessions ) + .mix( ch_exclude_geo_accessions_file.splitText() ) + .unique() + .map { acc -> acc.trim() } // removing spaces + .set { ch_excluded_accessions } ch_excluded_accessions .collectFile( name: 'excluded_geo_accessions.txt', + storeDir: "${params.outdir}/geo/", sort: true, newLine: true ) .ifEmpty('none') .set { ch_excluded_accessions_file } + // ------------------------------------------------------------------------------------ + // GET GEO ACCESSIONS + // ------------------------------------------------------------------------------------ + + // fetching GEO accessions if applicable + if ( !params.skip_fetch_geo_accessions ) { + // getting GEO accessions given a species name and keywords // keywords can be an empty string - def platform = params.platform?: 'none' + def platform = params.platform ?: 'none' GEO_GETACCESSIONS( - ch_species, + species, params.keywords, platform, ch_excluded_accessions_file, @@ -53,41 +71,43 @@ workflow GEO_FETCHDATA { ) GEO_GETACCESSIONS.out.accessions - .splitText() + .splitCsv(header: true, sep: '\t') + .map { row -> [ [ platform_taxon: row["platform_taxon"] ], row["accession"] ] } .set { ch_fetched_accessions } } - ch_exclude_geo_accessions_file = params.exclude_geo_accessions_file ? Channel.fromPath(params.exclude_geo_accessions_file, checkIfExists: true) : Channel.empty() + // ------------------------------------------------------------------------------------ + // PREPARE ACCESSIONS PROVIDED BY THE USER + // ------------------------------------------------------------------------------------ - // getting accessions to exclude and preparing in the right format - Channel.fromList( params.exclude_geo_accessions.tokenize(',') ) - .mix( ch_exclude_geo_accessions_file.splitText() ) + ch_geo_accessions_file = params.geo_accessions_file ? Channel.fromPath(params.geo_accessions_file, checkIfExists: true) : Channel.empty() + + Channel.fromList( params.geo_accessions.tokenize(',') ) + .mix( ch_geo_accessions_file.splitText() ) .unique() + .filter { acc -> acc.startsWith('GSE') } .map { acc -> acc.trim() } - .toList() - .map { lst -> [lst] } // list of lists : mandatory when combining in the next step - .set { ch_excluded_accessions } + .set { ch_input_accessions } // appending to accessions provided by the user // ensures that no accessions is present twice (provided by the user and fetched from GEO) - // removing excluded accessions ch_input_accessions + .map { accession -> [ [ platform_taxon: species ], accession ] } .mix( ch_fetched_accessions ) .unique() - .map { acc -> acc.trim() } - .filter { acc -> acc.startsWith('GSE') } - .combine ( ch_excluded_accessions ) - .filter { accession, excluded_accessions -> !(accession in excluded_accessions) } - .map { accession, excluded_accessions -> accession } .set { ch_accessions } + // ------------------------------------------------------------------------------------ + // DOWNLOAD GEO DATASETS + // ------------------------------------------------------------------------------------ + if ( !params.accessions_only ) { // Downloading GEO datasets for each accession in ch_accessions GEO_GETDATA( ch_accessions, - ch_species + species ) // adding dataset id (accession + data_type) in the file meta @@ -98,7 +118,7 @@ workflow GEO_FETCHDATA { ch_datasets = groupFilesByDatasetId( ch_design, ch_counts ) // adding normalisation state in the meta - augmentToMetadata( ch_datasets ) + augmentMetadata( ch_datasets ) } diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index 01ac44e9..da64fe09 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -355,7 +355,7 @@ def getNthPartFromEnd(String s, int n) { // // Add normalised: true / false in meta // -def augmentToMetadata( ch_files ) { +def augmentMetadata( ch_files ) { return ch_files .map { meta, file -> diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 62099720..53c1b937 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -41,23 +41,21 @@ workflow STABLEEXPRESSION { ch_all_genes_statistics = Channel.empty() ch_top_stable_genes_transposed_counts = Channel.empty() - ch_species = Channel.value( params.species.split(' ').join('_') ) + def species = params.species.split(' ').join('_') // ----------------------------------------------------------------- // FETCH AND DOWNLOAD EXPRESSION ATLAS DATASETS IF NEEDED // ----------------------------------------------------------------- - EXPRESSIONATLAS_FETCHDATA( ch_species ) + EXPRESSIONATLAS_FETCHDATA( species ) - // getting accessions to exclude from GEO - EXPRESSIONATLAS_FETCHDATA.out.accessions - .filter { accession -> accession.startsWith("E-GEOD-") } - .map { accession -> accession.replace("E-GEOD-", "GSE")} - .set { ch_excluded_geo_accessions } + // ----------------------------------------------------------------- + // FETCH AND DOWNLOAD GEO DATASETS IF NEEDED + // ----------------------------------------------------------------- GEO_FETCHDATA ( - ch_species, - ch_excluded_geo_accessions + species, + EXPRESSIONATLAS_FETCHDATA.out.accessions ) if ( !params.accessions_only && !params.download_only ) { @@ -82,7 +80,7 @@ workflow STABLEEXPRESSION { // tries to map gene IDs to Ensembl IDs whenever possible GPROFILER_IDMAPPING( ch_counts, - ch_species, + species, ch_gene_id_mapping, ch_gene_metadata ) From a163678948ceee99da595550a0cdc70ce8ebd8a5 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Fri, 7 Nov 2025 17:20:52 +0100 Subject: [PATCH 133/258] mapping gene ids against geo platform taxon instead of species --- bin/download_geo_data.R | 3 +- modules/local/expressionatlas/getdata/main.nf | 7 +-- modules/local/geo/getdata/main.nf | 1 + modules/local/gprofiler/idmapping/main.nf | 2 +- .../local/expressionatlas_fetchdata/main.nf | 4 +- subworkflows/local/geo_fetchdata/main.nf | 4 +- subworkflows/local/idmapping/main.nf | 46 +++++++++++++++++++ .../main.nf | 6 +-- workflows/stableexpression.nf | 24 +++++----- 9 files changed, 73 insertions(+), 24 deletions(-) create mode 100644 subworkflows/local/idmapping/main.nf diff --git a/bin/download_geo_data.R b/bin/download_geo_data.R index 212811b2..91f9944a 100755 --- a/bin/download_geo_data.R +++ b/bin/download_geo_data.R @@ -184,11 +184,12 @@ process_data <- function(atlas_data, accession, species) { file <- names(geo_data)[[ 1 ]] data <- geo_data [[ file ]] + #print(fData(data)) # get count data for samples corresponding to the species of interest count_df <- data.frame(exprs(data)) %>% select(all_of(species_samples)) - + print(count_df) # checking that data are from RMA pipeline and followed proper normalisation # raises error otherwise check_microarray_normalisation(count_df) diff --git a/modules/local/expressionatlas/getdata/main.nf b/modules/local/expressionatlas/getdata/main.nf index e4327b4e..f907e9a9 100644 --- a/modules/local/expressionatlas/getdata/main.nf +++ b/modules/local/expressionatlas/getdata/main.nf @@ -12,17 +12,18 @@ process EXPRESSIONATLAS_GETDATA { 'community.wave.seqera.io/library/bioconductor-expressionatlas_r-base_r-optparse:ca0f8cd9d3f44af9' }" input: - val(accession) + val accession output: - path "*.design.csv", optional: true, emit: design - path "*.counts.csv", optional: true, emit: counts + tuple val(meta), path("*.counts.csv"), optional: true, emit: counts + tuple val(meta), path("*.design.csv"), optional: true, emit: design tuple val(accession), path("failure_reason.txt"), optional: true, topic: eatlas_failure_reason tuple val(accession), path("warning_reason.txt"), optional: true, topic: eatlas_warning_reason tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //"'), topic: versions tuple val("${task.process}"), val('ExpressionAtlas'), eval('Rscript -e "cat(as.character(packageVersion(\'ExpressionAtlas\')))"'), topic: versions script: + meta = [accession: accession] """ download_eatlas_data.R --accession $accession """ diff --git a/modules/local/geo/getdata/main.nf b/modules/local/geo/getdata/main.nf index b96e801e..410da0ba 100644 --- a/modules/local/geo/getdata/main.nf +++ b/modules/local/geo/getdata/main.nf @@ -25,6 +25,7 @@ process GEO_GETDATA { tuple val("${task.process}"), val('dplyr'), eval('Rscript -e "cat(as.character(packageVersion(\'dplyr\')))"'), topic: versions script: + meta = meta + [accession: accession] """ download_geo_data.R \\ --accession $accession \\ diff --git a/modules/local/gprofiler/idmapping/main.nf b/modules/local/gprofiler/idmapping/main.nf index e585821c..f3a35bb2 100644 --- a/modules/local/gprofiler/idmapping/main.nf +++ b/modules/local/gprofiler/idmapping/main.nf @@ -2,7 +2,7 @@ process GPROFILER_IDMAPPING { label 'process_single' - tag "${meta.dataset}" + tag "${meta.dataset} on ${meta.platform_taxon}" // limiting to 8 threads at a time to avoid 429 errors with the G Profiler API server maxForks 8 diff --git a/subworkflows/local/expressionatlas_fetchdata/main.nf b/subworkflows/local/expressionatlas_fetchdata/main.nf index 23dad91d..81d200aa 100644 --- a/subworkflows/local/expressionatlas_fetchdata/main.nf +++ b/subworkflows/local/expressionatlas_fetchdata/main.nf @@ -79,8 +79,8 @@ workflow EXPRESSIONATLAS_FETCHDATA { EXPRESSIONATLAS_GETDATA( ch_accessions ) // adding dataset id (accession + data_type) in the file meta - ch_design = addDatasetIdToMetadata( EXPRESSIONATLAS_GETDATA.out.design.flatten() ) - ch_counts = addDatasetIdToMetadata( EXPRESSIONATLAS_GETDATA.out.counts.flatten() ) + ch_design = addDatasetIdToMetadata( EXPRESSIONATLAS_GETDATA.out.design ) + ch_counts = addDatasetIdToMetadata( EXPRESSIONATLAS_GETDATA.out.counts ) // adding design files to the meta of their respective count files ch_eatlas_datasets = groupFilesByDatasetId( ch_design, ch_counts ) diff --git a/subworkflows/local/geo_fetchdata/main.nf b/subworkflows/local/geo_fetchdata/main.nf index 6594ce5b..62a2e5f4 100644 --- a/subworkflows/local/geo_fetchdata/main.nf +++ b/subworkflows/local/geo_fetchdata/main.nf @@ -111,8 +111,8 @@ workflow GEO_FETCHDATA { ) // adding dataset id (accession + data_type) in the file meta - ch_design = addDatasetIdToMetadata( GEO_GETDATA.out.design.flatten() ) - ch_counts = addDatasetIdToMetadata( GEO_GETDATA.out.counts.flatten() ) + ch_design = addDatasetIdToMetadata( GEO_GETDATA.out.design ) + ch_counts = addDatasetIdToMetadata( GEO_GETDATA.out.counts ) // adding design files to the meta of their respective count files ch_datasets = groupFilesByDatasetId( ch_design, ch_counts ) diff --git a/subworkflows/local/idmapping/main.nf b/subworkflows/local/idmapping/main.nf new file mode 100644 index 00000000..5d5c907f --- /dev/null +++ b/subworkflows/local/idmapping/main.nf @@ -0,0 +1,46 @@ +include { GPROFILER_IDMAPPING } from '../../../modules/local/gprofiler/idmapping' + +/* +======================================================================================== + SUBWORKFLOW TO DOWNLOAD EXPRESSIONATLAS ACCESSIONS AND DATASETS +======================================================================================== +*/ + +workflow ID_MAPPING { + + take: + ch_counts + species + ch_gene_id_mapping + ch_gene_metadata + + + main: + + ch_counts + .map { + meta, file -> + def platform_taxon = meta.platform_taxon ?: species + meta.platform_taxon = platform_taxon + [ meta, file ] + } + .view() + .set { ch_counts } + + GPROFILER_IDMAPPING( + ch_counts, + species, + ch_gene_id_mapping, + ch_gene_metadata + ) + + GPROFILER_IDMAPPING.out.counts.set { ch_counts } + GPROFILER_IDMAPPING.out.mapping.set { ch_gene_id_mapping } + GPROFILER_IDMAPPING.out.metadata.set { ch_gene_metadata } + + emit: + counts = GPROFILER_IDMAPPING.out.counts + mapping = GPROFILER_IDMAPPING.out.mapping + metadata = GPROFILER_IDMAPPING.out.metadata + +} diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index da64fe09..d6255bd6 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -319,9 +319,9 @@ def formatVersionsToYAML( ch_versions ) { def addDatasetIdToMetadata( ch_files ) { return ch_files .map { - file -> - def meta = [dataset: file.getSimpleName()] - [meta, file] + meta, file -> + def new_meta = meta + [ dataset: file.getSimpleName() ] + [new_meta, file] } } diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 53c1b937..dc63e515 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -6,6 +6,7 @@ include { EXPRESSIONATLAS_FETCHDATA } from '../subworkflows/local/expressionatlas_fetchdata' include { GEO_FETCHDATA } from '../subworkflows/local/geo_fetchdata' +include { ID_MAPPING } from '../subworkflows/local/idmapping' include { EXPRESSION_NORMALISATION } from '../subworkflows/local/expression_normalisation' include { DATA_CLEANSING } from '../subworkflows/local/data_cleansing' include { MERGE_DATA } from '../subworkflows/local/merge_data' @@ -13,7 +14,6 @@ include { BASE_STATISTICS } from '../subworkflows/local/b include { STABILITY_SCORING } from '../subworkflows/local/stability_scoring' include { MULTIQC_WORKFLOW } from '../subworkflows/local/multiqc' -include { GPROFILER_IDMAPPING } from '../modules/local/gprofiler/idmapping' include { AGGREGATE_RESULTS } from '../modules/local/aggregate_results' include { DASH_APP } from '../modules/local/dash_app' @@ -58,15 +58,15 @@ workflow STABLEEXPRESSION { EXPRESSIONATLAS_FETCHDATA.out.accessions ) - if ( !params.accessions_only && !params.download_only ) { + // putting all datasets together (local datasets + Expression Atlas datasets) + ch_input_datasets + .concat( EXPRESSIONATLAS_FETCHDATA.out.downloaded_datasets ) + .concat( GEO_FETCHDATA.out.downloaded_datasets ) + .set { ch_counts } - // putting all datasets together (local datasets + Expression Atlas datasets) - ch_input_datasets - .concat( EXPRESSIONATLAS_FETCHDATA.out.downloaded_datasets ) - .concat( GEO_FETCHDATA.out.downloaded_datasets ) - .set { ch_counts } + ch_counts = storeDatasetSize( ch_counts, "nb_genes", "nb_samples" ) - ch_counts = storeDatasetSize( ch_counts, "nb_genes", "nb_samples" ) + if ( !params.accessions_only && !params.download_only ) { // ----------------------------------------------------------------- // IDMAPPING @@ -78,15 +78,15 @@ workflow STABLEEXPRESSION { if ( !params.skip_gprofiler ) { // tries to map gene IDs to Ensembl IDs whenever possible - GPROFILER_IDMAPPING( + ID_MAPPING( ch_counts, species, ch_gene_id_mapping, ch_gene_metadata ) - GPROFILER_IDMAPPING.out.counts.set { ch_counts } - GPROFILER_IDMAPPING.out.mapping.set { ch_gene_id_mapping } - GPROFILER_IDMAPPING.out.metadata.set { ch_gene_metadata } + ID_MAPPING.out.counts.set { ch_counts } + ID_MAPPING.out.mapping.set { ch_gene_id_mapping } + ID_MAPPING.out.metadata.set { ch_gene_metadata } } From 6460cae93a0208ef6e8801e055f0ebe09347d40e Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Fri, 7 Nov 2025 18:01:41 +0100 Subject: [PATCH 134/258] add failures and warnings of normalisation and dataset cleaning to multiqc --- assets/multiqc_config.yml | 39 +++++++++++++++++++ bin/clean_count_data.py | 12 ++++-- bin/compute_base_statistics.py | 6 +-- bin/gprofiler_utils.py | 32 +++++++++------- bin/map_ids_to_ensembl.py | 22 +++++++---- bin/normalise_with_deseq2.R | 13 +++++-- bin/normalise_with_edger.R | 14 +++++-- modules/local/clean_count_data/main.nf | 3 +- modules/local/normalisation/deseq2/main.nf | 2 + modules/local/normalisation/edger/main.nf | 2 + subworkflows/local/idmapping/main.nf | 1 - subworkflows/local/multiqc/main.nf | 40 +++++++++++++++++++- subworkflows/local/stability_scoring/main.nf | 15 +++++--- workflows/stableexpression.nf | 7 +++- 14 files changed, 165 insertions(+), 43 deletions(-) diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index 32cfccb3..0f839b70 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -592,6 +592,39 @@ custom_data: Reasons of failure during ID mapping plot_type: "table" + normalisation_failure_reasons: + section_name: "Failure reasons" + parent_id: normalisation + parent_name: "Normalisation" + parent_description: "Information about the normalisation" + file_format: "tsv" + no_violin: true + description: | + Reasons of failure during Normalisation (DESeq2 or edgeR) + plot_type: "table" + + normalisation_warning_reasons: + section_name: "Warning reasons" + parent_id: normalisation + parent_name: "Normalisation" + parent_description: "Information about the normalisation" + file_format: "tsv" + no_violin: true + description: | + Reasons of failure during Normalisation (DESeq2 or edgeR) + plot_type: "table" + + clean_count_failure_reasons: + section_name: "Failure reasons" + parent_id: cleaning + parent_name: "Cleaning" + parent_description: "Information about the cleaning" + file_format: "tsv" + no_violin: true + description: | + Reasons of failure during dataset cleaning + plot_type: "table" + #violin_downsample_after: 10000 log_filesize_limit: 10000000000 # 10GB @@ -632,3 +665,9 @@ sp: fn: "*geo_warning_reasons.csv" id_mapping_failure_reasons: fn: "*id_mapping_failure_reasons.tsv" + normalisation_failure_reasons: + fn: "*normalisation_failure_reasons.csv" + normalisation_warning_reasons: + fn: "*normalisation_warning_reasons.csv" + clean_count_failure_reasons: + fn: "*clean_count_failure_reasons.csv" diff --git a/bin/clean_count_data.py b/bin/clean_count_data.py index 828904c3..b31c47ed 100755 --- a/bin/clean_count_data.py +++ b/bin/clean_count_data.py @@ -3,12 +3,12 @@ # Written by Olivier Coen. Released under the MIT license. import argparse +import logging import sys -import polars as pl from pathlib import Path -import logging import config +import polars as pl logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -16,6 +16,8 @@ # outfile names ALL_COUNTS_FILTERED_PARQUET_OUTFILENAME = "cleaned_counts_filtered.parquet" +FAILURE_REASON_FILE = "failure_reason.txt" + ##################################################### ##################################################### @@ -99,7 +101,11 @@ def remove_samples_with_low_ks_pvalue( if not valid_samples: logger.warning("No more valid sample to process...") - sys.exit(101) + msg = "COUNT FILE IS EMPTY" + logger.warning(msg) + with open(FAILURE_REASON_FILE, "w") as f: + f.write(msg) + sys.exit(0) # filtering the count dataframe to keep only the valid samples return count_lf.select([config.ENSEMBL_GENE_ID_COLNAME] + valid_samples) diff --git a/bin/compute_base_statistics.py b/bin/compute_base_statistics.py index 70df4c06..45892497 100755 --- a/bin/compute_base_statistics.py +++ b/bin/compute_base_statistics.py @@ -3,13 +3,13 @@ # Written by Olivier Coen. Released under the MIT license. import argparse -import polars as pl -from pathlib import Path +import logging from dataclasses import dataclass, field +from pathlib import Path from typing import ClassVar -import logging import config +import polars as pl logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) diff --git a/bin/gprofiler_utils.py b/bin/gprofiler_utils.py index 5f106ff0..7d72340a 100755 --- a/bin/gprofiler_utils.py +++ b/bin/gprofiler_utils.py @@ -2,22 +2,20 @@ # Written by Olivier Coen. Released under the MIT license. -import requests -import pandas as pd import logging import sys +import config +import pandas as pd +import requests +from requests.exceptions import ConnectionError, HTTPError from tenacity import ( + before_sleep_log, retry, stop_after_delay, wait_exponential, - before_sleep_log, ) -from requests.exceptions import HTTPError, ConnectionError - -import config - logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -37,12 +35,23 @@ COLS_TO_KEEP = ["incoming", "converted", "name", "description"] DESCRIPTION_PART_TO_REMOVE_REGEX = r"\s*\[Source:.*?\]" +GPROFILER_ERROR_MESSAGE = ( + "g:Profiler servers (main and beta) seem to be down... Please retry later... " + "If you have gene ID mappings and / or gene metadata for these datasets, you can provide them " + "directly using the `--gene_id_mapping` and `--gene_metadata` parameters respectively, " + "and by skipping the g:Profiler ID mapping step with `--skip_gprofiler`." +) + ################################################################## # FUNCTIONS ################################################################## +class GProfilerConnectionError(Exception): + pass + + def format_species_name(species: str): """ Format a species name into a format accepted by g:Profiler. @@ -137,13 +146,8 @@ def request_conversion( ) else: # both servers appear down, we stop here... - logger.error( - "g:Profiler servers (main and beta) seem to be down... Please retry later... " - "If you have gene ID mappings and / or gene metadata for these datasets, you can provide them " - "directly using the `--gene_id_mapping` and `--gene_metadata` parameters respectively, " - "and by skipping the g:Profiler ID mapping step with `--skip_gprofiler`." - ) - sys.exit(102) + logger.error(GPROFILER_ERROR_MESSAGE) + raise GProfilerConnectionError else: return response.json()["result"] diff --git a/bin/map_ids_to_ensembl.py b/bin/map_ids_to_ensembl.py index c18b0476..b4394286 100755 --- a/bin/map_ids_to_ensembl.py +++ b/bin/map_ids_to_ensembl.py @@ -2,14 +2,14 @@ # Written by Olivier Coen. Released under the MIT license. -import pandas as pd -from pathlib import Path import argparse import logging import sys +from pathlib import Path -from gprofiler_utils import convert_ids import config +import pandas as pd +from gprofiler_utils import GProfilerConnectionError, convert_ids logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -101,11 +101,17 @@ def main(): ############################################################# # QUERYING g:PROFILER SERVER ############################################################# - - if gene_ids_left_to_map: - gprofiler_mapping_dict, gene_metadata_dfs = convert_ids( - gene_ids_left_to_map, args.species - ) + try: + if gene_ids_left_to_map: + gprofiler_mapping_dict, gene_metadata_dfs = convert_ids( + gene_ids_left_to_map, args.species + ) + except GProfilerConnectionError: + msg = "COULD NOT CONNECT TO GPROFILER SERVER" + logger.warning(msg) + with open(FAILURE_REASON_FILE, "w") as f: + f.write(msg) + sys.exit(0) # overall mappings is the custom_mappings_dict complemented with gprofiler_mapping_dict mapping_dict = custom_mappings_dict | gprofiler_mapping_dict diff --git a/bin/normalise_with_deseq2.R b/bin/normalise_with_deseq2.R index 54c6670a..c1ff5721 100755 --- a/bin/normalise_with_deseq2.R +++ b/bin/normalise_with_deseq2.R @@ -6,6 +6,9 @@ suppressPackageStartupMessages(library("DESeq2")) library(DESeq2) library(optparse) +FAILURE_REASON_FILE <- "failure_reason.txt" +WARNING_REASON_FILE <- "warning_reason.txt" + ##################################################### ##################################################### # FUNCTIONS @@ -31,12 +34,16 @@ get_args <- function() { check_samples <- function(count_matrix, design_data) { # check if the column names of count_matrix match the sample names if (!all( colnames(count_matrix) == design_data$sample )) { - stop("Sample names in the count matrix do not match the design data.") + write("SAMPLE NAMES IN COUNT MATRIX DO NOT MATCH DESIGN DATA", file = FAILURE_REASON_FILE) + quit(save = "no", status = 0) } # check for extra samples extra_samples <- setdiff( colnames(count_matrix), design_data$sample ) if (length(extra_samples) > 0) { - warning("The following samples are in the count matrix but not in design: ", paste(extra_samples, collapse = ", ")) + write( + "THE FOLLOWING SAMPLES ARE IN THE COUNT MATRIX BUT NOT IN DESIGN: ", paste(extra_samples, collapse = ", "), + file = WARNING_REASON_FILE + ) } } @@ -123,7 +130,7 @@ get_normalised_cpm_counts <- function(count_file, design_file) { # if the dataframe is now empty, stop the process if (nrow(filtered_count_matrix) == 0) { message("No genes left after pre-filtering.") - #quit(save = "no", status = 100) + write("NO GENES LEFT AFTER PRE-FILTERING", file = FAILURE_REASON_FILE) quit(save = "no", status = 0) } diff --git a/bin/normalise_with_edger.R b/bin/normalise_with_edger.R index 84b4b24f..b90044ca 100755 --- a/bin/normalise_with_edger.R +++ b/bin/normalise_with_edger.R @@ -5,6 +5,9 @@ library(edgeR) library(optparse) +FAILURE_REASON_FILE <- "failure_reason.txt" +WARNING_REASON_FILE <- "warning_reason.txt" + ##################################################### ##################################################### # FUNCTIONS @@ -35,12 +38,16 @@ remove_all_zero_columns <- function(df) { check_samples <- function(count_matrix, design_data) { # check if the column names of count_matrix match the sample names if (!all( colnames(count_matrix) == design_data$sample )) { - stop("Sample names in the count matrix do not match the design data.") + write("SAMPLE NAMES IN COUNT MATRIX DO NOT MATCH DESIGN DATA", file = FAILURE_REASON_FILE) + quit(save = "no", status = 0) } # check for extra samples extra_samples <- setdiff( colnames(count_matrix), design_data$sample ) if (length(extra_samples) > 0) { - warning("The following samples are in the count matrix but not in design: ", paste(extra_samples, collapse = ", ")) + write( + "THE FOLLOWING SAMPLES ARE IN THE COUNT MATRIX BUT NOT IN DESIGN: ", paste(extra_samples, collapse = ", "), + file = WARNING_REASON_FILE + ) } } @@ -108,7 +115,8 @@ get_normalised_cpm_counts <- function(count_file, design_file) { # if the dataframe is now empty, stop the process if (nrow(dge) == 0) { message("No genes left after pre-filtering.") - quit(save = "no", status = 100) + write("NO GENES LEFT AFTER PRE-FILTERING", file = FAILURE_REASON_FILE) + quit(save = "no", status = 0) } # normalisation diff --git a/modules/local/clean_count_data/main.nf b/modules/local/clean_count_data/main.nf index 9fbe4a7b..e3f64f46 100644 --- a/modules/local/clean_count_data/main.nf +++ b/modules/local/clean_count_data/main.nf @@ -14,7 +14,8 @@ process CLEAN_COUNT_DATA { val ks_pvalue_threshold output: - tuple val(meta), path('cleaned_counts_filtered.parquet'), emit: counts + tuple val(meta), path('cleaned_counts_filtered.parquet'), emit: counts + tuple val(meta.dataset), path("failure_reason.txt"), optional: true, topic: clean_count_failure_reason tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions diff --git a/modules/local/normalisation/deseq2/main.nf b/modules/local/normalisation/deseq2/main.nf index 60f5deee..e7b497d1 100644 --- a/modules/local/normalisation/deseq2/main.nf +++ b/modules/local/normalisation/deseq2/main.nf @@ -14,6 +14,8 @@ process NORMALISATION_DESEQ2 { output: tuple val(meta), path('*.cpm.csv'), emit: cpm + tuple val(meta.dataset), path("failure_reason.txt"), optional: true, topic: normalisation_failure_reason + tuple val(meta.dataset), path("warning_reason.txt"), optional: true, topic: normalisation_warning_reason tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //"'), topic: versions tuple val("${task.process}"), val('DESeq2'), eval('Rscript -e "cat(as.character(packageVersion(\'DESeq2\')))"'), topic: versions diff --git a/modules/local/normalisation/edger/main.nf b/modules/local/normalisation/edger/main.nf index bf0fa6aa..91cf1dc7 100644 --- a/modules/local/normalisation/edger/main.nf +++ b/modules/local/normalisation/edger/main.nf @@ -14,6 +14,8 @@ process NORMALISATION_EDGER { output: tuple val(meta), path('*.cpm.csv'), emit: cpm + tuple val(meta.dataset), path("failure_reason.txt"), optional: true, topic: normalisation_failure_reason + tuple val(meta.dataset), path("warning_reason.txt"), optional: true, topic: normalisation_warning_reason tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //"'), topic: versions tuple val("${task.process}"), val('edgeR'), eval('Rscript -e "cat(as.character(packageVersion(\'edgeR\')))"'), topic: versions diff --git a/subworkflows/local/idmapping/main.nf b/subworkflows/local/idmapping/main.nf index 5d5c907f..3aa0800b 100644 --- a/subworkflows/local/idmapping/main.nf +++ b/subworkflows/local/idmapping/main.nf @@ -24,7 +24,6 @@ workflow ID_MAPPING { meta.platform_taxon = platform_taxon [ meta, file ] } - .view() .set { ch_counts } GPROFILER_IDMAPPING( diff --git a/subworkflows/local/multiqc/main.nf b/subworkflows/local/multiqc/main.nf index 0e9e4b62..4379626a 100644 --- a/subworkflows/local/multiqc/main.nf +++ b/subworkflows/local/multiqc/main.nf @@ -78,12 +78,47 @@ workflow MULTIQC_WORKFLOW { name: 'id_mapping_failure_reasons.tsv', seed: "Dataset\tReason", newLine: true, - storeDir: "${params.outdir}/warnings/" + storeDir: "${params.outdir}/errors/" ) { item -> "${item[0]}\t${item[1]}" } .set { ch_id_mapping_failure_reasons } + Channel.topic('normalisation_failure_reason') + .map { accession, file -> [ accession, file.readLines()[0] ] } + .collectFile( + name: 'normalisation_failure_reasons.tsv', + seed: "Dataset\tReason", + newLine: true, + storeDir: "${params.outdir}/errors/" + ) { + item -> "${item[0]}\t${item[1]}" + } + .set { ch_normalisation_failure_reasons } + + Channel.topic('normalisation_warning_reason') + .map { accession, file -> [ accession, file.readLines()[0] ] } + .collectFile( + name: 'normalisation_warning_reasons.tsv', + seed: "Dataset\tReason", + newLine: true, + storeDir: "${params.outdir}/warnings/" + ) { + item -> "${item[0]}\t${item[1]}" + } + .set { ch_normalisation_warning_reasons } + + Channel.topic('clean_count_failure_reason') + .map { accession, file -> [ accession, file.readLines()[0] ] } + .collectFile( + name: 'clean_count_failure_reasons.tsv', + seed: "Dataset\tReason", + newLine: true, + storeDir: "${params.outdir}/errors/" + ) { + item -> "${item[0]}\t${item[1]}" + } + .set { ch_clean_count_failure_reasons } // ------------------------------------------------------------------------------------ @@ -101,6 +136,9 @@ workflow MULTIQC_WORKFLOW { .mix( ch_geo_failure_reasons ) .mix( ch_geo_warning_reasons ) .mix( ch_id_mapping_failure_reasons ) + .mix( ch_normalisation_failure_reasons ) + .mix( ch_normalisation_warning_reasons ) + .mix( ch_clean_count_failure_reasons ) .set { ch_multiqc_files } // ------------------------------------------------------------------------------------ diff --git a/subworkflows/local/stability_scoring/main.nf b/subworkflows/local/stability_scoring/main.nf index caa6f74e..f902703f 100644 --- a/subworkflows/local/stability_scoring/main.nf +++ b/subworkflows/local/stability_scoring/main.nf @@ -15,6 +15,11 @@ workflow STABILITY_SCORING { ch_counts ch_design ch_stats + candidate_selection_descriptor + nb_top_gene_candidates + min_expr_threshold + run_genorm + stability_score_weights main: @@ -25,9 +30,9 @@ workflow STABILITY_SCORING { GET_CANDIDATE_GENES( ch_counts, ch_stats, - params.candidate_selection_descriptor, - params.nb_top_gene_candidates, - params.min_expr_threshold + candidate_selection_descriptor, + nb_top_gene_candidates, + min_expr_threshold ) GET_CANDIDATE_GENES.out.counts.set { ch_candidate_gene_counts } @@ -45,7 +50,7 @@ workflow STABILITY_SCORING { // GENORM // ----------------------------------------------------------------- - if ( params.run_genorm ) { + if ( run_genorm ) { GENORM ( ch_candidate_gene_counts ) GENORM.out.m_measures.set { ch_genorm_stability } } else { @@ -58,7 +63,7 @@ workflow STABILITY_SCORING { COMPUTE_STABILITY_SCORES ( ch_stats, - params.stability_score_weights, + stability_score_weights, ch_normfinder_stability, ch_genorm_stability ) diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index dc63e515..48d123f0 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -144,7 +144,12 @@ workflow STABLEEXPRESSION { STABILITY_SCORING ( ch_all_counts, ch_whole_design, - ch_all_datasets_stats + ch_all_datasets_stats, + params.candidate_selection_descriptor, + params.nb_top_gene_candidates, + params.min_expr_threshold, + params.run_genorm, + params.stability_score_weights ) STABILITY_SCORING.out.summary_statistics.set { ch_stats_all_genes_with_scores } From a396d4e40d62a0ee017ab080203ac51a7715e20f Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Fri, 7 Nov 2025 18:07:01 +0100 Subject: [PATCH 135/258] update .gitignore to ignore galaxy test files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c790fe08..c44a0ec1 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ tokenizers/ corpora/ .github/act.custom_runner.Dockerfile .ruff_cache +galaxy/test_output/ From 83a3ae7b30c940376625101c05a2641f2e97c705 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sat, 8 Nov 2025 00:35:49 +0100 Subject: [PATCH 136/258] fix discrepancies following git am --- .gitpod.yml | 17 --------- .pre-commit-config.yaml | 11 +----- README.md | 8 ++-- main.nf | 7 +++- nextflow.config | 82 ++++++++++++----------------------------- nextflow_schema.json | 8 ++-- nf-test.config | 18 +++++++++ ro-crate-metadata.json | 39 +++++--------------- 8 files changed, 68 insertions(+), 122 deletions(-) delete mode 100644 .gitpod.yml diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index 46118637..00000000 --- a/.gitpod.yml +++ /dev/null @@ -1,17 +0,0 @@ -image: nfcore/gitpod:latest -tasks: - - name: Update Nextflow and setup pre-commit - command: | - pre-commit install --install-hooks - nextflow self-update - -vscode: - extensions: # based on nf-core.nf-core-extensionpack - #- esbenp.prettier-vscode # Markdown/CommonMark linting and style checking for Visual Studio Code - - EditorConfig.EditorConfig # override user/workspace settings with settings found in .editorconfig files - - Gruntfuggly.todo-tree # Display TODO and FIXME in a tree view in the activity bar - - mechatroner.rainbow-csv # Highlight columns in csv files in different colors - - nextflow.nextflow # Nextflow syntax highlighting - - oderwat.indent-rainbow # Highlight indentation level - - streetsidesoftware.code-spell-checker # Spelling checker for source code - - charliermarsh.ruff # Code linter Ruff diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9f4c32e3..fea329a7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/mirrors-prettier - rev: "v3.1.0" + rev: "v4.0.0-alpha.8" hooks: - id: prettier additional_dependencies: @@ -28,15 +28,8 @@ repos: .*\.snap$ )$ - - repo: https://github.com/editorconfig-checker/editorconfig-checker.python - rev: "3.1.2" - hooks: - - id: editorconfig-checker - exclude: '\.drawio$|^galaxy/.*\.xml$' - alias: ec - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.4 + rev: v0.14.1 hooks: # Run the linter. - id: ruff diff --git a/README.md b/README.md index 9f0788e1..45ba48e3 100644 --- a/README.md +++ b/README.md @@ -5,17 +5,19 @@ -[![GitHub Actions CI Status](https://github.com/nf-core/stableexpression/actions/workflows/ci.yml/badge.svg)](https://github.com/nf-core/stableexpression/actions/workflows/ci.yml) +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new/nf-core/stableexpression) +[![GitHub Actions CI Status](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml/badge.svg)](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml) [![GitHub Actions Linting Status](https://github.com/nf-core/stableexpression/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/stableexpression/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/stableexpression/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%A525.04.00-23aa62.svg)](https://www.nextflow.io/) +[![Nextflow](https://img.shields.io/badge/version-%E2%89%A525.04.0-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-3.4.1-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/3.4.1) [![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/stableexpression) -[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23stableexpression-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/stableexpression)[![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%23stableexpression-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/stableexpression)[![Follow on Bluesky](https://img.shields.io/badge/bluesky-%40nf__core-1185fe?labelColor=000000&logo=bluesky)](https://bsky.app/profile/nf-co.re)[![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 diff --git a/main.nf b/main.nf index dc57aeaa..d437ca24 100644 --- a/main.nf +++ b/main.nf @@ -59,7 +59,12 @@ workflow { params.version, params.validate_params, params.monochrome_logs, - args + args, + params.outdir, + params.input, + params.help, + params.help_full, + params.show_hidden ) // diff --git a/nextflow.config b/nextflow.config index 6bc3f0c5..d1898ecb 100644 --- a/nextflow.config +++ b/nextflow.config @@ -52,6 +52,13 @@ params { candidate_selection_descriptor = "cv" stability_score_weights = "0.8,0.1,0.1,0" + // MultiQC options + multiqc_config = null + multiqc_title = null + multiqc_logo = null + max_multiqc_email_size = '25.MB' + multiqc_methods_description = null + // Boilerplate options outdir = null publish_dir_mode = 'copy' @@ -76,23 +83,10 @@ params { config_profile_contact = null config_profile_url = null - // MultiQC - multiqc_config = null - multiqc_logo = null - multiqc_methods_description = null - max_multiqc_email_size = "25.MB" - multiqc_title = null - // Schema validation default options validate_params = true } -validation { - // logs - monochromeLogs = false - help.enabled = true -} - // Load base.config by default for all pipelines includeConfig 'conf/base.config' @@ -211,17 +205,10 @@ 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' } @@ -230,12 +217,16 @@ profiles { test_one_rnaseq_one_microarray { includeConfig 'conf/test_one_rnaseq_one_microarray.config' } local { includeConfig 'conf/local.config' } } +// Load nf-core custom profiles from different institutions + +// 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/stableexpression 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 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/stableexpression custom profiles from different institutions. -// includeConfig !System.getenv('NXF_OFFLINE') && params.custom_config_base ? "${params.custom_config_base}/pipeline/stableexpression.config" : "/dev/null" +// TODO nf-core: Optionally, you can add a pipeline-specific nf-core config at https://github.com/nf-core/configs +// includeConfig params.custom_config_base && (!System.getenv('NXF_OFFLINE') || !params.custom_config_base.startsWith('http')) ? "${params.custom_config_base}/pipeline/stableexpression.config" : "/dev/null" // Set default registry for Apptainer, Docker, Podman and Singularity independent of -profile // Will not be used unless Apptainer / Docker / Podman / Singularity are enabled @@ -246,6 +237,8 @@ podman.registry = 'quay.io' singularity.registry = 'quay.io' charliecloud.registry = 'quay.io' + + // Export these variables to prevent local Python/R libraries from conflicting with those in the container // The JULIA depot path has been adjusted to a fixed path `/usr/local/share/julia` that needs to be used for packages in the container. // See https://apeltzer.github.io/post/03-julia-lang-nextflow/ for details on that. Once we have a common agreement on where to keep Julia packages, this is adjustable. @@ -301,51 +294,22 @@ manifest { ], ] homePage = 'https://github.com/nf-core/stableexpression' - description = """ -This pipeline is dedicated to finding the most stable genes across count datasets -""" + description = """This pipeline is dedicated to finding the most stable genes across count datasets""" mainScript = 'main.nf' defaultBranch = 'main' - nextflowVersion = '!>=25.04.00' + nextflowVersion = '!>=25.04.0' version = '1.0dev' doi = '' } // Nextflow plugins plugins { - id 'nf-schema@2.2.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/stableexpression -profile --species genus_species --datasets 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/stableexpression ${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/stableexpression/blob/main/CITATIONS.md -""" - } - summary { - beforeText = validation.help.beforeText - afterText = validation.help.afterText - } } // Load modules.config for DSL2 module specific options diff --git a/nextflow_schema.json b/nextflow_schema.json index 4cc1faa3..76ae4bfc 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/stableexpression/master/nextflow_schema.json", + "$id": "https://raw.githubusercontent.com/nf-core/stableexpression/main/nextflow_schema.json", "title": "nf-core/stableexpression pipeline parameters", - "description": "\nPipeline dedicated to finding the most stable genes across count datasets\n", + "description": "This pipeline is dedicated to finding the most stable genes across count datasets", "type": "object", "$defs": { "input_output_options": { @@ -71,8 +71,7 @@ }, "multiqc_title": { "type": "string", - "description": "MultiQC report title", - "help_text": "Printed as page header, used for filename if not otherwise specified.", + "description": "MultiQC report title. Printed as page header, used for filename if not otherwise specified.", "fa_icon": "fas fa-file-signature" } } @@ -367,6 +366,7 @@ "type": "string", "description": "File size limit when attaching MultiQC reports to summary emails.", "pattern": "^\\d+(\\.\\d+)?\\.?\\s*(K|M|G|T)?B$", + "default": "25.MB", "fa_icon": "fas fa-file-upload", "hidden": true }, diff --git a/nf-test.config b/nf-test.config index 65fb719c..984c552c 100644 --- a/nf-test.config +++ b/nf-test.config @@ -1,13 +1,31 @@ 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 testsDir "tests" workDir ".nf-test" 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 'nextflow.config', 'nf-test.config', 'conf/test.config', 'tests/nextflow.config', 'tests/.nftignore' + + // load the necessary plugins profile "apptainer" requires ( "nf-test": "0.9.2" ) 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 4ed8f1ac..8418c941 100644 --- a/ro-crate-metadata.json +++ b/ro-crate-metadata.json @@ -23,7 +23,7 @@ "@type": "Dataset", "creativeWorkStatus": "InProgress", "datePublished": "2025-05-08T08:00:57+00:00", - "description": "

    \n \n \n \"nf-core/stableexpression\"\n \n

    \n\n[![GitHub Actions CI Status](https://github.com/nf-core/stableexpression/actions/workflows/ci.yml/badge.svg)](https://github.com/nf-core/stableexpression/actions/workflows/ci.yml)\n[![GitHub Actions Linting Status](https://github.com/nf-core/stableexpression/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/stableexpression/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/stableexpression/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%A525.04.00-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/stableexpression)\n\n[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23stableexpression-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/stableexpression)[![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/stableexpression** 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/stableexpression \\\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/stableexpression/usage) and the [parameter documentation](https://nf-co.re/stableexpression/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/stableexpression/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/stableexpression/output).\n\n## Credits\n\nnf-core/stableexpression was originally written by Olivier Coen.\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 `#stableexpression` channel](https://nfcore.slack.com/channels/stableexpression) (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", + "description": "

    \n \n \n \"nf-core/stableexpression\"\n \n

    \n\n[![GitHub Actions CI Status](https://github.com/nf-core/stableexpression/actions/workflows/ci.yml/badge.svg)](https://github.com/nf-core/stableexpression/actions/workflows/ci.yml)\n[![GitHub Actions Linting Status](https://github.com/nf-core/stableexpression/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/stableexpression/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/stableexpression/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/stableexpression)\n\n[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23stableexpression-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/stableexpression)[![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/stableexpression** 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/stableexpression \\\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/stableexpression/usage) and the [parameter documentation](https://nf-co.re/stableexpression/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/stableexpression/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/stableexpression/output).\n\n## Credits\n\nnf-core/stableexpression was originally written by Olivier Coen.\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 `#stableexpression` channel](https://nfcore.slack.com/channels/stableexpression) (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", "hasPart": [ { "@id": "main.nf" @@ -121,11 +121,7 @@ }, { "@id": "main.nf", - "@type": [ - "File", - "SoftwareSourceCode", - "ComputationalWorkflow" - ], + "@type": ["File", "SoftwareSourceCode", "ComputationalWorkflow"], "creator": [ { "@id": "https://orcid.org/0000-0003-3387-1040" @@ -134,37 +130,22 @@ "dateCreated": "", "dateModified": "2025-10-19T19:52:14Z", "dct:conformsTo": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE/", - "keywords": [ - "nf-core", - "nextflow", - "expression", - "housekeeping-genes", - "qpcr-analysis" - ], - "license": [ - "MIT" - ], + "keywords": ["nf-core", "nextflow", "expression", "housekeeping-genes", "qpcr-analysis"], + "license": ["MIT"], "maintainer": [ { "@id": "https://orcid.org/0000-0003-3387-1040" } ], - "name": [ - "nf-core/stableexpression" - ], + "name": ["nf-core/stableexpression"], "programmingLanguage": { "@id": "https://w3id.org/workflowhub/workflow-ro-crate#nextflow" }, "sdPublisher": { "@id": "https://nf-co.re/" }, - "url": [ - "https://github.com/nf-core/stableexpression", - "https://nf-co.re/stableexpression/dev/" - ], - "version": [ - "1.0dev" - ] + "url": ["https://github.com/nf-core/stableexpression", "https://nf-co.re/stableexpression/dev/"], + "version": ["1.0dev"] }, { "@id": "https://w3id.org/workflowhub/workflow-ro-crate#nextflow", @@ -176,14 +157,14 @@ "url": { "@id": "https://www.nextflow.io/" }, - "version": "!>=25.04.00" + "version": "!>=24.04.2" }, { - "@id": "#ea61203f-47c5-4f0f-bd91-43195b3225f1", + "@id": "#131b74e6-1c06-44fe-8e15-4987d1b14f07", "@type": "TestSuite", "instance": [ { - "@id": "#6c536155-37c4-45b0-87c2-977dd7e02196" + "@id": "#2316b008-0740-4e2e-b924-649112987083" } ], "mainEntity": { From 4659d78feff49de1b7568240affda1704197501f Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Sat, 8 Nov 2025 00:36:43 +0100 Subject: [PATCH 137/258] Template update for nf-core/tools version 3.3.2 --- nextflow.config | 9 +++++++++ nextflow_schema.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/nextflow.config b/nextflow.config index e4e72bae..7cb79402 100644 --- a/nextflow.config +++ b/nextflow.config @@ -161,6 +161,11 @@ profiles { apptainer.runOptions = '--nv' singularity.runOptions = '--nv' } + 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' } } @@ -171,6 +176,10 @@ profiles { 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/stableexpression 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/stableexpression 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 params.custom_config_base && (!System.getenv('NXF_OFFLINE') || !params.custom_config_base.startsWith('http')) ? "${params.custom_config_base}/pipeline/stableexpression.config" : "/dev/null" diff --git a/nextflow_schema.json b/nextflow_schema.json index bc12467a..92a4a6e6 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -185,7 +185,6 @@ "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": { "type": ["boolean", "string"], "description": "Display the help message." @@ -197,6 +196,7 @@ "show_hidden": { "type": "boolean", "description": "Display hidden parameters in the help message (only works when --help or --help_full are provided)." + }, } } } From 77db12ca94bdf4e8c38111590fbd22f417467b2d Mon Sep 17 00:00:00 2001 From: Olivier Date: Sat, 8 Nov 2025 11:09:59 +0100 Subject: [PATCH 138/258] fix various issues linked to merge --- .gitignore | 1 + main.nf | 2 +- nextflow.config | 5 ----- nextflow_schema.json | 4 ++-- .../utils_nfcore_stableexpression_pipeline/main.nf | 13 ++++++++----- subworkflows/nf-core/utils_nfschema_plugin/main.nf | 1 - 6 files changed, 12 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index c44a0ec1..2f9c5b0d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ testing* null/ .nf-test* .idea/ +.vscode/ taggers/ tokenizers/ corpora/ diff --git a/main.nf b/main.nf index d437ca24..be4d42ea 100644 --- a/main.nf +++ b/main.nf @@ -61,7 +61,7 @@ workflow { params.monochrome_logs, args, params.outdir, - params.input, + params.datasets, params.help, params.help_full, params.show_hidden diff --git a/nextflow.config b/nextflow.config index f3d767e3..a38e0f9c 100644 --- a/nextflow.config +++ b/nextflow.config @@ -223,11 +223,6 @@ profiles { // Load nf-core/stableexpression 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/stableexpression 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/stableexpression 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 params.custom_config_base && (!System.getenv('NXF_OFFLINE') || !params.custom_config_base.startsWith('http')) ? "${params.custom_config_base}/pipeline/stableexpression.config" : "/dev/null" diff --git a/nextflow_schema.json b/nextflow_schema.json index 28ff150b..5859eeca 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -16,7 +16,7 @@ "type": "string", "description": "Scientifc species name (genus and species)", "fa_icon": "fas fa-hippo", - "pattern": "([a-zA-Z]+)[_ ]([a-zA-Z]+)", + "pattern": "^([a-zA-Z]+)[_ ]([a-zA-Z]+)$", "help_text": "Genus and species may be separated by ` ` or `_`. Example: `--species 'Arabidopsis thaliana'` or `--species 'homo_sapiens'`. Character case is not important." }, "outdir": { @@ -260,7 +260,6 @@ "description": "Number of candidate genes to keep for stability scoring", "fa_icon": "fas fa-sort-numeric-up-alt", "minimum": 1, - "default": 5000, "help_text": "Number of candidate genes to keep in the final list. These candidates genes are chosen as the ones showing the least standard variation." }, "run_genorm": { @@ -420,6 +419,7 @@ "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": { "type": ["boolean", "string"], "description": "Display the help message." diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index 2be22037..dfffd6ab 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -40,8 +40,6 @@ workflow PIPELINE_INITIALISATION { main: - ch_versions = Channel.empty() - // // Print version and exit if required and dump pipeline parameters to JSON file // @@ -72,7 +70,7 @@ workflow PIPELINE_INITIALISATION { * Software dependencies https://github.com/nf-core/stableexpression/blob/main/CITATIONS.md """ - command = "nextflow run ${workflow.manifest.name} -profile --input samplesheet.csv --outdir " + command = "nextflow run ${workflow.manifest.name} -profile --species --outdir " UTILS_NFSCHEMA_PLUGIN ( workflow, @@ -228,8 +226,6 @@ def validateInputSamplesheet(input) { } } - return [ metas[0], fastqs ] -} // // Generate methods description for MultiQC // @@ -377,6 +373,13 @@ def augmentMetadata( ch_files ) { } } + +/* +======================================================================================== + FUNCTIONS FOR CALCULATING SIZE OF DATA +======================================================================================== +*/ + def storeDatasetSize( ch_counts, nb_genes_key, nb_samples_key ) { // adding nb genes and nb samples in the meta map under keys provided as parameters return ch_counts diff --git a/subworkflows/nf-core/utils_nfschema_plugin/main.nf b/subworkflows/nf-core/utils_nfschema_plugin/main.nf index ee4738c8..acb39724 100644 --- a/subworkflows/nf-core/utils_nfschema_plugin/main.nf +++ b/subworkflows/nf-core/utils_nfschema_plugin/main.nf @@ -71,4 +71,3 @@ workflow UTILS_NFSCHEMA_PLUGIN { emit: dummy_emit = true } - From 0fe67d13f76036159a29bf6d7f6ea5fc37b35b91 Mon Sep 17 00:00:00 2001 From: Olivier Date: Sat, 8 Nov 2025 11:48:21 +0100 Subject: [PATCH 139/258] fix output issue with expression atlas get accessions --- bin/get_geo_dataset_accessions.py | 22 +++++++++---------- .../expressionatlas/getaccessions/main.nf | 2 +- modules/local/geo/getaccessions/main.nf | 8 +++---- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/bin/get_geo_dataset_accessions.py b/bin/get_geo_dataset_accessions.py index 98d5cc32..d361eca2 100755 --- a/bin/get_geo_dataset_accessions.py +++ b/bin/get_geo_dataset_accessions.py @@ -3,28 +3,28 @@ # Written by Olivier Coen. Released under the MIT license. import argparse -from tqdm import tqdm +import logging +import tarfile from multiprocessing import Pool -from Bio import Entrez from pathlib import Path +from urllib.request import urlretrieve + +import pandas as pd # from random import sample import requests -import pandas as pd import xmltodict -from urllib.request import urlretrieve -import tarfile +from Bio import Entrez +from gprofiler_utils import chunk_list +from natural_language_utils import keywords_in_fields +from requests.exceptions import ConnectionError, HTTPError from tenacity import ( + before_sleep_log, retry, stop_after_delay, wait_exponential, - before_sleep_log, ) -import logging -from requests.exceptions import HTTPError, ConnectionError - -from natural_language_utils import keywords_in_fields -from gprofiler_utils import chunk_list +from tqdm import tqdm logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) diff --git a/modules/local/expressionatlas/getaccessions/main.nf b/modules/local/expressionatlas/getaccessions/main.nf index b3b10dbc..ee553a8a 100644 --- a/modules/local/expressionatlas/getaccessions/main.nf +++ b/modules/local/expressionatlas/getaccessions/main.nf @@ -13,7 +13,7 @@ process EXPRESSIONATLAS_GETACCESSIONS { val platform output: - path "accessions.txt", optional: true, emit: accessions + path "accessions.txt", optional: true, emit: accessions path "selected_experiments.metadata.tsv", optional: true, topic: eatlas_selected_datasets path "species_experiments.metadata.tsv", optional: true, topic: eatlas_all_datasets //path "filtered_experiments.metadata.tsv", optional: true, topic: filtered_eatlas_experiment_metadata diff --git a/modules/local/geo/getaccessions/main.nf b/modules/local/geo/getaccessions/main.nf index f7b7f1e8..50e9e33c 100644 --- a/modules/local/geo/getaccessions/main.nf +++ b/modules/local/geo/getaccessions/main.nf @@ -15,10 +15,10 @@ process GEO_GETACCESSIONS { val accessions output: - path "accessions.tsv", emit: accessions - path "geo_selected_datasets.metadata.tsv", optional: true, topic: geo_selected_datasets - path "geo_all_datasets.metadata.tsv", optional: true, topic: geo_all_datasets - path "geo_rejected_datasets.metadata.tsv", optional: true, topic: geo_rejected_datasets + path "accessions.tsv", optional: true, emit: accessions + path "geo_selected_datasets.metadata.tsv", optional: true, topic: geo_selected_datasets + path "geo_all_datasets.metadata.tsv", optional: true, topic: geo_all_datasets + path "geo_rejected_datasets.metadata.tsv", optional: true, topic: geo_rejected_datasets tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('requests'), eval('python3 -c "import requests; print(requests.__version__)"'), topic: versions From ab7922a19d1a918cc5e841f9d8924ed1b902f94d Mon Sep 17 00:00:00 2001 From: Olivier Date: Sat, 8 Nov 2025 12:03:27 +0100 Subject: [PATCH 140/258] early fix of release_annoucements.yml --- .github/workflows/release-announcements.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-announcements.yml b/.github/workflows/release-announcements.yml index e64cebd6..8509c7bb 100644 --- a/.github/workflows/release-announcements.yml +++ b/.github/workflows/release-announcements.yml @@ -15,7 +15,7 @@ jobs: 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 - name: get description - id: get_topics + 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 From 134bb45aed05fa587cd0546e81dccf89acef4ec0 Mon Sep 17 00:00:00 2001 From: Olivier Date: Sat, 8 Nov 2025 12:36:57 +0100 Subject: [PATCH 141/258] fix malformed nextflow schema --- nextflow_schema.json | 1 - 1 file changed, 1 deletion(-) diff --git a/nextflow_schema.json b/nextflow_schema.json index 5859eeca..9ca94074 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -431,7 +431,6 @@ "show_hidden": { "type": "boolean", "description": "Display hidden parameters in the help message (only works when --help or --help_full are provided)." - }, } } } From 6445bfdcda8e9f7076f51fa0018e9feb46159153 Mon Sep 17 00:00:00 2001 From: Olivier Date: Sat, 8 Nov 2025 13:33:43 +0100 Subject: [PATCH 142/258] pass all module tests --- bin/download_eatlas_data.R | 8 + bin/download_geo_data.R | 2 + bin/map_ids_to_ensembl.py | 3 + tests/default.nf.test.snap | 1545 ++++------------- .../getaccessions/main.nf.test.snap | 42 +- .../expressionatlas/getdata/main.nf.test | 67 +- .../expressionatlas/getdata/main.nf.test.snap | 120 +- .../local/idmapping/gprofiler/main.nf.test | 14 +- .../idmapping/gprofiler/main.nf.test.snap | 101 +- .../local/normfinder/main.nf.test.snap | 14 +- tests/test_data/idmapping/custom/metadata.csv | 4 + 11 files changed, 629 insertions(+), 1291 deletions(-) create mode 100644 tests/test_data/idmapping/custom/metadata.csv diff --git a/bin/download_eatlas_data.R b/bin/download_eatlas_data.R index 5f50edec..9b738d92 100755 --- a/bin/download_eatlas_data.R +++ b/bin/download_eatlas_data.R @@ -46,6 +46,7 @@ download_expression_atlas_data_with_retries <- function(accession, max_retries = if (grepl("does not look like an ArrayExpress/BioStudies experiment accession.", w$message)) { warning(w$message) write("EXPERIMENT NOT FOUND", file = FAILURE_REASON_FILE) + quit(save = "no", status = 0) } # else, retrying @@ -60,12 +61,15 @@ download_expression_atlas_data_with_retries <- function(accession, max_retries = if (grepl("550 Requested action not taken; file unavailable", w$message)) { warning(w$message) write("EXPERIMENT SUMMARY NOT FOUND", file = FAILURE_REASON_FILE) + quit(save = "no", status = 0) } else if (grepl("Failure when receiving data from the peer", w$message)) { warning(w$message) write("EXPERIMENT NOT FOUND", file = FAILURE_REASON_FILE) + quit(save = "no", status = 0) } else { warning("Unhandled warning: ", w$message) write("UNKNOWN ERROR", file = FAILURE_REASON_FILE) + quit(save = "no", status = 0) } } @@ -82,9 +86,11 @@ download_expression_atlas_data_with_retries <- function(accession, max_retries = if (grepl("Download appeared successful but no experiment summary object was found", e$message)) { warning(e$message) write("EXPERIMENT SUMMARY NOT FOUND", file = FAILURE_REASON_FILE) + quit(save = "no", status = 0) } else { warning("Unhandled error: ", e$message) write("UNKNOWN ERROR", file = FAILURE_REASON_FILE) + quit(save = "no", status = 0) } } @@ -173,6 +179,7 @@ process_data <- function(atlas_data, accession) { result <- get_one_colour_microarray_data(data) } else { write(paste("UNKNOWN DATA TYPE:", data_type), file = FAILURE_REASON_FILE) + quit(save = "no", status = 0) } }, error = function(e) { @@ -211,6 +218,7 @@ accession <- trimws(args$accession) if (startsWith(accession, "E-PROT")) { warning("Ignoring the ", accession, " experiment.") write("PROTEOME ACCESSIONS NOT HANDLED", file = FAILURE_REASON_FILE) + quit(save = "no", status = 0) } # searching and downloading expression atlas data diff --git a/bin/download_geo_data.R b/bin/download_geo_data.R index 91f9944a..b38b3cd2 100755 --- a/bin/download_geo_data.R +++ b/bin/download_geo_data.R @@ -120,6 +120,7 @@ download_geo_data_with_retries <- function(accession, species, max_retries = 3, } else { warning("Unhandled error: ", e$message) write("EXPERIMENT NOT FOUND", file = FAILURE_REASON_FILE) + quit(save = "no", status = 0) } }) @@ -179,6 +180,7 @@ process_data <- function(atlas_data, accession, species) { if ( length(names(geo_data)) > 1 ) { warning("Multiple data files were found") write("EXPERIMENT CONTAINS MULTIPLE FILES", file = FAILURE_REASON_FILE) + quit(save = "no", status = 0) } file <- names(geo_data)[[ 1 ]] diff --git a/bin/map_ids_to_ensembl.py b/bin/map_ids_to_ensembl.py index b4394286..864b1e2f 100755 --- a/bin/map_ids_to_ensembl.py +++ b/bin/map_ids_to_ensembl.py @@ -101,6 +101,9 @@ def main(): ############################################################# # QUERYING g:PROFILER SERVER ############################################################# + + gprofiler_mapping_dict = {} + try: if gene_ids_left_to_map: gprofiler_mapping_dict, gene_metadata_dfs = convert_ids( diff --git a/tests/default.nf.test.snap b/tests/default.nf.test.snap index 18397c44..82c2c944 100644 --- a/tests/default.nf.test.snap +++ b/tests/default.nf.test.snap @@ -1,1210 +1,431 @@ { "-profile test": { "content": [ - { - "AGGREGATE_RESULTS": { - "python": "3.12.8", - "polars": "1.17.1" - }, - "CLEAN_COUNT_DATA": { - "python": "3.12.8", - "polars": "1.17.1" - }, - "COMPUTE_BASE_STATISTICS": { - "python": "3.12.8", - "polars": "1.17.1" - }, - "COMPUTE_BASE_STATISTICS_FOR_RNASEQ": { - "python": "3.12.8", - "polars": "1.17.1" - }, - "COMPUTE_M_MEASURE": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "COMPUTE_STABILITY_SCORES": { - "python": "3.13.7", - "polars": "1.33.1" - }, - "CROSS_JOIN": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "DATASET_STATISTICS": { - "python": "3.12.8", - "scipy": "1.15.0", - "pyarrow": "19.0.0", - "pandas": "2.2.3" - }, - "EXPRESSIONATLAS_GETACCESSIONS": { - "nltk": "3.9.1", - "python": "3.13.5", - "pandas": "2.3.0", - "requests": "2.32.4", - "pyyaml": "6.0.2" - }, - "EXPRESSIONATLAS_GETDATA": { - "ExpressionAtlas": "1.30.0", - "R": "4.3.3 (2024-02-29)" - }, - "EXPRESSION_RATIO": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "GEO_GETACCESSIONS": { - "pyyaml": "6.0.2", - "requests": "2.32.5", - "nltk": "3.9.1", - "xmltodict": "0.14.2", - "python": "3.13.7", - "pandas": "2.3.2", - "biopython": 1.85 - }, - "GET_CANDIDATE_GENES": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "GPROFILER_IDMAPPING": { - "requests": "2.32.4", - "python": "3.13.5", - "pandas": "2.3.1" - }, - "MAKE_CHUNKS": { - "python": "3.12.8", - "polars": "1.17.1" - }, - "MERGE_ALL_COUNTS": { - "python": "3.12.8", - "polars": "1.17.1" - }, - "MERGE_DESIGNS": { - "python": "3.13.5", - "pandas": "2.3.2" - }, - "MERGE_RNASEQ_COUNTS": { - "python": "3.12.8", - "polars": "1.17.1" - }, - "NORMALISATION_DESEQ2": { - "DESeq2": "1.42.0", - "R": "4.3.3 (2024-02-29)" - }, - "NORMFINDER": { - "python": "3.13.7", - "polars": "1.33.1" - }, - "QUANTILE_NORMALISATION": { - "python": "3.12.8", - "pyarrow": "19.0.0", - "scikit-learn": "1.6.1", - "pandas": "2.2.3" - }, - "RATIO_STANDARD_VARIATION": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "Workflow": { - "nf-core/stableexpression": "v1.0dev" - } - }, + null, + [ + "errors", + "expression_atlas", + "expression_atlas/accessions", + "expression_atlas/accessions/accessions.txt", + "expression_atlas/accessions/selected_experiments.metadata.tsv", + "expression_atlas/accessions/species_experiments.metadata.tsv", + "expression_atlas/datasets", + "expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", + "expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", + "geo", + "idmapping", + "merged_datasets", + "pipeline_info", + "pipeline_info/software_mqc_versions.yml", + "warnings" + ], + [ + "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", + "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-08T11:05:48.72465951" + }, + "-profile test_accessions_only": { + "content": [ + null, + [ + "errors", + "expression_atlas", + "expression_atlas/accessions", + "expression_atlas/accessions/accessions.txt", + "expression_atlas/accessions/selected_experiments.metadata.tsv", + "expression_atlas/accessions/species_experiments.metadata.tsv", + "geo", + "geo/accessions", + "geo/accessions/accessions.tsv", + "geo/accessions/geo_all_datasets.metadata.tsv", + "geo/accessions/geo_rejected_datasets.metadata.tsv", + "geo/accessions/geo_selected_datasets.metadata.tsv", + "multiqc", + "multiqc/multiqc_data", + "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_eatlas_all_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_rejected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_selected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_software_versions.txt", + "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_plots", + "multiqc/multiqc_plots/pdf", + "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_rejected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_selected_experiments_metadata.pdf", + "multiqc/multiqc_plots/png", + "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", + "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_rejected_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_selected_experiments_metadata.png", + "multiqc/multiqc_plots/svg", + "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_rejected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_selected_experiments_metadata.svg", + "multiqc/multiqc_report.html", + "multiqc/versions.yml", + "pipeline_info", + "pipeline_info/software_mqc_versions.yml", + "warnings" + ], + [ + "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", + "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "accessions.tsv:md5,095fb7f3a666b4382d4ba7296053451b", + "geo_all_datasets.metadata.tsv:md5,47367b8ff7c7ab98c575185c3b51284f", + "geo_rejected_datasets.metadata.tsv:md5,341a71045752afba5a83cedb0c0f23e8", + "geo_selected_datasets.metadata.tsv:md5,47367b8ff7c7ab98c575185c3b51284f", + "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", + "multiqc_eatlas_all_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", + "multiqc_eatlas_selected_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", + "multiqc_geo_all_experiments_metadata.txt:md5,6b524d6e36600f1b6d1faadde102e10a", + "multiqc_geo_rejected_experiments_metadata.txt:md5,3b196d575f0051031ebd78a42d24e47a", + "multiqc_geo_selected_experiments_metadata.txt:md5,6b524d6e36600f1b6d1faadde102e10a", + "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-08T11:06:24.97488603" + }, + "-profile test_download_only": { + "content": [ + null, + [ + "errors", + "expression_atlas", + "expression_atlas/accessions", + "expression_atlas/accessions/accessions.txt", + "expression_atlas/accessions/selected_experiments.metadata.tsv", + "expression_atlas/accessions/species_experiments.metadata.tsv", + "expression_atlas/datasets", + "expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", + "expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", + "geo", + "geo/accessions", + "geo/accessions/accessions.tsv", + "geo/accessions/geo_all_datasets.metadata.tsv", + "geo/accessions/geo_rejected_datasets.metadata.tsv", + "geo/accessions/geo_selected_datasets.metadata.tsv", + "geo/datasets", + "geo/datasets/GSE55951.design.csv", + "geo/datasets/GSE55951.microarray.normalised.counts.csv", + "multiqc", + "multiqc/multiqc_data", + "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_eatlas_all_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_rejected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_selected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_software_versions.txt", + "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_plots", + "multiqc/multiqc_plots/pdf", + "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_rejected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_selected_experiments_metadata.pdf", + "multiqc/multiqc_plots/png", + "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", + "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_rejected_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_selected_experiments_metadata.png", + "multiqc/multiqc_plots/svg", + "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_rejected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_selected_experiments_metadata.svg", + "multiqc/multiqc_report.html", + "multiqc/versions.yml", + "pipeline_info", + "pipeline_info/software_mqc_versions.yml", + "warnings" + ], + [ + "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", + "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", + "accessions.tsv:md5,095fb7f3a666b4382d4ba7296053451b", + "geo_all_datasets.metadata.tsv:md5,f7369624c52232498b5bc2454fa0821a", + "geo_rejected_datasets.metadata.tsv:md5,341a71045752afba5a83cedb0c0f23e8", + "geo_selected_datasets.metadata.tsv:md5,f7369624c52232498b5bc2454fa0821a", + "GSE55951.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d", + "GSE55951.microarray.normalised.counts.csv:md5,18fd2d728ad2ec5cb78f994f73375144", + "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", + "multiqc_eatlas_all_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", + "multiqc_eatlas_selected_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", + "multiqc_geo_all_experiments_metadata.txt:md5,daf7c9d1abf16831d9e1a24482393635", + "multiqc_geo_rejected_experiments_metadata.txt:md5,3b196d575f0051031ebd78a42d24e47a", + "multiqc_geo_selected_experiments_metadata.txt:md5,daf7c9d1abf16831d9e1a24482393635", + "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-08T11:07:13.377517982" + }, + "-profile test_one_rnaseq_one_microarray": { + "content": [ + null, [ "aggregate_results", "aggregate_results/all_counts_filtered.parquet", - "aggregate_results/stats_all_genes.csv", + "aggregate_results/all_genes_summary.csv", "aggregate_results/top_stable_genes_summary.csv", "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", "base_statistics", "base_statistics/all", "base_statistics/all/stats_all_genes.csv", + "base_statistics/microarray", + "base_statistics/microarray/microarray.stats_all_genes.csv", "base_statistics/rnaseq", - "base_statistics/rnaseq/stats_all_genes.csv", + "base_statistics/rnaseq/rnaseq.stats_all_genes.csv", "clean_count_data", "clean_count_data/cleaned_counts_filtered.parquet", "compute_stability_scores", "compute_stability_scores/stats_with_scores.csv", + "dash_app", + "dash_app/app.py", + "dash_app/assets", + "dash_app/assets/style.css", + "dash_app/data", + "dash_app/data/all_counts.parquet", + "dash_app/data/all_genes_summary.csv", + "dash_app/data/whole_design.csv", + "dash_app/file_system_backend", + "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", + "dash_app/spec-file.txt", + "dash_app/src", + "dash_app/src/callbacks", + "dash_app/src/callbacks/__pycache__", + "dash_app/src/callbacks/__pycache__/common.cpython-313.pyc", + "dash_app/src/callbacks/__pycache__/genes.cpython-313.pyc", + "dash_app/src/callbacks/__pycache__/samples.cpython-313.pyc", + "dash_app/src/callbacks/common.py", + "dash_app/src/callbacks/genes.py", + "dash_app/src/callbacks/samples.py", + "dash_app/src/components", + "dash_app/src/components/__pycache__", + "dash_app/src/components/__pycache__/graphs.cpython-313.pyc", + "dash_app/src/components/__pycache__/right_sidebar.cpython-313.pyc", + "dash_app/src/components/__pycache__/stores.cpython-313.pyc", + "dash_app/src/components/__pycache__/tables.cpython-313.pyc", + "dash_app/src/components/__pycache__/tooltips.cpython-313.pyc", + "dash_app/src/components/__pycache__/top.cpython-313.pyc", + "dash_app/src/components/graphs.py", + "dash_app/src/components/icons.py", + "dash_app/src/components/right_sidebar.py", + "dash_app/src/components/settings", + "dash_app/src/components/settings/__pycache__", + "dash_app/src/components/settings/__pycache__/genes.cpython-313.pyc", + "dash_app/src/components/settings/__pycache__/samples.cpython-313.pyc", + "dash_app/src/components/settings/genes.py", + "dash_app/src/components/settings/samples.py", + "dash_app/src/components/stores.py", + "dash_app/src/components/tables.py", + "dash_app/src/components/tooltips.py", + "dash_app/src/components/top.py", + "dash_app/src/utils", + "dash_app/src/utils/__pycache__", + "dash_app/src/utils/__pycache__/config.cpython-313.pyc", + "dash_app/src/utils/__pycache__/data_management.cpython-313.pyc", + "dash_app/src/utils/__pycache__/style.cpython-313.pyc", + "dash_app/src/utils/config.py", + "dash_app/src/utils/data_management.py", + "dash_app/src/utils/style.py", + "dash_app/versions.yml", "dataset_statistics", - "dataset_statistics/E_MTAB_8187_rnaseq.dataset_stats.csv", + "dataset_statistics/E_GEOD_21945_A_AFFY_2.dataset_stats.csv", + "dataset_statistics/E_GEOD_52806_rnaseq.dataset_stats.csv", + "errors", "expression_atlas", - "expression_atlas/accessions", - "expression_atlas/accessions/accessions.txt", - "expression_atlas/accessions/all_experiments.metadata.tsv", - "expression_atlas/accessions/filtered_experiments.keywords.yaml", - "expression_atlas/accessions/filtered_experiments.metadata.tsv", - "expression_atlas/accessions/species_experiments.metadata.tsv", "expression_atlas/datasets", - "expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", - "expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", - "genorm", - "genorm/chunks", - "genorm/chunks/count_chunk.0.parquet", - "genorm/chunks/count_chunk.1.parquet", - "genorm/chunks/count_chunk.10.parquet", - "genorm/chunks/count_chunk.11.parquet", - "genorm/chunks/count_chunk.12.parquet", - "genorm/chunks/count_chunk.13.parquet", - "genorm/chunks/count_chunk.14.parquet", - "genorm/chunks/count_chunk.15.parquet", - "genorm/chunks/count_chunk.16.parquet", - "genorm/chunks/count_chunk.2.parquet", - "genorm/chunks/count_chunk.3.parquet", - "genorm/chunks/count_chunk.4.parquet", - "genorm/chunks/count_chunk.5.parquet", - "genorm/chunks/count_chunk.6.parquet", - "genorm/chunks/count_chunk.7.parquet", - "genorm/chunks/count_chunk.8.parquet", - "genorm/chunks/count_chunk.9.parquet", - "genorm/cross_joins", - "genorm/cross_joins/cross_join.0.0.parquet", - "genorm/cross_joins/cross_join.0.1.parquet", - "genorm/cross_joins/cross_join.0.10.parquet", - "genorm/cross_joins/cross_join.0.11.parquet", - "genorm/cross_joins/cross_join.0.12.parquet", - "genorm/cross_joins/cross_join.0.13.parquet", - "genorm/cross_joins/cross_join.0.14.parquet", - "genorm/cross_joins/cross_join.0.15.parquet", - "genorm/cross_joins/cross_join.0.16.parquet", - "genorm/cross_joins/cross_join.0.2.parquet", - "genorm/cross_joins/cross_join.0.3.parquet", - "genorm/cross_joins/cross_join.0.4.parquet", - "genorm/cross_joins/cross_join.0.5.parquet", - "genorm/cross_joins/cross_join.0.6.parquet", - "genorm/cross_joins/cross_join.0.7.parquet", - "genorm/cross_joins/cross_join.0.8.parquet", - "genorm/cross_joins/cross_join.0.9.parquet", - "genorm/cross_joins/cross_join.1.1.parquet", - "genorm/cross_joins/cross_join.1.10.parquet", - "genorm/cross_joins/cross_join.1.11.parquet", - "genorm/cross_joins/cross_join.1.12.parquet", - "genorm/cross_joins/cross_join.1.13.parquet", - "genorm/cross_joins/cross_join.1.14.parquet", - "genorm/cross_joins/cross_join.1.15.parquet", - "genorm/cross_joins/cross_join.1.16.parquet", - "genorm/cross_joins/cross_join.1.2.parquet", - "genorm/cross_joins/cross_join.1.3.parquet", - "genorm/cross_joins/cross_join.1.4.parquet", - "genorm/cross_joins/cross_join.1.5.parquet", - "genorm/cross_joins/cross_join.1.6.parquet", - "genorm/cross_joins/cross_join.1.7.parquet", - "genorm/cross_joins/cross_join.1.8.parquet", - "genorm/cross_joins/cross_join.1.9.parquet", - "genorm/cross_joins/cross_join.10.10.parquet", - "genorm/cross_joins/cross_join.10.11.parquet", - "genorm/cross_joins/cross_join.10.12.parquet", - "genorm/cross_joins/cross_join.10.13.parquet", - "genorm/cross_joins/cross_join.10.14.parquet", - "genorm/cross_joins/cross_join.10.15.parquet", - "genorm/cross_joins/cross_join.10.16.parquet", - "genorm/cross_joins/cross_join.10.2.parquet", - "genorm/cross_joins/cross_join.10.3.parquet", - "genorm/cross_joins/cross_join.10.4.parquet", - "genorm/cross_joins/cross_join.10.5.parquet", - "genorm/cross_joins/cross_join.10.6.parquet", - "genorm/cross_joins/cross_join.10.7.parquet", - "genorm/cross_joins/cross_join.10.8.parquet", - "genorm/cross_joins/cross_join.10.9.parquet", - "genorm/cross_joins/cross_join.11.11.parquet", - "genorm/cross_joins/cross_join.11.12.parquet", - "genorm/cross_joins/cross_join.11.13.parquet", - "genorm/cross_joins/cross_join.11.14.parquet", - "genorm/cross_joins/cross_join.11.15.parquet", - "genorm/cross_joins/cross_join.11.16.parquet", - "genorm/cross_joins/cross_join.11.2.parquet", - "genorm/cross_joins/cross_join.11.3.parquet", - "genorm/cross_joins/cross_join.11.4.parquet", - "genorm/cross_joins/cross_join.11.5.parquet", - "genorm/cross_joins/cross_join.11.6.parquet", - "genorm/cross_joins/cross_join.11.7.parquet", - "genorm/cross_joins/cross_join.11.8.parquet", - "genorm/cross_joins/cross_join.11.9.parquet", - "genorm/cross_joins/cross_join.12.12.parquet", - "genorm/cross_joins/cross_join.12.13.parquet", - "genorm/cross_joins/cross_join.12.14.parquet", - "genorm/cross_joins/cross_join.12.15.parquet", - "genorm/cross_joins/cross_join.12.16.parquet", - "genorm/cross_joins/cross_join.12.2.parquet", - "genorm/cross_joins/cross_join.12.3.parquet", - "genorm/cross_joins/cross_join.12.4.parquet", - "genorm/cross_joins/cross_join.12.5.parquet", - "genorm/cross_joins/cross_join.12.6.parquet", - "genorm/cross_joins/cross_join.12.7.parquet", - "genorm/cross_joins/cross_join.12.8.parquet", - "genorm/cross_joins/cross_join.12.9.parquet", - "genorm/cross_joins/cross_join.13.13.parquet", - "genorm/cross_joins/cross_join.13.14.parquet", - "genorm/cross_joins/cross_join.13.15.parquet", - "genorm/cross_joins/cross_join.13.16.parquet", - "genorm/cross_joins/cross_join.13.2.parquet", - "genorm/cross_joins/cross_join.13.3.parquet", - "genorm/cross_joins/cross_join.13.4.parquet", - "genorm/cross_joins/cross_join.13.5.parquet", - "genorm/cross_joins/cross_join.13.6.parquet", - "genorm/cross_joins/cross_join.13.7.parquet", - "genorm/cross_joins/cross_join.13.8.parquet", - "genorm/cross_joins/cross_join.13.9.parquet", - "genorm/cross_joins/cross_join.14.14.parquet", - "genorm/cross_joins/cross_join.14.15.parquet", - "genorm/cross_joins/cross_join.14.16.parquet", - "genorm/cross_joins/cross_join.14.2.parquet", - "genorm/cross_joins/cross_join.14.3.parquet", - "genorm/cross_joins/cross_join.14.4.parquet", - "genorm/cross_joins/cross_join.14.5.parquet", - "genorm/cross_joins/cross_join.14.6.parquet", - "genorm/cross_joins/cross_join.14.7.parquet", - "genorm/cross_joins/cross_join.14.8.parquet", - "genorm/cross_joins/cross_join.14.9.parquet", - "genorm/cross_joins/cross_join.15.15.parquet", - "genorm/cross_joins/cross_join.15.16.parquet", - "genorm/cross_joins/cross_join.15.2.parquet", - "genorm/cross_joins/cross_join.15.3.parquet", - "genorm/cross_joins/cross_join.15.4.parquet", - "genorm/cross_joins/cross_join.15.5.parquet", - "genorm/cross_joins/cross_join.15.6.parquet", - "genorm/cross_joins/cross_join.15.7.parquet", - "genorm/cross_joins/cross_join.15.8.parquet", - "genorm/cross_joins/cross_join.15.9.parquet", - "genorm/cross_joins/cross_join.16.16.parquet", - "genorm/cross_joins/cross_join.16.2.parquet", - "genorm/cross_joins/cross_join.16.3.parquet", - "genorm/cross_joins/cross_join.16.4.parquet", - "genorm/cross_joins/cross_join.16.5.parquet", - "genorm/cross_joins/cross_join.16.6.parquet", - "genorm/cross_joins/cross_join.16.7.parquet", - "genorm/cross_joins/cross_join.16.8.parquet", - "genorm/cross_joins/cross_join.16.9.parquet", - "genorm/cross_joins/cross_join.2.2.parquet", - "genorm/cross_joins/cross_join.2.3.parquet", - "genorm/cross_joins/cross_join.2.4.parquet", - "genorm/cross_joins/cross_join.2.5.parquet", - "genorm/cross_joins/cross_join.2.6.parquet", - "genorm/cross_joins/cross_join.2.7.parquet", - "genorm/cross_joins/cross_join.2.8.parquet", - "genorm/cross_joins/cross_join.2.9.parquet", - "genorm/cross_joins/cross_join.3.3.parquet", - "genorm/cross_joins/cross_join.3.4.parquet", - "genorm/cross_joins/cross_join.3.5.parquet", - "genorm/cross_joins/cross_join.3.6.parquet", - "genorm/cross_joins/cross_join.3.7.parquet", - "genorm/cross_joins/cross_join.3.8.parquet", - "genorm/cross_joins/cross_join.3.9.parquet", - "genorm/cross_joins/cross_join.4.4.parquet", - "genorm/cross_joins/cross_join.4.5.parquet", - "genorm/cross_joins/cross_join.4.6.parquet", - "genorm/cross_joins/cross_join.4.7.parquet", - "genorm/cross_joins/cross_join.4.8.parquet", - "genorm/cross_joins/cross_join.4.9.parquet", - "genorm/cross_joins/cross_join.5.5.parquet", - "genorm/cross_joins/cross_join.5.6.parquet", - "genorm/cross_joins/cross_join.5.7.parquet", - "genorm/cross_joins/cross_join.5.8.parquet", - "genorm/cross_joins/cross_join.5.9.parquet", - "genorm/cross_joins/cross_join.6.6.parquet", - "genorm/cross_joins/cross_join.6.7.parquet", - "genorm/cross_joins/cross_join.6.8.parquet", - "genorm/cross_joins/cross_join.6.9.parquet", - "genorm/cross_joins/cross_join.7.7.parquet", - "genorm/cross_joins/cross_join.7.8.parquet", - "genorm/cross_joins/cross_join.7.9.parquet", - "genorm/cross_joins/cross_join.8.8.parquet", - "genorm/cross_joins/cross_join.8.9.parquet", - "genorm/cross_joins/cross_join.9.9.parquet", - "genorm/expression_ratios", - "genorm/expression_ratios/ratios.0.0.parquet", - "genorm/expression_ratios/ratios.0.1.parquet", - "genorm/expression_ratios/ratios.0.10.parquet", - "genorm/expression_ratios/ratios.0.11.parquet", - "genorm/expression_ratios/ratios.0.12.parquet", - "genorm/expression_ratios/ratios.0.13.parquet", - "genorm/expression_ratios/ratios.0.14.parquet", - "genorm/expression_ratios/ratios.0.15.parquet", - "genorm/expression_ratios/ratios.0.16.parquet", - "genorm/expression_ratios/ratios.0.2.parquet", - "genorm/expression_ratios/ratios.0.3.parquet", - "genorm/expression_ratios/ratios.0.4.parquet", - "genorm/expression_ratios/ratios.0.5.parquet", - "genorm/expression_ratios/ratios.0.6.parquet", - "genorm/expression_ratios/ratios.0.7.parquet", - "genorm/expression_ratios/ratios.0.8.parquet", - "genorm/expression_ratios/ratios.0.9.parquet", - "genorm/expression_ratios/ratios.1.1.parquet", - "genorm/expression_ratios/ratios.1.10.parquet", - "genorm/expression_ratios/ratios.1.11.parquet", - "genorm/expression_ratios/ratios.1.12.parquet", - "genorm/expression_ratios/ratios.1.13.parquet", - "genorm/expression_ratios/ratios.1.14.parquet", - "genorm/expression_ratios/ratios.1.15.parquet", - "genorm/expression_ratios/ratios.1.16.parquet", - "genorm/expression_ratios/ratios.1.2.parquet", - "genorm/expression_ratios/ratios.1.3.parquet", - "genorm/expression_ratios/ratios.1.4.parquet", - "genorm/expression_ratios/ratios.1.5.parquet", - "genorm/expression_ratios/ratios.1.6.parquet", - "genorm/expression_ratios/ratios.1.7.parquet", - "genorm/expression_ratios/ratios.1.8.parquet", - "genorm/expression_ratios/ratios.1.9.parquet", - "genorm/expression_ratios/ratios.10.10.parquet", - "genorm/expression_ratios/ratios.10.11.parquet", - "genorm/expression_ratios/ratios.10.12.parquet", - "genorm/expression_ratios/ratios.10.13.parquet", - "genorm/expression_ratios/ratios.10.14.parquet", - "genorm/expression_ratios/ratios.10.15.parquet", - "genorm/expression_ratios/ratios.10.16.parquet", - "genorm/expression_ratios/ratios.10.2.parquet", - "genorm/expression_ratios/ratios.10.3.parquet", - "genorm/expression_ratios/ratios.10.4.parquet", - "genorm/expression_ratios/ratios.10.5.parquet", - "genorm/expression_ratios/ratios.10.6.parquet", - "genorm/expression_ratios/ratios.10.7.parquet", - "genorm/expression_ratios/ratios.10.8.parquet", - "genorm/expression_ratios/ratios.10.9.parquet", - "genorm/expression_ratios/ratios.11.11.parquet", - "genorm/expression_ratios/ratios.11.12.parquet", - "genorm/expression_ratios/ratios.11.13.parquet", - "genorm/expression_ratios/ratios.11.14.parquet", - "genorm/expression_ratios/ratios.11.15.parquet", - "genorm/expression_ratios/ratios.11.16.parquet", - "genorm/expression_ratios/ratios.11.2.parquet", - "genorm/expression_ratios/ratios.11.3.parquet", - "genorm/expression_ratios/ratios.11.4.parquet", - "genorm/expression_ratios/ratios.11.5.parquet", - "genorm/expression_ratios/ratios.11.6.parquet", - "genorm/expression_ratios/ratios.11.7.parquet", - "genorm/expression_ratios/ratios.11.8.parquet", - "genorm/expression_ratios/ratios.11.9.parquet", - "genorm/expression_ratios/ratios.12.12.parquet", - "genorm/expression_ratios/ratios.12.13.parquet", - "genorm/expression_ratios/ratios.12.14.parquet", - "genorm/expression_ratios/ratios.12.15.parquet", - "genorm/expression_ratios/ratios.12.16.parquet", - "genorm/expression_ratios/ratios.12.2.parquet", - "genorm/expression_ratios/ratios.12.3.parquet", - "genorm/expression_ratios/ratios.12.4.parquet", - "genorm/expression_ratios/ratios.12.5.parquet", - "genorm/expression_ratios/ratios.12.6.parquet", - "genorm/expression_ratios/ratios.12.7.parquet", - "genorm/expression_ratios/ratios.12.8.parquet", - "genorm/expression_ratios/ratios.12.9.parquet", - "genorm/expression_ratios/ratios.13.13.parquet", - "genorm/expression_ratios/ratios.13.14.parquet", - "genorm/expression_ratios/ratios.13.15.parquet", - "genorm/expression_ratios/ratios.13.16.parquet", - "genorm/expression_ratios/ratios.13.2.parquet", - "genorm/expression_ratios/ratios.13.3.parquet", - "genorm/expression_ratios/ratios.13.4.parquet", - "genorm/expression_ratios/ratios.13.5.parquet", - "genorm/expression_ratios/ratios.13.6.parquet", - "genorm/expression_ratios/ratios.13.7.parquet", - "genorm/expression_ratios/ratios.13.8.parquet", - "genorm/expression_ratios/ratios.13.9.parquet", - "genorm/expression_ratios/ratios.14.14.parquet", - "genorm/expression_ratios/ratios.14.15.parquet", - "genorm/expression_ratios/ratios.14.16.parquet", - "genorm/expression_ratios/ratios.14.2.parquet", - "genorm/expression_ratios/ratios.14.3.parquet", - "genorm/expression_ratios/ratios.14.4.parquet", - "genorm/expression_ratios/ratios.14.5.parquet", - "genorm/expression_ratios/ratios.14.6.parquet", - "genorm/expression_ratios/ratios.14.7.parquet", - "genorm/expression_ratios/ratios.14.8.parquet", - "genorm/expression_ratios/ratios.14.9.parquet", - "genorm/expression_ratios/ratios.15.15.parquet", - "genorm/expression_ratios/ratios.15.16.parquet", - "genorm/expression_ratios/ratios.15.2.parquet", - "genorm/expression_ratios/ratios.15.3.parquet", - "genorm/expression_ratios/ratios.15.4.parquet", - "genorm/expression_ratios/ratios.15.5.parquet", - "genorm/expression_ratios/ratios.15.6.parquet", - "genorm/expression_ratios/ratios.15.7.parquet", - "genorm/expression_ratios/ratios.15.8.parquet", - "genorm/expression_ratios/ratios.15.9.parquet", - "genorm/expression_ratios/ratios.16.16.parquet", - "genorm/expression_ratios/ratios.16.2.parquet", - "genorm/expression_ratios/ratios.16.3.parquet", - "genorm/expression_ratios/ratios.16.4.parquet", - "genorm/expression_ratios/ratios.16.5.parquet", - "genorm/expression_ratios/ratios.16.6.parquet", - "genorm/expression_ratios/ratios.16.7.parquet", - "genorm/expression_ratios/ratios.16.8.parquet", - "genorm/expression_ratios/ratios.16.9.parquet", - "genorm/expression_ratios/ratios.2.2.parquet", - "genorm/expression_ratios/ratios.2.3.parquet", - "genorm/expression_ratios/ratios.2.4.parquet", - "genorm/expression_ratios/ratios.2.5.parquet", - "genorm/expression_ratios/ratios.2.6.parquet", - "genorm/expression_ratios/ratios.2.7.parquet", - "genorm/expression_ratios/ratios.2.8.parquet", - "genorm/expression_ratios/ratios.2.9.parquet", - "genorm/expression_ratios/ratios.3.3.parquet", - "genorm/expression_ratios/ratios.3.4.parquet", - "genorm/expression_ratios/ratios.3.5.parquet", - "genorm/expression_ratios/ratios.3.6.parquet", - "genorm/expression_ratios/ratios.3.7.parquet", - "genorm/expression_ratios/ratios.3.8.parquet", - "genorm/expression_ratios/ratios.3.9.parquet", - "genorm/expression_ratios/ratios.4.4.parquet", - "genorm/expression_ratios/ratios.4.5.parquet", - "genorm/expression_ratios/ratios.4.6.parquet", - "genorm/expression_ratios/ratios.4.7.parquet", - "genorm/expression_ratios/ratios.4.8.parquet", - "genorm/expression_ratios/ratios.4.9.parquet", - "genorm/expression_ratios/ratios.5.5.parquet", - "genorm/expression_ratios/ratios.5.6.parquet", - "genorm/expression_ratios/ratios.5.7.parquet", - "genorm/expression_ratios/ratios.5.8.parquet", - "genorm/expression_ratios/ratios.5.9.parquet", - "genorm/expression_ratios/ratios.6.6.parquet", - "genorm/expression_ratios/ratios.6.7.parquet", - "genorm/expression_ratios/ratios.6.8.parquet", - "genorm/expression_ratios/ratios.6.9.parquet", - "genorm/expression_ratios/ratios.7.7.parquet", - "genorm/expression_ratios/ratios.7.8.parquet", - "genorm/expression_ratios/ratios.7.9.parquet", - "genorm/expression_ratios/ratios.8.8.parquet", - "genorm/expression_ratios/ratios.8.9.parquet", - "genorm/expression_ratios/ratios.9.9.parquet", - "genorm/ratio_standard_variations", - "genorm/ratio_standard_variations/std.0.0.parquet", - "genorm/ratio_standard_variations/std.0.1.parquet", - "genorm/ratio_standard_variations/std.0.10.parquet", - "genorm/ratio_standard_variations/std.0.11.parquet", - "genorm/ratio_standard_variations/std.0.12.parquet", - "genorm/ratio_standard_variations/std.0.13.parquet", - "genorm/ratio_standard_variations/std.0.14.parquet", - "genorm/ratio_standard_variations/std.0.15.parquet", - "genorm/ratio_standard_variations/std.0.16.parquet", - "genorm/ratio_standard_variations/std.0.2.parquet", - "genorm/ratio_standard_variations/std.0.3.parquet", - "genorm/ratio_standard_variations/std.0.4.parquet", - "genorm/ratio_standard_variations/std.0.5.parquet", - "genorm/ratio_standard_variations/std.0.6.parquet", - "genorm/ratio_standard_variations/std.0.7.parquet", - "genorm/ratio_standard_variations/std.0.8.parquet", - "genorm/ratio_standard_variations/std.0.9.parquet", - "genorm/ratio_standard_variations/std.1.1.parquet", - "genorm/ratio_standard_variations/std.1.10.parquet", - "genorm/ratio_standard_variations/std.1.11.parquet", - "genorm/ratio_standard_variations/std.1.12.parquet", - "genorm/ratio_standard_variations/std.1.13.parquet", - "genorm/ratio_standard_variations/std.1.14.parquet", - "genorm/ratio_standard_variations/std.1.15.parquet", - "genorm/ratio_standard_variations/std.1.16.parquet", - "genorm/ratio_standard_variations/std.1.2.parquet", - "genorm/ratio_standard_variations/std.1.3.parquet", - "genorm/ratio_standard_variations/std.1.4.parquet", - "genorm/ratio_standard_variations/std.1.5.parquet", - "genorm/ratio_standard_variations/std.1.6.parquet", - "genorm/ratio_standard_variations/std.1.7.parquet", - "genorm/ratio_standard_variations/std.1.8.parquet", - "genorm/ratio_standard_variations/std.1.9.parquet", - "genorm/ratio_standard_variations/std.10.10.parquet", - "genorm/ratio_standard_variations/std.10.11.parquet", - "genorm/ratio_standard_variations/std.10.12.parquet", - "genorm/ratio_standard_variations/std.10.13.parquet", - "genorm/ratio_standard_variations/std.10.14.parquet", - "genorm/ratio_standard_variations/std.10.15.parquet", - "genorm/ratio_standard_variations/std.10.16.parquet", - "genorm/ratio_standard_variations/std.10.2.parquet", - "genorm/ratio_standard_variations/std.10.3.parquet", - "genorm/ratio_standard_variations/std.10.4.parquet", - "genorm/ratio_standard_variations/std.10.5.parquet", - "genorm/ratio_standard_variations/std.10.6.parquet", - "genorm/ratio_standard_variations/std.10.7.parquet", - "genorm/ratio_standard_variations/std.10.8.parquet", - "genorm/ratio_standard_variations/std.10.9.parquet", - "genorm/ratio_standard_variations/std.11.11.parquet", - "genorm/ratio_standard_variations/std.11.12.parquet", - "genorm/ratio_standard_variations/std.11.13.parquet", - "genorm/ratio_standard_variations/std.11.14.parquet", - "genorm/ratio_standard_variations/std.11.15.parquet", - "genorm/ratio_standard_variations/std.11.16.parquet", - "genorm/ratio_standard_variations/std.11.2.parquet", - "genorm/ratio_standard_variations/std.11.3.parquet", - "genorm/ratio_standard_variations/std.11.4.parquet", - "genorm/ratio_standard_variations/std.11.5.parquet", - "genorm/ratio_standard_variations/std.11.6.parquet", - "genorm/ratio_standard_variations/std.11.7.parquet", - "genorm/ratio_standard_variations/std.11.8.parquet", - "genorm/ratio_standard_variations/std.11.9.parquet", - "genorm/ratio_standard_variations/std.12.12.parquet", - "genorm/ratio_standard_variations/std.12.13.parquet", - "genorm/ratio_standard_variations/std.12.14.parquet", - "genorm/ratio_standard_variations/std.12.15.parquet", - "genorm/ratio_standard_variations/std.12.16.parquet", - "genorm/ratio_standard_variations/std.12.2.parquet", - "genorm/ratio_standard_variations/std.12.3.parquet", - "genorm/ratio_standard_variations/std.12.4.parquet", - "genorm/ratio_standard_variations/std.12.5.parquet", - "genorm/ratio_standard_variations/std.12.6.parquet", - "genorm/ratio_standard_variations/std.12.7.parquet", - "genorm/ratio_standard_variations/std.12.8.parquet", - "genorm/ratio_standard_variations/std.12.9.parquet", - "genorm/ratio_standard_variations/std.13.13.parquet", - "genorm/ratio_standard_variations/std.13.14.parquet", - "genorm/ratio_standard_variations/std.13.15.parquet", - "genorm/ratio_standard_variations/std.13.16.parquet", - "genorm/ratio_standard_variations/std.13.2.parquet", - "genorm/ratio_standard_variations/std.13.3.parquet", - "genorm/ratio_standard_variations/std.13.4.parquet", - "genorm/ratio_standard_variations/std.13.5.parquet", - "genorm/ratio_standard_variations/std.13.6.parquet", - "genorm/ratio_standard_variations/std.13.7.parquet", - "genorm/ratio_standard_variations/std.13.8.parquet", - "genorm/ratio_standard_variations/std.13.9.parquet", - "genorm/ratio_standard_variations/std.14.14.parquet", - "genorm/ratio_standard_variations/std.14.15.parquet", - "genorm/ratio_standard_variations/std.14.16.parquet", - "genorm/ratio_standard_variations/std.14.2.parquet", - "genorm/ratio_standard_variations/std.14.3.parquet", - "genorm/ratio_standard_variations/std.14.4.parquet", - "genorm/ratio_standard_variations/std.14.5.parquet", - "genorm/ratio_standard_variations/std.14.6.parquet", - "genorm/ratio_standard_variations/std.14.7.parquet", - "genorm/ratio_standard_variations/std.14.8.parquet", - "genorm/ratio_standard_variations/std.14.9.parquet", - "genorm/ratio_standard_variations/std.15.15.parquet", - "genorm/ratio_standard_variations/std.15.16.parquet", - "genorm/ratio_standard_variations/std.15.2.parquet", - "genorm/ratio_standard_variations/std.15.3.parquet", - "genorm/ratio_standard_variations/std.15.4.parquet", - "genorm/ratio_standard_variations/std.15.5.parquet", - "genorm/ratio_standard_variations/std.15.6.parquet", - "genorm/ratio_standard_variations/std.15.7.parquet", - "genorm/ratio_standard_variations/std.15.8.parquet", - "genorm/ratio_standard_variations/std.15.9.parquet", - "genorm/ratio_standard_variations/std.16.16.parquet", - "genorm/ratio_standard_variations/std.16.2.parquet", - "genorm/ratio_standard_variations/std.16.3.parquet", - "genorm/ratio_standard_variations/std.16.4.parquet", - "genorm/ratio_standard_variations/std.16.5.parquet", - "genorm/ratio_standard_variations/std.16.6.parquet", - "genorm/ratio_standard_variations/std.16.7.parquet", - "genorm/ratio_standard_variations/std.16.8.parquet", - "genorm/ratio_standard_variations/std.16.9.parquet", - "genorm/ratio_standard_variations/std.2.2.parquet", - "genorm/ratio_standard_variations/std.2.3.parquet", - "genorm/ratio_standard_variations/std.2.4.parquet", - "genorm/ratio_standard_variations/std.2.5.parquet", - "genorm/ratio_standard_variations/std.2.6.parquet", - "genorm/ratio_standard_variations/std.2.7.parquet", - "genorm/ratio_standard_variations/std.2.8.parquet", - "genorm/ratio_standard_variations/std.2.9.parquet", - "genorm/ratio_standard_variations/std.3.3.parquet", - "genorm/ratio_standard_variations/std.3.4.parquet", - "genorm/ratio_standard_variations/std.3.5.parquet", - "genorm/ratio_standard_variations/std.3.6.parquet", - "genorm/ratio_standard_variations/std.3.7.parquet", - "genorm/ratio_standard_variations/std.3.8.parquet", - "genorm/ratio_standard_variations/std.3.9.parquet", - "genorm/ratio_standard_variations/std.4.4.parquet", - "genorm/ratio_standard_variations/std.4.5.parquet", - "genorm/ratio_standard_variations/std.4.6.parquet", - "genorm/ratio_standard_variations/std.4.7.parquet", - "genorm/ratio_standard_variations/std.4.8.parquet", - "genorm/ratio_standard_variations/std.4.9.parquet", - "genorm/ratio_standard_variations/std.5.5.parquet", - "genorm/ratio_standard_variations/std.5.6.parquet", - "genorm/ratio_standard_variations/std.5.7.parquet", - "genorm/ratio_standard_variations/std.5.8.parquet", - "genorm/ratio_standard_variations/std.5.9.parquet", - "genorm/ratio_standard_variations/std.6.6.parquet", - "genorm/ratio_standard_variations/std.6.7.parquet", - "genorm/ratio_standard_variations/std.6.8.parquet", - "genorm/ratio_standard_variations/std.6.9.parquet", - "genorm/ratio_standard_variations/std.7.7.parquet", - "genorm/ratio_standard_variations/std.7.8.parquet", - "genorm/ratio_standard_variations/std.7.9.parquet", - "genorm/ratio_standard_variations/std.8.8.parquet", - "genorm/ratio_standard_variations/std.8.9.parquet", - "genorm/ratio_standard_variations/std.9.9.parquet", + "expression_atlas/datasets/E_GEOD_21945_A_AFFY_2.design.csv", + "expression_atlas/datasets/E_GEOD_21945_A_AFFY_2.microarray.normalised.counts.csv", + "expression_atlas/datasets/E_GEOD_52806_rnaseq.design.csv", + "expression_atlas/datasets/E_GEOD_52806_rnaseq.rnaseq.raw.counts.csv", "geo", - "geo/accessions", - "geo/accessions/accessions.txt", - "geo/accessions/filtered_datasets.metadata.tsv", - "geo/accessions/rejected_datasets.metadata.tsv", - "geo/accessions/selected_datasets.keywords.yaml", - "geo/accessions/species_datasets.metadata.tsv", + "geo/excluded_geo_accessions.txt", "get_candidate_genes", "get_candidate_genes/candidate_counts.parquet", "idmapping", "idmapping/datasets", - "idmapping/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv", - "merge_designs", - "merge_designs/whole_design.csv", + "idmapping/datasets/E_GEOD_21945_A_AFFY_2.microarray.normalised.counts.mapping.csv", + "idmapping/datasets/E_GEOD_21945_A_AFFY_2.microarray.normalised.counts.metadata.csv", + "idmapping/datasets/E_GEOD_21945_A_AFFY_2.microarray.normalised.counts.renamed.csv", + "idmapping/datasets/E_GEOD_52806_rnaseq.rnaseq.raw.counts.mapping.csv", + "idmapping/datasets/E_GEOD_52806_rnaseq.rnaseq.raw.counts.metadata.csv", + "idmapping/datasets/E_GEOD_52806_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/whole_gene_id_mapping.csv", + "idmapping/whole_gene_metadata.csv", "merged_datasets", "merged_datasets/all", "merged_datasets/all/all_counts.parquet", + "merged_datasets/microarray", + "merged_datasets/microarray/all_counts.parquet", "merged_datasets/rnaseq", "merged_datasets/rnaseq/all_counts.parquet", + "merged_datasets/whole_design.csv", "multiqc", "multiqc/multiqc_data", + "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_expression_distributions_top_stable_genes.txt", + "multiqc/multiqc_data/multiqc_gene_statistics.txt", + "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", + "multiqc/multiqc_data/multiqc_software_versions.txt", + "multiqc/multiqc_data/multiqc_sources.txt", "multiqc/multiqc_plots", + "multiqc/multiqc_plots/pdf", + "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", + "multiqc/multiqc_plots/pdf/gene_statistics.pdf", + "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", + "multiqc/multiqc_plots/png", + "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", + "multiqc/multiqc_plots/png/gene_statistics.png", + "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", + "multiqc/multiqc_plots/svg", + "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", + "multiqc/multiqc_plots/svg/gene_statistics.svg", + "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", "normalised", - "normalised/E_MTAB_8187_rnaseq", - "normalised/E_MTAB_8187_rnaseq/deseq2", - "normalised/E_MTAB_8187_rnaseq/deseq2/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normalised/E_GEOD_52806_rnaseq", + "normalised/E_GEOD_52806_rnaseq/normalisation_deseq2", + "normalised/E_GEOD_52806_rnaseq/normalisation_deseq2/E_GEOD_52806_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", "pipeline_info", - "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", + "pipeline_info/software_mqc_versions.yml", "quantile_normalised", - "quantile_normalised/E_MTAB_8187_rnaseq", - "quantile_normalised/E_MTAB_8187_rnaseq/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", + "quantile_normalised/E_GEOD_21945_A_AFFY_2", + "quantile_normalised/E_GEOD_21945_A_AFFY_2/E_GEOD_21945_A_AFFY_2.microarray.normalised.counts.renamed.quant_norm.parquet", + "quantile_normalised/E_GEOD_52806_rnaseq", + "quantile_normalised/E_GEOD_52806_rnaseq/E_GEOD_52806_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", "stability_scoring", - "stability_scoring/genorm", - "stability_scoring/genorm/m_measures.csv", "stability_scoring/normfinder", - "stability_scoring/normfinder/stability_values.csv" + "stability_scoring/normfinder/stability_values.normfinder.csv", + "warnings" ], [ - "all_counts_filtered.parquet:md5,99d4d034bddd5a46340ba93c26c3125e", - "stats_all_genes.csv:md5,a951798726f3fa7ad88f9b21bd0e5d26", - "top_stable_genes_summary.csv:md5,0def60a7945664a13c8485bbe7990744", - "top_stable_genes_transposed_counts_filtered.csv:md5,946ea76e7ae94173c71043d49a96c841", - "stats_all_genes.csv:md5,d7df222d27f435032c8775c68b96e07a", - "stats_all_genes.csv:md5,d7df222d27f435032c8775c68b96e07a", - "cleaned_counts_filtered.parquet:md5,03c31345d3be9fc2d2a47136f43f4e4c", - "stats_with_scores.csv:md5,4d3bc97f172a647ea7f96711b053d4e9", - "E_MTAB_8187_rnaseq.dataset_stats.csv:md5,985cdbddaa17b599e824a5ecd8167cca", - "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", - "all_experiments.metadata.tsv:md5,00780d7b6f6134a1ae6c8bc07d439587", - "filtered_experiments.keywords.yaml:md5,70f58279aa541b43ddc85e7444bc6370", - "filtered_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", - "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", - "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", - "count_chunk.0.parquet:md5,a2c0831918d2ef31b051d13ab740f5ec", - "count_chunk.1.parquet:md5,e7c543c11120a7c3a43293031b44195a", - "count_chunk.10.parquet:md5,459b35bab04f756eb2f02687da8b8674", - "count_chunk.11.parquet:md5,9d41f56ddd0dfd3143c246b92b81fc93", - "count_chunk.12.parquet:md5,fa74e83cff73c91f41319205c87386f8", - "count_chunk.13.parquet:md5,42789d02aa82f56c0e1e90b1bcf435d2", - "count_chunk.14.parquet:md5,36283788c022c3013b010974f9171910", - "count_chunk.15.parquet:md5,67b88d5116b3fcb2237b279ab356f8f8", - "count_chunk.16.parquet:md5,4cf49284a7b8296d803928280ce6c677", - "count_chunk.2.parquet:md5,ebcce7e818df501a2eee2dd823b2780e", - "count_chunk.3.parquet:md5,12bdeb01c4ff00f7d4f92f92bc1369bb", - "count_chunk.4.parquet:md5,87333ea1dd74a530f3e9badc4fac8b26", - "count_chunk.5.parquet:md5,c9e839a10575a4cfcfa8aa27f91f9ded", - "count_chunk.6.parquet:md5,feaef549b263f3aa331373e1834841c3", - "count_chunk.7.parquet:md5,e5dba7eff508b7ec423ec75749c460b5", - "count_chunk.8.parquet:md5,2aec0c2dd58f2c0af76ae07bef8a6342", - "count_chunk.9.parquet:md5,8547319955ad2ed62e6c5aa2ed63d352", - "cross_join.0.0.parquet:md5,10b8de189ab2ce11c5ed33b12d2c357d", - "cross_join.0.1.parquet:md5,7204ba359c0f874674fceaed8fb73854", - "cross_join.0.10.parquet:md5,42356453d0460b9c99920e582a2f6836", - "cross_join.0.11.parquet:md5,a95aa0ed6f79e478968b3df56dcf0cca", - "cross_join.0.12.parquet:md5,e29c5e810193cd5d3deaa390f5a9b5cb", - "cross_join.0.13.parquet:md5,78726daff8a4a2db7c552aed7bdc8cc9", - "cross_join.0.14.parquet:md5,87f5da223cdb03f69356ab8a512fbbdf", - "cross_join.0.15.parquet:md5,0fce6cf427200d2c15b1fce50f5d5b0b", - "cross_join.0.16.parquet:md5,ee93e516bef1bcc6ec02b891c4393186", - "cross_join.0.2.parquet:md5,e60b1a80f3fc3f867259b28598ae3a54", - "cross_join.0.3.parquet:md5,44a32599262ac2725576280fb8a59d90", - "cross_join.0.4.parquet:md5,12f700d3f4dadd7489bff24c5faf8c1d", - "cross_join.0.5.parquet:md5,fafd2662f8cfea4bfa5f5d547c5f6f24", - "cross_join.0.6.parquet:md5,e9dcde16379f2ef1571cd3d4d2372fd2", - "cross_join.0.7.parquet:md5,c2af8dc86464925a40315caf3c524f5d", - "cross_join.0.8.parquet:md5,ed70121bc2073cb87b0900d9ee9cfc02", - "cross_join.0.9.parquet:md5,6e554a577cee1c762bb1ccbe889b13be", - "cross_join.1.1.parquet:md5,7ba68af3b852c3998d269985de9bd824", - "cross_join.1.10.parquet:md5,983198481194d98e181400b3b9a43ed1", - "cross_join.1.11.parquet:md5,ecc34a521ee73818a75ab64323654239", - "cross_join.1.12.parquet:md5,53d923e15b4ca633354349232a93a609", - "cross_join.1.13.parquet:md5,63ac3cd37d2cbce60a03b0999e0af0c3", - "cross_join.1.14.parquet:md5,89adb0989aa0a2ed2b71522133b65781", - "cross_join.1.15.parquet:md5,7daefae1473e9d3d5a9dc8760d18d2b7", - "cross_join.1.16.parquet:md5,a201ac22842aae9cb8d7189a52f429e9", - "cross_join.1.2.parquet:md5,7fc1a41f594b80d7075cc7adfd444a95", - "cross_join.1.3.parquet:md5,f84db6f0d44d987fda945f3471eb612c", - "cross_join.1.4.parquet:md5,70abda9206bb5197ea288cfefcd88a0f", - "cross_join.1.5.parquet:md5,04fb7a3c55cb6260d6086dcba134e0a9", - "cross_join.1.6.parquet:md5,f7441f088592c0fa4bcd4730619c098b", - "cross_join.1.7.parquet:md5,69781baa7a2f2c92c9837273c5b275f9", - "cross_join.1.8.parquet:md5,fffc5e652f45e7bcef759624b11d38c9", - "cross_join.1.9.parquet:md5,8eee4a9290704e2d3051f51787ae2459", - "cross_join.10.10.parquet:md5,57362d101071f96d88ff49ad46e00f63", - "cross_join.10.11.parquet:md5,5a3988889c2920323d834d9f49331120", - "cross_join.10.12.parquet:md5,a3e0686a1c7b220e9235a685e9489719", - "cross_join.10.13.parquet:md5,929454fffc85ae392c12dc6eb92d608d", - "cross_join.10.14.parquet:md5,8246b045c97f4cdb4d9846d7c2ce60a5", - "cross_join.10.15.parquet:md5,d43034a7e8002cedce7b0c1f4cb92aa1", - "cross_join.10.16.parquet:md5,a053a9650d057a1b248f512bc199c9c4", - "cross_join.10.2.parquet:md5,6976287c9582d07739e13674d558b537", - "cross_join.10.3.parquet:md5,b7d68a5b33898b4792a491080f8ab4f8", - "cross_join.10.4.parquet:md5,485b375a20fcfd6749f6a55dd194dd63", - "cross_join.10.5.parquet:md5,302c4df6ba65c0611498813f2ede790f", - "cross_join.10.6.parquet:md5,faef4541522d6565dc970844ef75525f", - "cross_join.10.7.parquet:md5,1f9903054536caca8813fbd746a76317", - "cross_join.10.8.parquet:md5,c06d78ff650054e7cd6d49ea897ac466", - "cross_join.10.9.parquet:md5,da0cbcf24559112c2d2eeea9fbffba95", - "cross_join.11.11.parquet:md5,c40a83dafcc9036af382537fdf29f180", - "cross_join.11.12.parquet:md5,0d2994cc97074de5062434ea28470146", - "cross_join.11.13.parquet:md5,af485e062d8424e352a3f758f6bec35b", - "cross_join.11.14.parquet:md5,d8cc2b2446220605d09a2ffcf06925b1", - "cross_join.11.15.parquet:md5,2bf70f575e69c2b25bcc5d3aea65f0b6", - "cross_join.11.16.parquet:md5,1e0226975591841057508f040d2cf962", - "cross_join.11.2.parquet:md5,f243970486a30938e8120ded4e5020ec", - "cross_join.11.3.parquet:md5,d8a48f56bab62d79a39ec292384693fb", - "cross_join.11.4.parquet:md5,8f50a700acf376522ed53821b0fc8985", - "cross_join.11.5.parquet:md5,f667fbe953cbc0b1b6304512ba065d33", - "cross_join.11.6.parquet:md5,7f23d66197042d1a5c2f36b9b01b5ba6", - "cross_join.11.7.parquet:md5,aa73709aa1da582f265f90f9665bc8c7", - "cross_join.11.8.parquet:md5,29ef550884cd66a5c365d1f1c1cacb3d", - "cross_join.11.9.parquet:md5,75aad8f4bdc18e86e380ae1d66330848", - "cross_join.12.12.parquet:md5,17fa7962302f175b4dc7abab571e36a3", - "cross_join.12.13.parquet:md5,3951c700c94487335760a6e3675176d0", - "cross_join.12.14.parquet:md5,77ad0f1401a3509e839636edb630124f", - "cross_join.12.15.parquet:md5,44619a0b5c0aea382dc7f1158e1fec8f", - "cross_join.12.16.parquet:md5,7b1cbe4994c884754eaed77f284c3396", - "cross_join.12.2.parquet:md5,317e76586885d3bd008cc924ab720f97", - "cross_join.12.3.parquet:md5,e016e8a48e58053ff2789a5bb439bc53", - "cross_join.12.4.parquet:md5,8de928e518c183d09ea681108163e1b8", - "cross_join.12.5.parquet:md5,70d33fd172daca1f0f3d2974098002d5", - "cross_join.12.6.parquet:md5,f144508bc6b59bd3eb883d4ec97c0169", - "cross_join.12.7.parquet:md5,6bc17de7f684d2babd26a20216dff5ca", - "cross_join.12.8.parquet:md5,23b6ef917b9973614e63a87a8996170d", - "cross_join.12.9.parquet:md5,bf0512b5103e0ee96ce89885ba394c52", - "cross_join.13.13.parquet:md5,5a6a5e665435e3118a7c050398f5b512", - "cross_join.13.14.parquet:md5,4aad82f9c1fceaa0b9ee7e95bdf34471", - "cross_join.13.15.parquet:md5,d10fb59f4e1833173ddc632a0c3bf4ec", - "cross_join.13.16.parquet:md5,e2d2a1d0dbdf171b79546e654e48ff8f", - "cross_join.13.2.parquet:md5,7bb91dd20a833f39dc4a64301e9f238b", - "cross_join.13.3.parquet:md5,9697a4634c94bf837b70d6c91f3ba747", - "cross_join.13.4.parquet:md5,320d874a1995e4487a99cb8234117e35", - "cross_join.13.5.parquet:md5,a73b180010f446da5dfe913af1b72bbf", - "cross_join.13.6.parquet:md5,c031354da4c3a459716de6ed0cdcc4f4", - "cross_join.13.7.parquet:md5,1ebae9657e2016ba503ce2b84f55c030", - "cross_join.13.8.parquet:md5,58152e4bcd37781e99942d2ae9af896d", - "cross_join.13.9.parquet:md5,cc6e55f09badfa1622d5ae2f9a721e8e", - "cross_join.14.14.parquet:md5,d0cee3adcc2442ab839d74e67722a32a", - "cross_join.14.15.parquet:md5,e131cd9501db7712ab05b37fad617270", - "cross_join.14.16.parquet:md5,8425d683771131d8b05da8116d3ad094", - "cross_join.14.2.parquet:md5,13b4ee2f323d2e9ce142a21689a8e935", - "cross_join.14.3.parquet:md5,684591ebc55e30e71c9dc0ab31ffef25", - "cross_join.14.4.parquet:md5,394270132f0b6556afc68c3e5034e691", - "cross_join.14.5.parquet:md5,8a047a3be3abbc60d2ef6f3449c06d67", - "cross_join.14.6.parquet:md5,0da716779ea5a0fddb62278b4e5453e2", - "cross_join.14.7.parquet:md5,ede6189d716369cf78c8f28e240196c4", - "cross_join.14.8.parquet:md5,42268ca5d573b5e477adf49d14ccc7ac", - "cross_join.14.9.parquet:md5,20db4456ede343fafd94c1e5b8cc9336", - "cross_join.15.15.parquet:md5,0d878164759c252d47eca6b696197462", - "cross_join.15.16.parquet:md5,d7ae68bb82120c232c9d33f91fb2e483", - "cross_join.15.2.parquet:md5,5559202fe953cbb4702d87d0c98e0a26", - "cross_join.15.3.parquet:md5,5376128859aea7778817662c07e6a2bc", - "cross_join.15.4.parquet:md5,42a57bfaead502bc1a5442a752694b97", - "cross_join.15.5.parquet:md5,88d463e44164da5123f8018397a8357d", - "cross_join.15.6.parquet:md5,80716ffeaf4d0d6aef2544d8d8d90e6a", - "cross_join.15.7.parquet:md5,68f21abb8d8dbfa36f702a24e1f3ca07", - "cross_join.15.8.parquet:md5,fa3966294c77a81e6ddb3460ff357ed3", - "cross_join.15.9.parquet:md5,ebfd9d3c515105cc57e7fdbbf8beb577", - "cross_join.16.16.parquet:md5,3b07e792dc498fcaf3c1bafc8e942252", - "cross_join.16.2.parquet:md5,e5177c73d3240db9bc93abbcfb7d486a", - "cross_join.16.3.parquet:md5,9cb7139d6eaf3f9fd43a561bf52c588d", - "cross_join.16.4.parquet:md5,451fec008a476de63b64a95f23c3ce92", - "cross_join.16.5.parquet:md5,508a4ac27b802d0131db21981e1a04fc", - "cross_join.16.6.parquet:md5,8205dd24913db1602d8d1c412b3772c8", - "cross_join.16.7.parquet:md5,f24a12a3a6e3261f503e99f1f0b0a0b1", - "cross_join.16.8.parquet:md5,813d16fd0a29318f163c07aa000e70de", - "cross_join.16.9.parquet:md5,fe786778faa99c7cc354702b92f19033", - "cross_join.2.2.parquet:md5,df3ea0aa3cf819848607a8ccb7427c67", - "cross_join.2.3.parquet:md5,3daa415c7b075d9bcbf8c361177df2b1", - "cross_join.2.4.parquet:md5,21181c24113754dbfc51b843cc162972", - "cross_join.2.5.parquet:md5,ceac28afb17179722d068e4898c1fed1", - "cross_join.2.6.parquet:md5,e212e99c688fbf19ca107b67658509f3", - "cross_join.2.7.parquet:md5,e9402b2e03cb3fc6787b613d39107ebd", - "cross_join.2.8.parquet:md5,2d8c2123728e6a77ce3c45cc229ae62c", - "cross_join.2.9.parquet:md5,71e2adc772475ac1f5227f8158a1e136", - "cross_join.3.3.parquet:md5,a761c328b538ec3c70a2e40fe6e926dd", - "cross_join.3.4.parquet:md5,776a5456210292aa60fec6581735fd2a", - "cross_join.3.5.parquet:md5,a7ba3388e08fbee985aaa68430212d9c", - "cross_join.3.6.parquet:md5,194aa30dea76d601b911b1f82738d81d", - "cross_join.3.7.parquet:md5,841538e8a9c8193f91c0e80b3778756c", - "cross_join.3.8.parquet:md5,e5726ad8cf8a34bb94a8eef1591a40ad", - "cross_join.3.9.parquet:md5,60c3e3f6b883208aee0cec83da8df15e", - "cross_join.4.4.parquet:md5,3d3933ba1381542abdca3a11deaf6572", - "cross_join.4.5.parquet:md5,0d6a08085677351134b549ca8ee54c5d", - "cross_join.4.6.parquet:md5,75b8695575c06de3a7a44d8faea0fc0b", - "cross_join.4.7.parquet:md5,bcbb4dc32840c8cd2ffb0d67b6851ca4", - "cross_join.4.8.parquet:md5,b38158830bac771290032b8db250ad18", - "cross_join.4.9.parquet:md5,32f6237531d2f81703369aa7d9b00863", - "cross_join.5.5.parquet:md5,bf8fa13846f29b3115cafd5ef5a67b88", - "cross_join.5.6.parquet:md5,eafbbcf5a0e641bf233440575f1d09ae", - "cross_join.5.7.parquet:md5,a44b957ab66fe2f33e78a2cb70146a4d", - "cross_join.5.8.parquet:md5,3600c92f24984a478602b48ef141fd1e", - "cross_join.5.9.parquet:md5,3be2aca9a1704c2f0da8d6dfeeb3bfad", - "cross_join.6.6.parquet:md5,0812cd38935fa1f20007c93a3332b400", - "cross_join.6.7.parquet:md5,010598ba5857828a2c19e1bd80171e6a", - "cross_join.6.8.parquet:md5,7d1fee5a1c77d9fc025a137465d6424e", - "cross_join.6.9.parquet:md5,a0aee942ca33abb4e2712ab28bc13049", - "cross_join.7.7.parquet:md5,bdf1c91f450994574ccdbc0b042b54a2", - "cross_join.7.8.parquet:md5,a8f6e38628f4c314d3f57764c8aa9c6d", - "cross_join.7.9.parquet:md5,655eae2689bafdb33dc70a2485dc202f", - "cross_join.8.8.parquet:md5,3623327056828b8748dee9c0a4ada804", - "cross_join.8.9.parquet:md5,09875586b3f7d0f6fe3ad8315ff63137", - "cross_join.9.9.parquet:md5,cd69fdfe16c2ca43965aa300d11d3a04", - "ratios.0.0.parquet:md5,220002c2b910e2a1bbfd27d6901f2b07", - "ratios.0.1.parquet:md5,7d6e9941830ae35294a73a245e2bc276", - "ratios.0.10.parquet:md5,5d28d8106015158b0d84790e588e1c52", - "ratios.0.11.parquet:md5,fbfceb95c581dead9bc7d0205e06df15", - "ratios.0.12.parquet:md5,41ea7363cde2086d03a89937b2925dce", - "ratios.0.13.parquet:md5,fa2c54ade1ab7ed712564c7e28533424", - "ratios.0.14.parquet:md5,421190c4a34611779deaa4d780363c3e", - "ratios.0.15.parquet:md5,0e7f7a03912ff4c0f6e8e92315a2e1d3", - "ratios.0.16.parquet:md5,3b50f2d3a7a143cc6bf07bfe13da1e87", - "ratios.0.2.parquet:md5,959eac8928c83dcd555a5962ffe2e3d0", - "ratios.0.3.parquet:md5,7b92ee164047b8cba80b810697c823aa", - "ratios.0.4.parquet:md5,c2fab7d4a3a09ccefc431ac24ad47c51", - "ratios.0.5.parquet:md5,8e33a56657c13b9b899d9669c687693f", - "ratios.0.6.parquet:md5,d72c628f71943bf472034165fb8ac85b", - "ratios.0.7.parquet:md5,dd37a01abf262ab4470489bfe81e80d3", - "ratios.0.8.parquet:md5,136446bbcb069b84bada5094974e5bd1", - "ratios.0.9.parquet:md5,6537315f3cdd0c77b445dd75af2e6171", - "ratios.1.1.parquet:md5,0f1bab2fcfadc57de1a4093459dfb2b0", - "ratios.1.10.parquet:md5,44d0fccdc37ec0e7f3328afa53738180", - "ratios.1.11.parquet:md5,412f6f123f831a5eef440bbc3674b2a3", - "ratios.1.12.parquet:md5,7e6d5c6d73be69f073f3b7f94317a266", - "ratios.1.13.parquet:md5,341afacfd02280937d9558f959992b69", - "ratios.1.14.parquet:md5,36821ec11e7f6dc9db71388207cb4c26", - "ratios.1.15.parquet:md5,93993f14c0fa871bd7016dd6ab2d94a1", - "ratios.1.16.parquet:md5,63e526f1ac1fb827fba959b8d18bcd6c", - "ratios.1.2.parquet:md5,0b09ff5298706e28e029f1bfcba2d278", - "ratios.1.3.parquet:md5,deb38687be8cbff0111560effbb88aec", - "ratios.1.4.parquet:md5,b00b555adc16309de09ebeef2c33dcbd", - "ratios.1.5.parquet:md5,0c50aeee8d2423c3f3caa1ede8168364", - "ratios.1.6.parquet:md5,1190bb49f0dd29cf7968489958ddcfd9", - "ratios.1.7.parquet:md5,0310c3b03c3489eafd37c68636c24bc6", - "ratios.1.8.parquet:md5,b05ecf0766987dc3b719bfb3e7903258", - "ratios.1.9.parquet:md5,5d9092765bfdb27d599aef846169e95e", - "ratios.10.10.parquet:md5,435b6f03fa2412d5ef96d84d8cbe449f", - "ratios.10.11.parquet:md5,1c801006936aa17b8d9a33dd53a43d19", - "ratios.10.12.parquet:md5,bca3e701e2798e25d9fde9645795f8f9", - "ratios.10.13.parquet:md5,0afa6c1d91ffbd81aab728e49d749be7", - "ratios.10.14.parquet:md5,ef906b77f3132260fe58aba64c5b39e5", - "ratios.10.15.parquet:md5,117ade2741f27be4626730f5dac6c17c", - "ratios.10.16.parquet:md5,fc97454acacee4ba3e7c6ed6448e5221", - "ratios.10.2.parquet:md5,720a743443963662f7fd25a4aba04f1e", - "ratios.10.3.parquet:md5,72ac29d074a108bf033030e3b91698c5", - "ratios.10.4.parquet:md5,793cb68ccb8da8223bc65a3c00a09d57", - "ratios.10.5.parquet:md5,386a83d8ff62e8864de392c4b5a52219", - "ratios.10.6.parquet:md5,d7ba1b6282aa01c7dd80d2d3d25a3445", - "ratios.10.7.parquet:md5,e2df0fa0def20577fe2f43807f3fb770", - "ratios.10.8.parquet:md5,00f09a204d1f59d8685cd56969867aa3", - "ratios.10.9.parquet:md5,f8e47e01e7d5dd35c7f79527ed768ef4", - "ratios.11.11.parquet:md5,b6a95d6ec068c9de33620e3c9e9529c2", - "ratios.11.12.parquet:md5,dd2f0403b3dc8fa3289e0470e15aec7f", - "ratios.11.13.parquet:md5,6392b7fa3ed7584ef307969f803b5cf6", - "ratios.11.14.parquet:md5,4065ae6790a7366eb4b48fff78950d51", - "ratios.11.15.parquet:md5,420d22f4266ee78610e0ccce63e4bdae", - "ratios.11.16.parquet:md5,8e66d7a5bbdd09268b70d529a51d010e", - "ratios.11.2.parquet:md5,91fc7c3f09143d9ea4b4aebdfec6453e", - "ratios.11.3.parquet:md5,7bed3428feb09c8e9e52c63ceb5daf72", - "ratios.11.4.parquet:md5,17fdf2f3ad1ae3d7e47b0c3fe42f048e", - "ratios.11.5.parquet:md5,41e56b087a145821b2a448bf55d13d1f", - "ratios.11.6.parquet:md5,d2a6e5136b5f135c740d3ee7648e97b4", - "ratios.11.7.parquet:md5,17dac5c25f13feeb3ca7bae7d4c400b0", - "ratios.11.8.parquet:md5,884c7b752f3943e24a055844fcbecd7a", - "ratios.11.9.parquet:md5,46aaf75a52c5adcddc191089e96491d6", - "ratios.12.12.parquet:md5,a921ce7f95844f17cbe99e2e2ec7316b", - "ratios.12.13.parquet:md5,fb259ea44f54be47e2aacf48572cc5f3", - "ratios.12.14.parquet:md5,bd3a88ec5d7bdb78d60a7b00592cf792", - "ratios.12.15.parquet:md5,4099107f3c25536f757a0bd2fe90d538", - "ratios.12.16.parquet:md5,d5dd05540ce1e5d9573cc8b18263c746", - "ratios.12.2.parquet:md5,4492d2f7a2ef11e9c6056afd0abf48b1", - "ratios.12.3.parquet:md5,138b87b6d3516dabc84dbafb6daeb051", - "ratios.12.4.parquet:md5,bdad8a7d9ce9bb25f595cd48468c5de4", - "ratios.12.5.parquet:md5,bff415bfcd7059d4fd75d80c543bc58c", - "ratios.12.6.parquet:md5,d8e5e18378d15e618fa911947ed32f4a", - "ratios.12.7.parquet:md5,aab82b04260431ee7960f29e179885bf", - "ratios.12.8.parquet:md5,1b29065f4dbef0791b5144141fe9653f", - "ratios.12.9.parquet:md5,75cdadef000dbf67f5322b3cb25447a8", - "ratios.13.13.parquet:md5,c68ed669c929728923c51ca2505afcf7", - "ratios.13.14.parquet:md5,582bb0e021549550727571666b32d978", - "ratios.13.15.parquet:md5,d6fecea5365ca98434f49d1a8c4dbc25", - "ratios.13.16.parquet:md5,95e21149fdaba3797b6f3f8ae8922180", - "ratios.13.2.parquet:md5,f61525b5db5f3132c016e0de54346e4a", - "ratios.13.3.parquet:md5,c27dacf0cac90081b39010affdbbdbf6", - "ratios.13.4.parquet:md5,943192e24a996148995dfcc469d71db1", - "ratios.13.5.parquet:md5,80769a297c020486f87c62aaf4b5c923", - "ratios.13.6.parquet:md5,d6678f8a7bb0ab4fdd554dc7cae41fa7", - "ratios.13.7.parquet:md5,ec3b437168b503d908280d701402a11b", - "ratios.13.8.parquet:md5,17ce86e4fb7d0102b26f8647d8912a46", - "ratios.13.9.parquet:md5,5bd33e9d503aac73f55ac5684cd5514a", - "ratios.14.14.parquet:md5,e5839c888e5db0793201f749dbd4567e", - "ratios.14.15.parquet:md5,5563ec42c25d7cb1268f0696057db869", - "ratios.14.16.parquet:md5,080e05db225aa0712a844e04fd0b24c5", - "ratios.14.2.parquet:md5,5225d0172940d985eb54565bb64fda3e", - "ratios.14.3.parquet:md5,a1e1ff33c5ef5e5c5362d3c1b76c6750", - "ratios.14.4.parquet:md5,8f521553fc0009e61834923effa9861d", - "ratios.14.5.parquet:md5,8d47c275b5435ea8a9a490e6ca3c510d", - "ratios.14.6.parquet:md5,b2f794d5de5e1c6e1c34348f1c2bf14e", - "ratios.14.7.parquet:md5,b1a4c1ddcdf76158d6f06607a2351e11", - "ratios.14.8.parquet:md5,5c1a1043c942f49808963b26ed14df90", - "ratios.14.9.parquet:md5,1499fa059120ea2ad7e4a2b7d227d28e", - "ratios.15.15.parquet:md5,5f872b3ad52bd389456c1118c4c60210", - "ratios.15.16.parquet:md5,10adc5043cb8da92f0f1df9870a186a6", - "ratios.15.2.parquet:md5,89792eb98c8d527c176f8deac38db8b2", - "ratios.15.3.parquet:md5,2dc4c53738676ee0e32013b3308452de", - "ratios.15.4.parquet:md5,5a251ff4f36bf315b5e7d7b5835e95a0", - "ratios.15.5.parquet:md5,f642340c8bfb8f31ea64854328bfc3be", - "ratios.15.6.parquet:md5,5d88c14c5424894fad720ff0aa0e780e", - "ratios.15.7.parquet:md5,90790e2c77a36c5dab9bce559213330a", - "ratios.15.8.parquet:md5,af607e2f46d894846a2b3803b2e342c2", - "ratios.15.9.parquet:md5,47d44e0122c8b141a16de1bbd57e1e2d", - "ratios.16.16.parquet:md5,41cf478d0acd5cfffd227fb24acfeb65", - "ratios.16.2.parquet:md5,208701103b2c606b5f2ae8fa7100391e", - "ratios.16.3.parquet:md5,de0debf5b48bee749e60a879d9942bb8", - "ratios.16.4.parquet:md5,a8228b4ed2d09cfc339e42c93b5a3d6b", - "ratios.16.5.parquet:md5,bfe9b53598ed35d041b93e5e02738db6", - "ratios.16.6.parquet:md5,aa2d94c922a0f64da46478f20b01cd0f", - "ratios.16.7.parquet:md5,4c656d6c48e6e4ff306aa7ae5ab9fd74", - "ratios.16.8.parquet:md5,e3f3e1bd32dea4efe681ff75283ac988", - "ratios.16.9.parquet:md5,0c91569c0543ca673373dca208f4abd3", - "ratios.2.2.parquet:md5,0f5c8f168439c1e9ef83bcc091fbc72c", - "ratios.2.3.parquet:md5,90f94b44026cd8f791c6900c157e6212", - "ratios.2.4.parquet:md5,5405327ad456798b51fd31d112e7e992", - "ratios.2.5.parquet:md5,9b0dfb17e7f7e8e139f56e68f7962251", - "ratios.2.6.parquet:md5,239db4d6d62b0a08bfdb024e089f3d53", - "ratios.2.7.parquet:md5,b06eba9859f7bfe1be6837cb87f8c90b", - "ratios.2.8.parquet:md5,38ae83f002edd55808da7f39ab92c593", - "ratios.2.9.parquet:md5,3fbfa06c07e71cb53a3566304afdb2bd", - "ratios.3.3.parquet:md5,720039da8d341a9768e655928fe3189c", - "ratios.3.4.parquet:md5,419802d7824fb9ddfab2d94dee3bf92f", - "ratios.3.5.parquet:md5,77d3923df5480e520893ca27a8ff3ad7", - "ratios.3.6.parquet:md5,53e4d57347aa53e07fe9a41a01bed449", - "ratios.3.7.parquet:md5,d829d61e036d464007f61d40dda227f1", - "ratios.3.8.parquet:md5,5334546a37340a3efd0b0fa9fe19b8e7", - "ratios.3.9.parquet:md5,c00596fdb9f0085e594aeef45792f591", - "ratios.4.4.parquet:md5,aa5e68b9a2ef1c4050cbbe3521eee3a7", - "ratios.4.5.parquet:md5,2ede9c9e5e9c0e142be3fd6eecfe64b7", - "ratios.4.6.parquet:md5,6024ac3d304241dc24b35c37af5ed29f", - "ratios.4.7.parquet:md5,abe99d233a04a89bfac879a6e2f398ad", - "ratios.4.8.parquet:md5,9a7039d6fab1705ac0dcfffa2476c59d", - "ratios.4.9.parquet:md5,e354d98d486ac273311ea24e20d9ca96", - "ratios.5.5.parquet:md5,5e7d71a159dd6ffe953571edd15b7d8f", - "ratios.5.6.parquet:md5,d00ffa5c48f00ad4a16d88d593df9875", - "ratios.5.7.parquet:md5,f90a1092938da2f7aa89cee3813b5b34", - "ratios.5.8.parquet:md5,07a5eac048104435f7d6bbcd10207011", - "ratios.5.9.parquet:md5,94aa5135e8d4fc446c854fcaeb8ba6cf", - "ratios.6.6.parquet:md5,21d8521e3add621c7e9a977a885b1b1d", - "ratios.6.7.parquet:md5,7c83c2389e87b342206d24f3e1c699ce", - "ratios.6.8.parquet:md5,8c5c9baec5edf4a97bd8ae9e25975e39", - "ratios.6.9.parquet:md5,8f656f0a02aeef2e7d244488dc29172d", - "ratios.7.7.parquet:md5,0ce6d6b93fc28158288e2c423d2d29c8", - "ratios.7.8.parquet:md5,aef4fe810ebda02acf220e3fefc85609", - "ratios.7.9.parquet:md5,95f47e05bb8230673a6ce367fe02dd1d", - "ratios.8.8.parquet:md5,4ff6c0dffff0fbc67c8a83e2d784829a", - "ratios.8.9.parquet:md5,dbfa58a7deb74eed23d053ebea055a21", - "ratios.9.9.parquet:md5,cb92fb150f0fe6faba89d350f9db153c", - "std.0.0.parquet:md5,736cd7889f2109702a662bbc31ca1552", - "std.0.1.parquet:md5,904f861bcb6ab69d749d900e326042fe", - "std.0.10.parquet:md5,79a953c1e99e5625f13f555556fdb7d5", - "std.0.11.parquet:md5,00e6a7f9ea8c5b4716c4d30975e1a20a", - "std.0.12.parquet:md5,0a724c75ca21ce9309c5d09aea479339", - "std.0.13.parquet:md5,6dd197b43e220208d0bdb7255edcb2d5", - "std.0.14.parquet:md5,5cca1c492bfcf29530795995ab7d52ce", - "std.0.15.parquet:md5,bfe6a92194461df82d9033e25b2201fa", - "std.0.16.parquet:md5,4c75724a7b33ae81f7a640e0b482ddde", - "std.0.2.parquet:md5,bb44b989d1a8cfc7bf636afdc51cf6ee", - "std.0.3.parquet:md5,83e34bac4234048dabd0c493477934a3", - "std.0.4.parquet:md5,8bc3917fcf8eff2e9eff947ed09f871b", - "std.0.5.parquet:md5,a69b0cdf99a00055cb3684d46ae0dd6b", - "std.0.6.parquet:md5,0eccc49a0788377d2647bfd053f490d8", - "std.0.7.parquet:md5,818d5a7447e67b8f621192ba1a03b233", - "std.0.8.parquet:md5,6d674a93a9ac8809eeecfb06743a1cf7", - "std.0.9.parquet:md5,8b87f15f5960e8cea7f430221621c243", - "std.1.1.parquet:md5,15a2e6249470c2018cc7d9e117ee416a", - "std.1.10.parquet:md5,6469567ff011a1c46a7d563bac4ff651", - "std.1.11.parquet:md5,8ac4e93ea31ba0e49365927b8b6e667d", - "std.1.12.parquet:md5,013c34feda6c2fa6dcfce1cfddceb2b2", - "std.1.13.parquet:md5,7ebf0bb4c3ecefbd2b15da95e9e1b839", - "std.1.14.parquet:md5,0114ece70ea3c1c8af0243508809f3ba", - "std.1.15.parquet:md5,be02bab6bf392cbb0a59d77e8719323d", - "std.1.16.parquet:md5,cd0e81c811cc855011b7e149c53bf767", - "std.1.2.parquet:md5,26acad8a4ff0cb30a25a378bb8a4cca3", - "std.1.3.parquet:md5,14f0776ccf50f93ac643a29f954a47a9", - "std.1.4.parquet:md5,63744d753916e7476373204a710f313d", - "std.1.5.parquet:md5,fed84ebf913e02583946c5b1e2fcc989", - "std.1.6.parquet:md5,7d102f9167b8f679fe837e0f61d998fc", - "std.1.7.parquet:md5,b5c3a3819a9a3dfc4a40f28ac24949d5", - "std.1.8.parquet:md5,5fad743355a906ea31abeede723146ee", - "std.1.9.parquet:md5,b8dda1efd653838ffe26c1baeb095a55", - "std.10.10.parquet:md5,2a176e0e0ed3549c5784dbd336d94ad6", - "std.10.11.parquet:md5,7192c70628929be55ce9f68ecd7af685", - "std.10.12.parquet:md5,b324bb0afbbb5dfea9d265bac16e8a1c", - "std.10.13.parquet:md5,40fed5958d7c5b2d634cb25bd7a69fca", - "std.10.14.parquet:md5,30dd97cb9d196faec1106f2788faac07", - "std.10.15.parquet:md5,42fc1b3ad43c419b6b0456eb8698f5d4", - "std.10.16.parquet:md5,9999f3a341c390be1c8b3b56bc1c8c09", - "std.10.2.parquet:md5,42b929f240c817a11e17c3cab4863a1e", - "std.10.3.parquet:md5,dbcf2be276e146e1e673a5d09bc83b5f", - "std.10.4.parquet:md5,3184c5c7434695a03186add5a6c039bd", - "std.10.5.parquet:md5,ecd24ad57cef6d642a1468f0af9e071e", - "std.10.6.parquet:md5,f09fd2ef904031028d7531c45ac7e935", - "std.10.7.parquet:md5,3c736d27101704573d4e108e0e4d8eb0", - "std.10.8.parquet:md5,72a02e3511e6d484b453f65a8bb95669", - "std.10.9.parquet:md5,fca78905f02fd7516c18b49f74932506", - "std.11.11.parquet:md5,be509878b36d4dcc3f7a446234d7f441", - "std.11.12.parquet:md5,2cab47894c51701c7c216934a8ca0a82", - "std.11.13.parquet:md5,1aca591524b78b2341b12aa824ebec06", - "std.11.14.parquet:md5,568abe4d00bc1a1cde4803a8dfa1a3ee", - "std.11.15.parquet:md5,d1804629a97dd00408eadad9b5b9f39f", - "std.11.16.parquet:md5,f28c2cf6fd870c063e4064b4fdfe0896", - "std.11.2.parquet:md5,32283cd358bc0cdd3d169ccd7c6d5a3a", - "std.11.3.parquet:md5,1fb4316401ffc90b00d5cb65a2e42f85", - "std.11.4.parquet:md5,66a2822569ae19231baaed970adef659", - "std.11.5.parquet:md5,4f6bb46e6edb079cb700702ca4eb7244", - "std.11.6.parquet:md5,d55632df5840f8cb807556c20e6513e3", - "std.11.7.parquet:md5,a03b854b1c46eae6df4b5de377fff35a", - "std.11.8.parquet:md5,612b0bbb33cab4c95951e1e0786b8425", - "std.11.9.parquet:md5,e5da532bb1f9b83a13a2d278158c4c58", - "std.12.12.parquet:md5,6b2e83c599364ab90b09fb7ed8c49adc", - "std.12.13.parquet:md5,e84a7125b4e97afd5216ff51d28927e3", - "std.12.14.parquet:md5,f59fbdae886eab7a83b18977b46c3263", - "std.12.15.parquet:md5,4e34a435dd3f4ccb97a499875ae75673", - "std.12.16.parquet:md5,e77ecefe20da467cfa30b6b2d3ccb664", - "std.12.2.parquet:md5,b2677238821f04196dba57a8dd3edd7f", - "std.12.3.parquet:md5,da7750292c5b0b925582c61a469f97f6", - "std.12.4.parquet:md5,f96d4036b363c0ff4a360ec34d40fafe", - "std.12.5.parquet:md5,726cf7734c4fe126fbdbdbd802e7d6b3", - "std.12.6.parquet:md5,c4bb9c000c41dc756117510c000b033d", - "std.12.7.parquet:md5,f6766f451e936910b0ed790c284efc39", - "std.12.8.parquet:md5,b5b4aab10b50f8f30b6b19293609dfbd", - "std.12.9.parquet:md5,c14c306c8febd854c5df6fa6c512609b", - "std.13.13.parquet:md5,aab6af50c7f905593e9823918dd6f8d1", - "std.13.14.parquet:md5,3770fdefe7e6fdb2cebfaaebe3ce434d", - "std.13.15.parquet:md5,71544fbd09956f4bc6ed730f3aa7e6f7", - "std.13.16.parquet:md5,21a7dc2fbbb254fe508947126276bc2a", - "std.13.2.parquet:md5,85e8377f045d0dee9cebe9541f9292dd", - "std.13.3.parquet:md5,fea523af95e4fc2ba5ede4dbb94b8cb6", - "std.13.4.parquet:md5,20ef6ebc27ade3e0e67b6088aacb9c04", - "std.13.5.parquet:md5,e577a2296ffe440d2f26f3e921052a3a", - "std.13.6.parquet:md5,fd031a00f332c94ebac3af63ba0c5dc9", - "std.13.7.parquet:md5,971ecf49d71fc07c139376011701cfb6", - "std.13.8.parquet:md5,5bcd2cb2e384ecb16785d8fc41065276", - "std.13.9.parquet:md5,fa1f9b759de82cc154578f5cd573ee66", - "std.14.14.parquet:md5,4d07377b5de2946b9519e1a1364328d2", - "std.14.15.parquet:md5,2fe817a9f94b5003f79550380a2b4347", - "std.14.16.parquet:md5,94d2648b341fdc336729f0aff2961348", - "std.14.2.parquet:md5,b96819e975edfa3af325e31f71881bea", - "std.14.3.parquet:md5,cde85631d860482cbcbf0bef851b0249", - "std.14.4.parquet:md5,4c57733ef5f6aab5005a6d0e5f1a60d1", - "std.14.5.parquet:md5,2c2cc0ef88838fb10463038040980f6e", - "std.14.6.parquet:md5,ffb48c8d7fae98a1adcc6de6c4ba576d", - "std.14.7.parquet:md5,2b118866dffee680fb1dfe4fe460242c", - "std.14.8.parquet:md5,420ef1d70490bdfe6a242b1d76eb3a75", - "std.14.9.parquet:md5,aaaddc023fd995113fe61982e797394d", - "std.15.15.parquet:md5,34ed8c5327b4b9a8ae02f99ff286e90d", - "std.15.16.parquet:md5,8218a9c98e21e9433e084c3867042241", - "std.15.2.parquet:md5,e9faf451353e8f666b2a423ce7dd4830", - "std.15.3.parquet:md5,a905ad60b927030b032743c9745a6a1f", - "std.15.4.parquet:md5,ae8e1daf6c3d46f237b2c78b292be0ac", - "std.15.5.parquet:md5,2143050506402f5f164b38313b4e61fe", - "std.15.6.parquet:md5,42f6403c44802ab837845b4ae217165f", - "std.15.7.parquet:md5,1e645943320aa9958bfab4b0e60d40a7", - "std.15.8.parquet:md5,c7d3a14397e74f24ba33c993092baf2e", - "std.15.9.parquet:md5,288d95240ad9b06a5cdeb31a08d95e33", - "std.16.16.parquet:md5,e20b82e0bc49230e19f4f263b511e829", - "std.16.2.parquet:md5,06e2e37948fd33a17a4565ca41b9e3d2", - "std.16.3.parquet:md5,107bca3ca4b249ca6a2b657715937dd0", - "std.16.4.parquet:md5,e6dcd3786dfcac5be47b149fc402534e", - "std.16.5.parquet:md5,34b6f5b76f1173f6c3afc4c928e3c3d7", - "std.16.6.parquet:md5,f00a0ba560bc7476b4b54f24e0d0b21d", - "std.16.7.parquet:md5,d58ddd235437b0bad5023936159a3032", - "std.16.8.parquet:md5,2f36f36c1a7a0a9420a77f5c5b5a34dd", - "std.16.9.parquet:md5,d860d2db18a2222ebb3f3ec6d4e72add", - "std.2.2.parquet:md5,4eb96e909c566f60506d5854c61b1635", - "std.2.3.parquet:md5,4d2074dd804dd57ed5141f1a5160f1bb", - "std.2.4.parquet:md5,77e781d6a43266346826135cb3f4f1b4", - "std.2.5.parquet:md5,95bc26acbeff7f30f6bec5f09849a761", - "std.2.6.parquet:md5,c2d9555fdd10bcfc1baac7852b3661e9", - "std.2.7.parquet:md5,1ac80c8d9191ebbedea4c3c16f9e32d2", - "std.2.8.parquet:md5,1e1f17eb7bb27ea152da9a36e493e8d7", - "std.2.9.parquet:md5,bb01a126902f6a8ffc5d0d52b681c867", - "std.3.3.parquet:md5,271fb382a4a64d8456f460609e210d36", - "std.3.4.parquet:md5,b2573ad1157ec5a679aa06b89a9bbf3c", - "std.3.5.parquet:md5,96a5a5cdefe4b7b305e888d4796b6f30", - "std.3.6.parquet:md5,94bfe090920dabaa5da248ab5a3095c9", - "std.3.7.parquet:md5,5120129fd2f20dcbeaaad539c692ea29", - "std.3.8.parquet:md5,ede0228d6f099f47b991b7014200fcc0", - "std.3.9.parquet:md5,bb90011f2a95db22db42241683a3e42b", - "std.4.4.parquet:md5,112d931606db6373ef4c6e6df227d670", - "std.4.5.parquet:md5,eeb27286b9da50c22894727a433ae188", - "std.4.6.parquet:md5,15384f18c9b4916a19bd5312abefbf43", - "std.4.7.parquet:md5,145a7ce8ada865a155571bd381acebe5", - "std.4.8.parquet:md5,acd5be3ef728009f3656a0cc63c9a767", - "std.4.9.parquet:md5,c335f0a92fd7b68efa9c318a2193141f", - "std.5.5.parquet:md5,d9a94584142693161c5715e1dfa7cd9f", - "std.5.6.parquet:md5,bc7b87c09e85c6be73fd383c2c7e01cf", - "std.5.7.parquet:md5,c333ba6de8ddce2f990d7fe4753f6456", - "std.5.8.parquet:md5,c2f2bd44e756069fb995f1ae7e2b7c0d", - "std.5.9.parquet:md5,7c295e8b59bb974ca4a677737ba8f3db", - "std.6.6.parquet:md5,05d412ccd35db8ee504e9f1b08c20e7a", - "std.6.7.parquet:md5,9f5808979fc9e2cc5b229e26947cdbdb", - "std.6.8.parquet:md5,68ce12fec999bfc1488e19ab9de1efb3", - "std.6.9.parquet:md5,59cf00631e334a3c4e352068db61b2f7", - "std.7.7.parquet:md5,3d29e9dfbe7921601695e4cc57223453", - "std.7.8.parquet:md5,0d355587e14dd6f27dd99578775594db", - "std.7.9.parquet:md5,44149886f0b15c49f3360d4f26e3a3e7", - "std.8.8.parquet:md5,b896ffeeb90291286b45f2a833e995ad", - "std.8.9.parquet:md5,aa542692fb87abc4e47f6987cb59d575", - "std.9.9.parquet:md5,c816fa4ba44b88df3fd0403b73af00a5", - "accessions.txt:md5,d41d8cd98f00b204e9800998ecf8427e", - "filtered_datasets.metadata.tsv:md5,b35f5b77316f414de45b5eb7bfca79f4", - "rejected_datasets.metadata.tsv:md5,a343c6c235d2b7ff8a639ba1ffb6d154", - "selected_datasets.keywords.yaml:md5,58e0494c51d30eb3494f7c9198986bb9", - "species_datasets.metadata.tsv:md5,1650a58b235c7eb30ea9d0192bb62fd2", - "candidate_counts.parquet:md5,6ea3907553c8c43dbff127e52490e4b4", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.mapping.csv:md5,d95581c2b10beed3ba4a4556c1afc44f", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.metadata.csv:md5,faf9789296e5578fd31efee8d0c40ca9", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv:md5,f8889bf65500b67c37b7b7549df57285", - "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", - "all_counts.parquet:md5,03c31345d3be9fc2d2a47136f43f4e4c", - "all_counts.parquet:md5,03c31345d3be9fc2d2a47136f43f4e4c", - [ - "llms-full.txt:md5,75a85251c607aff48dddb080a8dc0f41", - "multiqc.log:md5,e4e7c34df68e70fcef6366f66aadb497", - "multiqc.parquet:md5,08e810aff7a2317f35d6bc6be502b584", - "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", - "multiqc_data.json:md5,3cd2f7d2c08fd31c872d09b9de100b06", - "multiqc_eatlas_filtered_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", - "multiqc_expression_distributions_top_stable_genes.txt:md5,bdcefa05d6ab91d7c6c811e34423c7a1", - "multiqc_gene_statistics.txt:md5,f8be92e12c60b2f6bba6a753fd5d60f7", - "multiqc_ranked_top_stable_genes_summary.txt:md5,bc36624d2fec89bccec45bdcafcd3eb1", - "multiqc_software_versions.txt:md5,6de2a64621c56546a7c801c5de8db51b", - "multiqc_sources.txt:md5,d2a044df39ce3c6abe5cdc2d67473490" - ], - [ - [ - "eatlas_filtered_experiments_metadata.pdf:md5,5bab6bafd08339e808f0958d230fc018", - "expression_distributions_top_stable_genes.pdf:md5,ddf5aeda8a25f95acfff0546f76ec113", - "gene_statistics.pdf:md5,989bd7865499f6c97677ac55fec42fd4", - "ranked_top_stable_genes_summary.pdf:md5,d4af0518c2f3654b9bc016c19ef15872" - ], - [ - "eatlas_filtered_experiments_metadata.png:md5,136a0f4c244441de39eba29b3cf77da8", - "expression_distributions_top_stable_genes.png:md5,6d1f987f1b09f30670dfa84c291257bc", - "gene_statistics.png:md5,0667bba4944117bdcf6e22c394b0915a", - "ranked_top_stable_genes_summary.png:md5,a2bde170fdca877d5553838fb78b3cda" - ], - [ - "eatlas_filtered_experiments_metadata.svg:md5,694b09ec17759d1cbf5ee10829f395d7", - "expression_distributions_top_stable_genes.svg:md5,6a63a56106f22c4030e4a07598fbdb9b", - "gene_statistics.svg:md5,e90c809dc41376b254bc77af425acab6", - "ranked_top_stable_genes_summary.svg:md5,f5370c880c4591bd675edd55ea82c20e" - ] - ], + "all_counts_filtered.parquet:md5,9dd07574791da408a3c00064774fd2a3", + "all_genes_summary.csv:md5,720e3ec50de0227cbfbce9cb0fd542f8", + "top_stable_genes_summary.csv:md5,0f86d98eb45898a80923232774e8ee81", + "top_stable_genes_transposed_counts_filtered.csv:md5,de8bf611ffbb33b1f600a13be0904fad", + "stats_all_genes.csv:md5,0561def8497c2c5d2291653105a4296e", + "microarray.stats_all_genes.csv:md5,449a0e1a08a6e1ec4ced368a8ab1538f", + "rnaseq.stats_all_genes.csv:md5,c8eb12ea25bc4368cf55753ed40b4271", + "cleaned_counts_filtered.parquet:md5,d1bb960ef0ca8d34430ef34a52b3ca96", + "stats_with_scores.csv:md5,bfc234a1d8c8cc0d53b102c5675e9988", + "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", + "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", + "all_counts.parquet:md5,c3e514cb129fffb55c0152b9116b81c5", + "all_genes_summary.csv:md5,720e3ec50de0227cbfbce9cb0fd542f8", + "whole_design.csv:md5,91d69f908aab581087541383a9cc1025", + "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", + "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", + "common.cpython-313.pyc:md5,0f0ff495fcb7243cc1188e601c374450", + "genes.cpython-313.pyc:md5,a8f7cb8770d654e7841291b9ef099e9c", + "samples.cpython-313.pyc:md5,0a8def370e4fe1e790ad572a84fc14c5", + "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", + "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", + "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", + "graphs.cpython-313.pyc:md5,6346dada33930b855e36e19eb96c6d29", + "right_sidebar.cpython-313.pyc:md5,1b209dbfa96e1f627952991f4301bf79", + "stores.cpython-313.pyc:md5,6cade39e1ae1f346befb8220f24c08b2", + "tables.cpython-313.pyc:md5,0fe34dad12b08318b334021f939f68d1", + "tooltips.cpython-313.pyc:md5,38d97cad354496df1cbbd32c11decc35", + "top.cpython-313.pyc:md5,3e31c5e39d2acbd49847378a23e33b1b", + "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", + "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", + "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", + "genes.cpython-313.pyc:md5,3119d2a9456b328722d503da5b07653a", + "samples.cpython-313.pyc:md5,7a2e47b0fac7db2b763035aa9d00f830", + "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", + "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", + "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", + "tables.py:md5,84d17ad7f43458412e550f74a24560e5", + "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", + "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", + "config.cpython-313.pyc:md5,62544cb677a32b64691d14ec763c9451", + "data_management.cpython-313.pyc:md5,240a7e00e5c6186e54225cafed7354d7", + "style.cpython-313.pyc:md5,1ac3d137e3a4775de24f91352a6a0b0f", + "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", + "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", + "style.py:md5,b9f1207f06464e43c05e6e3912f12731", + "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", + "E_GEOD_21945_A_AFFY_2.dataset_stats.csv:md5,c9fd75d794c11b7f97ee111b743e040d", + "E_GEOD_52806_rnaseq.dataset_stats.csv:md5,a28d66897810174044e9ca30f8e4675e", + "E_GEOD_21945_A_AFFY_2.design.csv:md5,3c2cc68528e555a885176d845f04d422", + "E_GEOD_21945_A_AFFY_2.microarray.normalised.counts.csv:md5,647f1f8b46457e201af3903be3326654", + "E_GEOD_52806_rnaseq.design.csv:md5,a0f1f0f86a2655f526d0cc099d5b6adc", + "E_GEOD_52806_rnaseq.rnaseq.raw.counts.csv:md5,da027fc750b0f00dca199122a67feac7", + "excluded_geo_accessions.txt:md5,3d247b02a5dfe0064be4c09ceb270c27", + "candidate_counts.parquet:md5,4ef144ae7c7c09e6d47117405eb47b37", + "E_GEOD_21945_A_AFFY_2.microarray.normalised.counts.mapping.csv:md5,e00b6804a4ba7ff25c708a9c6a4770f8", + "E_GEOD_21945_A_AFFY_2.microarray.normalised.counts.metadata.csv:md5,7efa12130b8d5bcdb79d630d07fec5d3", + "E_GEOD_21945_A_AFFY_2.microarray.normalised.counts.renamed.csv:md5,91576c77a0cff0116ef8aad73281b729", + "E_GEOD_52806_rnaseq.rnaseq.raw.counts.mapping.csv:md5,1bbc8cddf18072309e81498806aa73ff", + "E_GEOD_52806_rnaseq.rnaseq.raw.counts.metadata.csv:md5,08861c29159a6a2fed38efe523ed9c56", + "E_GEOD_52806_rnaseq.rnaseq.raw.counts.renamed.csv:md5,b8774e7b2c57ac8763dfd244c2c9b0f9", + "whole_gene_id_mapping.csv:md5,b7d2972efa44bdf1903702e260189b68", + "whole_gene_metadata.csv:md5,a08ab44a98542a8529d2a4b5a47e4408", + "all_counts.parquet:md5,c3e514cb129fffb55c0152b9116b81c5", + "all_counts.parquet:md5,f23c8b1ca3930141ea7016d516703e2c", + "all_counts.parquet:md5,f7a2c9c30f41a91fc4b71cb521cdb2c8", + "whole_design.csv:md5,91d69f908aab581087541383a9cc1025", + "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", + "multiqc_expression_distributions_top_stable_genes.txt:md5,6da36ac9ecb493270d9e7b4b04281cd4", + "multiqc_gene_statistics.txt:md5,6e3b76f372c326055c6f302d8fc6594d", + "multiqc_ranked_top_stable_genes_summary.txt:md5,4a1ea438cfcc3f6a8f2ffcbabf87dec2", "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,285ad46e7f5da70041815ecbcd9b5601", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,7ecbe8f933f447400466f65bd111a910", - "m_measures.csv:md5,c5da0cae45333dfa9eb99847ff2de7d3", - "stability_values.csv:md5,e516f503b73715bc962a4d7eca802616" + "E_GEOD_52806_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,b3b7bd2d373cd9f2e84c015c4d3c9d67", + "E_GEOD_21945_A_AFFY_2.microarray.normalised.counts.renamed.quant_norm.parquet:md5,2a27fa11e3e5143313f40d16b3815adf", + "E_GEOD_52806_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,bca82a0a962312e90a02096438b5eed7", + "stability_values.normfinder.csv:md5,9d6fb3c72f8dcedfc1c4295f9b2b7a6f" ] ], "meta": { "nf-test": "0.9.2", - "nextflow": "25.04.6" + "nextflow": "25.04.8" }, - "timestamp": "2025-10-21T08:28:15.486728677" + "timestamp": "2025-11-08T11:10:17.827869101" } -} +} \ No newline at end of file diff --git a/tests/modules/local/expressionatlas/getaccessions/main.nf.test.snap b/tests/modules/local/expressionatlas/getaccessions/main.nf.test.snap index 542353c5..51068350 100644 --- a/tests/modules/local/expressionatlas/getaccessions/main.nf.test.snap +++ b/tests/modules/local/expressionatlas/getaccessions/main.nf.test.snap @@ -6,46 +6,40 @@ "accessions.txt:md5,d41d8cd98f00b204e9800998ecf8427e" ], "1": [ - "all_experiments.metadata.tsv:md5,f75144aeb9027bbe613b34e0a0068ce7" + ], "2": [ "species_experiments.metadata.tsv:md5,68b329da9893e34099c7d8ad5cb9c940" ], "3": [ - - ], - "4": [ - "filtered_experiments.keywords.yaml:md5,58e0494c51d30eb3494f7c9198986bb9" - ], - "5": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "python", "3.13.5" ] ], - "6": [ + "4": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "requests", "2.32.4" ] ], - "7": [ + "5": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "nltk", "3.9.1" ] ], - "8": [ + "6": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "pyyaml", "6.0.2" ] ], - "9": [ + "7": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "pandas", @@ -54,9 +48,6 @@ ], "accessions": [ "accessions.txt:md5,d41d8cd98f00b204e9800998ecf8427e" - ], - "all_eatlas_experiment_metadata": [ - "all_experiments.metadata.tsv:md5,f75144aeb9027bbe613b34e0a0068ce7" ] } ], @@ -64,7 +55,7 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-10-16T08:22:09.660442662" + "timestamp": "2025-11-08T12:14:53.220718399" }, "Solanum tuberosum two keywords": { "content": [ @@ -73,46 +64,40 @@ "accessions.txt:md5,d41d8cd98f00b204e9800998ecf8427e" ], "1": [ - "all_experiments.metadata.tsv:md5,f75144aeb9027bbe613b34e0a0068ce7" + ], "2": [ "species_experiments.metadata.tsv:md5,68b329da9893e34099c7d8ad5cb9c940" ], "3": [ - - ], - "4": [ - "filtered_experiments.keywords.yaml:md5,58e0494c51d30eb3494f7c9198986bb9" - ], - "5": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "python", "3.13.5" ] ], - "6": [ + "4": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "requests", "2.32.4" ] ], - "7": [ + "5": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "nltk", "3.9.1" ] ], - "8": [ + "6": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "pyyaml", "6.0.2" ] ], - "9": [ + "7": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "pandas", @@ -121,9 +106,6 @@ ], "accessions": [ "accessions.txt:md5,d41d8cd98f00b204e9800998ecf8427e" - ], - "all_eatlas_experiment_metadata": [ - "all_experiments.metadata.tsv:md5,f75144aeb9027bbe613b34e0a0068ce7" ] } ], @@ -131,6 +113,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-10-16T08:22:00.469536946" + "timestamp": "2025-11-08T12:14:45.907461526" } } \ No newline at end of file diff --git a/tests/modules/local/expressionatlas/getdata/main.nf.test b/tests/modules/local/expressionatlas/getdata/main.nf.test index f3156920..878669a1 100644 --- a/tests/modules/local/expressionatlas/getdata/main.nf.test +++ b/tests/modules/local/expressionatlas/getdata/main.nf.test @@ -3,7 +3,7 @@ nextflow_process { name "Test Process EXPRESSIONATLAS_GETDATA" script "modules/local/expressionatlas/getdata/main.nf" process "EXPRESSIONATLAS_GETDATA" - tag "getdata" + tag "eatlas_getdata" tag "module" test("Transcriptome Analysis of the potato (rnaseq)") { @@ -20,8 +20,10 @@ nextflow_process { } then { - assert process.success - assert snapshot(process.out).match() + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) } } @@ -40,8 +42,10 @@ nextflow_process { } then { - assert process.success - assert snapshot(process.out).match() + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) } } @@ -60,8 +64,10 @@ nextflow_process { } then { - assert process.success - assert snapshot(process.out).match() + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) } } @@ -79,12 +85,13 @@ nextflow_process { } } - // check for the absence of expected output (the error is ignored but no output is produced) + // must be successful without any output then { - assert process.success - assert process.trace.succeeded().size() == 0 - assert process.trace.failed().size() == 1 - assert process.out.design.size() == 0 + assertAll( + { assert process.success }, + { assert process.out.counts.size() == 0 }, + { assert process.out.design.size() == 0 } + ) } } @@ -104,10 +111,11 @@ nextflow_process { // check for the absence of expected output (the error is ignored but no output is produced) then { - assert process.success - assert process.trace.succeeded().size() == 0 - assert process.trace.failed().size() == 1 - assert process.out.design.size() == 0 + assertAll( + { assert process.success }, + { assert process.out.counts.size() == 0 }, + { assert process.out.design.size() == 0 } + ) } } @@ -127,10 +135,11 @@ nextflow_process { // check for the absence of expected output (the error is ignored but no output is produced) then { - assert process.success - assert process.trace.succeeded().size() == 0 - assert process.trace.failed().size() == 1 - assert process.out.design.size() == 0 + assertAll( + { assert process.success }, + { assert process.out.counts.size() == 0 }, + { assert process.out.design.size() == 0 } + ) } } @@ -150,10 +159,11 @@ nextflow_process { // check for the absence of expected output (the error is ignored but no output is produced) then { - assert process.success - assert process.trace.succeeded().size() == 0 - assert process.trace.failed().size() == 1 - assert process.out.design.size() == 0 + assertAll( + { assert process.success }, + { assert process.out.counts.size() == 0 }, + { assert process.out.design.size() == 0 } + ) } } @@ -173,10 +183,11 @@ nextflow_process { // check for the absence of expected output (the error is ignored but no output is produced) then { - assert process.success - assert process.trace.succeeded().size() == 0 - assert process.trace.failed().size() == 1 - assert process.out.design.size() == 0 + assertAll( + { assert process.success }, + { assert process.out.counts.size() == 0 }, + { assert process.out.design.size() == 0 } + ) } } diff --git a/tests/modules/local/expressionatlas/getdata/main.nf.test.snap b/tests/modules/local/expressionatlas/getdata/main.nf.test.snap index 15ebc35b..f242cbb9 100644 --- a/tests/modules/local/expressionatlas/getdata/main.nf.test.snap +++ b/tests/modules/local/expressionatlas/getdata/main.nf.test.snap @@ -3,19 +3,35 @@ "content": [ { "0": [ - "E_MTAB_552_rnaseq.design.csv:md5,b81490696f638e90c1cf14236bb0c08c" + [ + { + "accession": "E-MTAB-552" + }, + "E_MTAB_552_rnaseq.rnaseq.raw.counts.csv:md5,830f50b60b17b62f9ca2f6a163a2879f" + ] ], "1": [ - "E_MTAB_552_rnaseq.rnaseq.raw.counts.csv:md5,830f50b60b17b62f9ca2f6a163a2879f" + [ + { + "accession": "E-MTAB-552" + }, + "E_MTAB_552_rnaseq.design.csv:md5,b81490696f638e90c1cf14236bb0c08c" + ] ], "2": [ + + ], + "3": [ + + ], + "4": [ [ "EXPRESSIONATLAS_GETDATA", "R", "4.3.3 (2024-02-29)" ] ], - "3": [ + "5": [ [ "EXPRESSIONATLAS_GETDATA", "ExpressionAtlas", @@ -23,36 +39,62 @@ ] ], "counts": [ - "E_MTAB_552_rnaseq.rnaseq.raw.counts.csv:md5,830f50b60b17b62f9ca2f6a163a2879f" + [ + { + "accession": "E-MTAB-552" + }, + "E_MTAB_552_rnaseq.rnaseq.raw.counts.csv:md5,830f50b60b17b62f9ca2f6a163a2879f" + ] ], "design": [ - "E_MTAB_552_rnaseq.design.csv:md5,b81490696f638e90c1cf14236bb0c08c" + [ + { + "accession": "E-MTAB-552" + }, + "E_MTAB_552_rnaseq.design.csv:md5,b81490696f638e90c1cf14236bb0c08c" + ] ] } ], "meta": { "nf-test": "0.9.2", - "nextflow": "25.04.4" + "nextflow": "25.04.8" }, - "timestamp": "2025-07-18T12:57:46.833178737" + "timestamp": "2025-11-08T12:15:01.955520856" }, "Arabidopsis Geo dataset": { "content": [ { "0": [ - "E_GEOD_62537_A_AFFY_2.design.csv:md5,7d7dd72be4f5b326dd25a36db01ebf88" + [ + { + "accession": "E-GEOD-62537" + }, + "E_GEOD_62537_A_AFFY_2.microarray.normalised.counts.csv:md5,673c55171d0ccfc1d036bf43c49ae320" + ] ], "1": [ - "E_GEOD_62537_A_AFFY_2.microarray.normalised.counts.csv:md5,673c55171d0ccfc1d036bf43c49ae320" + [ + { + "accession": "E-GEOD-62537" + }, + "E_GEOD_62537_A_AFFY_2.design.csv:md5,7d7dd72be4f5b326dd25a36db01ebf88" + ] ], "2": [ + + ], + "3": [ + + ], + "4": [ [ "EXPRESSIONATLAS_GETDATA", "R", "4.3.3 (2024-02-29)" ] ], - "3": [ + "5": [ [ "EXPRESSIONATLAS_GETDATA", "ExpressionAtlas", @@ -60,36 +102,62 @@ ] ], "counts": [ - "E_GEOD_62537_A_AFFY_2.microarray.normalised.counts.csv:md5,673c55171d0ccfc1d036bf43c49ae320" + [ + { + "accession": "E-GEOD-62537" + }, + "E_GEOD_62537_A_AFFY_2.microarray.normalised.counts.csv:md5,673c55171d0ccfc1d036bf43c49ae320" + ] ], "design": [ - "E_GEOD_62537_A_AFFY_2.design.csv:md5,7d7dd72be4f5b326dd25a36db01ebf88" + [ + { + "accession": "E-GEOD-62537" + }, + "E_GEOD_62537_A_AFFY_2.design.csv:md5,7d7dd72be4f5b326dd25a36db01ebf88" + ] ] } ], "meta": { "nf-test": "0.9.2", - "nextflow": "25.04.4" + "nextflow": "25.04.8" }, - "timestamp": "2025-07-18T12:58:41.56141538" + "timestamp": "2025-11-08T12:15:21.026054246" }, "Transcription profiling by array of Arabidopsis mutant for fis2 (microarray)": { "content": [ { "0": [ - "E_TABM_1007_A_AFFY_2.design.csv:md5,120f63cae2193b97d7451483bdbbaab1" + [ + { + "accession": "E-TABM-1007" + }, + "E_TABM_1007_A_AFFY_2.microarray.normalised.counts.csv:md5,a3afe33d7eaed3339da9109bf25bb3ed" + ] ], "1": [ - "E_TABM_1007_A_AFFY_2.microarray.normalised.counts.csv:md5,a3afe33d7eaed3339da9109bf25bb3ed" + [ + { + "accession": "E-TABM-1007" + }, + "E_TABM_1007_A_AFFY_2.design.csv:md5,120f63cae2193b97d7451483bdbbaab1" + ] ], "2": [ + + ], + "3": [ + + ], + "4": [ [ "EXPRESSIONATLAS_GETDATA", "R", "4.3.3 (2024-02-29)" ] ], - "3": [ + "5": [ [ "EXPRESSIONATLAS_GETDATA", "ExpressionAtlas", @@ -97,17 +165,27 @@ ] ], "counts": [ - "E_TABM_1007_A_AFFY_2.microarray.normalised.counts.csv:md5,a3afe33d7eaed3339da9109bf25bb3ed" + [ + { + "accession": "E-TABM-1007" + }, + "E_TABM_1007_A_AFFY_2.microarray.normalised.counts.csv:md5,a3afe33d7eaed3339da9109bf25bb3ed" + ] ], "design": [ - "E_TABM_1007_A_AFFY_2.design.csv:md5,120f63cae2193b97d7451483bdbbaab1" + [ + { + "accession": "E-TABM-1007" + }, + "E_TABM_1007_A_AFFY_2.design.csv:md5,120f63cae2193b97d7451483bdbbaab1" + ] ] } ], "meta": { "nf-test": "0.9.2", - "nextflow": "25.04.4" + "nextflow": "25.04.8" }, - "timestamp": "2025-07-18T12:58:01.254049012" + "timestamp": "2025-11-08T12:15:11.97993127" } } \ No newline at end of file diff --git a/tests/modules/local/idmapping/gprofiler/main.nf.test b/tests/modules/local/idmapping/gprofiler/main.nf.test index 09c758f2..7592183a 100644 --- a/tests/modules/local/idmapping/gprofiler/main.nf.test +++ b/tests/modules/local/idmapping/gprofiler/main.nf.test @@ -19,6 +19,7 @@ nextflow_process { ) input[1] = "Beta vulgaris" input[2] = Channel.value([]) + input[3] = Channel.value([]) """ } } @@ -45,6 +46,7 @@ nextflow_process { ) input[1] = "Arabidopsis thaliana" input[2] = Channel.value([]) + input[3] = Channel.value([]) """ } } @@ -72,6 +74,7 @@ nextflow_process { ) input[1] = "Arabidopsis thaliana" input[2] = Channel.value([]) + input[3] = Channel.value([]) """ } } @@ -100,6 +103,7 @@ nextflow_process { ) input[1] = "Arabidopsis thaliana" input[2] = Channel.value([]) + input[3] = Channel.value([]) """ } } @@ -108,9 +112,7 @@ nextflow_process { then { assertAll( { assert process.success }, - { assert process.trace.succeeded().size() == 0 }, - { assert process.trace.failed().size() == 1 }, - { assert process.out.renamed.size() == 0 }, + { assert process.out.counts.size() == 0 }, { assert process.out.metadata.size() == 0 }, { assert process.out.mapping.size() == 0 } ) @@ -133,6 +135,7 @@ nextflow_process { ) input[1] = "Homo sapiens" input[2] = Channel.value([]) + input[3] = Channel.value([]) """ } } @@ -141,9 +144,7 @@ nextflow_process { then { assertAll( { assert process.success }, - { assert process.trace.succeeded().size() == 0 }, - { assert process.trace.failed().size() == 1 }, - { assert process.out.renamed.size() == 0 }, + { assert process.out.counts.size() == 0 }, { assert process.out.metadata.size() == 0 }, { assert process.out.mapping.size() == 0 } ) @@ -166,6 +167,7 @@ nextflow_process { ) input[1] = "Beta vulgaris" input[2] = Channel.fromPath( "$projectDir/tests/test_data/idmapping/custom/mapping.csv", checkIfExists: true) + input[3] = Channel.fromPath( "$projectDir/tests/test_data/idmapping/custom/metadata.csv", checkIfExists: true) """ } } diff --git a/tests/modules/local/idmapping/gprofiler/main.nf.test.snap b/tests/modules/local/idmapping/gprofiler/main.nf.test.snap index e70ee267..b8aafef2 100644 --- a/tests/modules/local/idmapping/gprofiler/main.nf.test.snap +++ b/tests/modules/local/idmapping/gprofiler/main.nf.test.snap @@ -11,42 +11,45 @@ ] ], "1": [ - + "counts.ensembl_ids.metadata.csv:md5,af7e8d352d9c4591d53d95f660457beb" ], "2": [ "counts.ensembl_ids.mapping.csv:md5,6ff8d8f71b9df7a1b08ff0bfda8da755" ], "3": [ + + ], + "4": [ [ - "IDMAPPING_GPROFILER", + "GPROFILER_IDMAPPING", "python", "3.13.5" ] ], - "4": [ + "5": [ [ - "IDMAPPING_GPROFILER", + "GPROFILER_IDMAPPING", "pandas", "2.3.1" ] ], - "5": [ + "6": [ [ - "IDMAPPING_GPROFILER", + "GPROFILER_IDMAPPING", "requests", "2.32.4" ] ], - "metadata": [ - - ], - "renamed": [ + "counts": [ [ { "dataset": "test" }, "counts.ensembl_ids.renamed.csv:md5,b2f45fd17fcb2688ea08b94527f70c1f" ] + ], + "metadata": [ + "counts.ensembl_ids.metadata.csv:md5,af7e8d352d9c4591d53d95f660457beb" ] } ], @@ -54,7 +57,7 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-10-16T08:27:50.328211036" + "timestamp": "2025-11-08T13:09:26.858764543" }, "Map Ensembl IDs to themselves": { "content": [ @@ -69,18 +72,36 @@ ], "3": [ - + [ + "test", + "failure_reason.txt:md5,7ae1b80b4f94b2bed454756d6487c03f" + ] ], "4": [ - + [ + "GPROFILER_IDMAPPING", + "python", + "3.13.5" + ] ], "5": [ - + [ + "GPROFILER_IDMAPPING", + "pandas", + "2.3.1" + ] ], - "metadata": [ + "6": [ + [ + "GPROFILER_IDMAPPING", + "requests", + "2.32.4" + ] + ], + "counts": [ ], - "renamed": [ + "metadata": [ ] } @@ -89,7 +110,7 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-10-16T08:27:11.012622456" + "timestamp": "2025-11-08T12:59:22.863610113" }, "Map Uniprot IDs": { "content": [ @@ -109,36 +130,39 @@ "counts.uniprot_ids.mapping.csv:md5,fe88c79c45d45825d28f325f7a2f383e" ], "3": [ + + ], + "4": [ [ - "IDMAPPING_GPROFILER", + "GPROFILER_IDMAPPING", "python", "3.13.5" ] ], - "4": [ + "5": [ [ - "IDMAPPING_GPROFILER", + "GPROFILER_IDMAPPING", "pandas", "2.3.1" ] ], - "5": [ + "6": [ [ - "IDMAPPING_GPROFILER", + "GPROFILER_IDMAPPING", "requests", "2.32.4" ] ], - "metadata": [ - "counts.uniprot_ids.metadata.csv:md5,b87d6533848a3ae07b289ec6b0c4a1ff" - ], - "renamed": [ + "counts": [ [ { "dataset": "test" }, "counts.uniprot_ids.renamed.csv:md5,a93d7145f8b35f6074cdf21c7c97de7c" ] + ], + "metadata": [ + "counts.uniprot_ids.metadata.csv:md5,b87d6533848a3ae07b289ec6b0c4a1ff" ] } ], @@ -146,7 +170,7 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-10-16T08:27:28.606917646" + "timestamp": "2025-11-08T12:59:34.111655741" }, "Map NCBI IDs": { "content": [ @@ -166,36 +190,39 @@ "counts.ncbi_ids.mapping.csv:md5,fe4fd9005dce99b7722b84134f51badd" ], "3": [ + + ], + "4": [ [ - "IDMAPPING_GPROFILER", + "GPROFILER_IDMAPPING", "python", "3.13.5" ] ], - "4": [ + "5": [ [ - "IDMAPPING_GPROFILER", + "GPROFILER_IDMAPPING", "pandas", "2.3.1" ] ], - "5": [ + "6": [ [ - "IDMAPPING_GPROFILER", + "GPROFILER_IDMAPPING", "requests", "2.32.4" ] ], - "metadata": [ - "counts.ncbi_ids.metadata.csv:md5,b87d6533848a3ae07b289ec6b0c4a1ff" - ], - "renamed": [ + "counts": [ [ { "dataset": "test" }, "counts.ncbi_ids.renamed.csv:md5,a93d7145f8b35f6074cdf21c7c97de7c" ] + ], + "metadata": [ + "counts.ncbi_ids.metadata.csv:md5,b87d6533848a3ae07b289ec6b0c4a1ff" ] } ], @@ -203,6 +230,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-10-16T08:27:20.052825156" + "timestamp": "2025-11-08T12:59:28.504346412" } } \ No newline at end of file diff --git a/tests/modules/local/normfinder/main.nf.test.snap b/tests/modules/local/normfinder/main.nf.test.snap index fb177183..f813c389 100644 --- a/tests/modules/local/normfinder/main.nf.test.snap +++ b/tests/modules/local/normfinder/main.nf.test.snap @@ -3,7 +3,7 @@ "content": [ { "0": [ - "stability_values.csv:md5,c42bf759bbbab5bd938c2b2804e68e62" + "stability_values.normfinder.csv:md5,2cbe1b4307c32acea9bb844631e79297" ], "1": [ [ @@ -20,21 +20,21 @@ ] ], "stability_values": [ - "stability_values.csv:md5,c42bf759bbbab5bd938c2b2804e68e62" + "stability_values.normfinder.csv:md5,2cbe1b4307c32acea9bb844631e79297" ] } ], "meta": { "nf-test": "0.9.2", - "nextflow": "25.04.6" + "nextflow": "25.04.8" }, - "timestamp": "2025-10-21T07:38:07.958785231" + "timestamp": "2025-11-08T12:20:22.039948992" }, "Very small dataset - Cq values": { "content": [ { "0": [ - "stability_values.csv:md5,a52096f27ba998ae76c939833dbc54fc" + "stability_values.normfinder.csv:md5,a52096f27ba998ae76c939833dbc54fc" ], "1": [ [ @@ -51,7 +51,7 @@ ] ], "stability_values": [ - "stability_values.csv:md5,a52096f27ba998ae76c939833dbc54fc" + "stability_values.normfinder.csv:md5,a52096f27ba998ae76c939833dbc54fc" ] } ], @@ -59,6 +59,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-10-16T05:45:46.983769703" + "timestamp": "2025-11-08T12:20:15.183464096" } } \ No newline at end of file diff --git a/tests/test_data/idmapping/custom/metadata.csv b/tests/test_data/idmapping/custom/metadata.csv new file mode 100644 index 00000000..ca3c36b9 --- /dev/null +++ b/tests/test_data/idmapping/custom/metadata.csv @@ -0,0 +1,4 @@ +ensembl_gene_id,name,description +SNSRNA049434199,geneA,descriptionA +SNSRNA049434246,geneB,descriptionB +SNSRNA049434252,geneC,descriptionC From f5d82d140070ac0f8f2b0f23c90d5d7f9582a1ff Mon Sep 17 00:00:00 2001 From: Olivier Date: Sat, 8 Nov 2025 15:14:14 +0100 Subject: [PATCH 143/258] remove tag "module" in module tests --- tests/modules/local/dataset_statistics/main.nf.test | 1 - tests/modules/local/expressionatlas/getaccessions/main.nf.test | 1 - tests/modules/local/expressionatlas/getdata/main.nf.test | 1 - tests/modules/local/genorm/compute_m_measure/main.nf.test | 1 - tests/modules/local/genorm/cross_join/main.nf.test | 1 - tests/modules/local/genorm/expression_ratio/main.nf.test | 1 - tests/modules/local/genorm/make_chunks/main.nf.test | 1 - .../modules/local/genorm/ratio_standard_variation/main.nf.test | 1 - tests/modules/local/idmapping/gprofiler/main.nf.test | 1 - tests/modules/local/normalisation/deseq2/main.nf.test | 2 -- tests/modules/local/normalisation/edger/main.nf.test | 2 -- tests/modules/local/quantile_normalisation/main.nf.test | 1 - 12 files changed, 14 deletions(-) diff --git a/tests/modules/local/dataset_statistics/main.nf.test b/tests/modules/local/dataset_statistics/main.nf.test index ddb5fe7d..b31fe99f 100644 --- a/tests/modules/local/dataset_statistics/main.nf.test +++ b/tests/modules/local/dataset_statistics/main.nf.test @@ -4,7 +4,6 @@ nextflow_process { script "modules/local/dataset_statistics/main.nf" process "DATASET_STATISTICS" tag "dataset_statistics" - tag "module" test("Uniform distribution") { diff --git a/tests/modules/local/expressionatlas/getaccessions/main.nf.test b/tests/modules/local/expressionatlas/getaccessions/main.nf.test index 4f025170..d4d383c4 100644 --- a/tests/modules/local/expressionatlas/getaccessions/main.nf.test +++ b/tests/modules/local/expressionatlas/getaccessions/main.nf.test @@ -4,7 +4,6 @@ nextflow_process { script "modules/local/expressionatlas/getaccessions/main.nf" process "EXPRESSIONATLAS_GETACCESSIONS" tag "eatlas_getaccessions" - tag "module" test("Solanum tuberosum one keyword") { diff --git a/tests/modules/local/expressionatlas/getdata/main.nf.test b/tests/modules/local/expressionatlas/getdata/main.nf.test index 878669a1..4558f426 100644 --- a/tests/modules/local/expressionatlas/getdata/main.nf.test +++ b/tests/modules/local/expressionatlas/getdata/main.nf.test @@ -4,7 +4,6 @@ nextflow_process { script "modules/local/expressionatlas/getdata/main.nf" process "EXPRESSIONATLAS_GETDATA" tag "eatlas_getdata" - tag "module" test("Transcriptome Analysis of the potato (rnaseq)") { diff --git a/tests/modules/local/genorm/compute_m_measure/main.nf.test b/tests/modules/local/genorm/compute_m_measure/main.nf.test index a3072d4a..0e77138b 100644 --- a/tests/modules/local/genorm/compute_m_measure/main.nf.test +++ b/tests/modules/local/genorm/compute_m_measure/main.nf.test @@ -4,7 +4,6 @@ nextflow_process { script "modules/local/genorm/compute_m_measure/main.nf" process "COMPUTE_M_MEASURE" tag "m_measure" - tag "module" test("Four initial chunk files") { diff --git a/tests/modules/local/genorm/cross_join/main.nf.test b/tests/modules/local/genorm/cross_join/main.nf.test index 244c74d0..fd154604 100644 --- a/tests/modules/local/genorm/cross_join/main.nf.test +++ b/tests/modules/local/genorm/cross_join/main.nf.test @@ -4,7 +4,6 @@ nextflow_process { script "modules/local/genorm/cross_join/main.nf" process "CROSS_JOIN" tag "cross_join" - tag "module" test("Should run without failures") { diff --git a/tests/modules/local/genorm/expression_ratio/main.nf.test b/tests/modules/local/genorm/expression_ratio/main.nf.test index 018fb962..5630b93a 100644 --- a/tests/modules/local/genorm/expression_ratio/main.nf.test +++ b/tests/modules/local/genorm/expression_ratio/main.nf.test @@ -4,7 +4,6 @@ nextflow_process { script "modules/local/genorm/expression_ratio/main.nf" process "EXPRESSION_RATIO" tag "expression_ratio" - tag "module" test("Should run without failures") { diff --git a/tests/modules/local/genorm/make_chunks/main.nf.test b/tests/modules/local/genorm/make_chunks/main.nf.test index bd64031e..d43bcea9 100644 --- a/tests/modules/local/genorm/make_chunks/main.nf.test +++ b/tests/modules/local/genorm/make_chunks/main.nf.test @@ -4,7 +4,6 @@ nextflow_process { script "modules/local/genorm/make_chunks/main.nf" process "MAKE_CHUNKS" tag "make_chunks" - tag "module" test("Should run without failures") { diff --git a/tests/modules/local/genorm/ratio_standard_variation/main.nf.test b/tests/modules/local/genorm/ratio_standard_variation/main.nf.test index f607a4d0..39b52373 100644 --- a/tests/modules/local/genorm/ratio_standard_variation/main.nf.test +++ b/tests/modules/local/genorm/ratio_standard_variation/main.nf.test @@ -4,7 +4,6 @@ nextflow_process { script "modules/local/genorm/ratio_standard_variation/main.nf" process "RATIO_STANDARD_VARIATION" tag "ratio_std" - tag "module" test("Should run without failures") { diff --git a/tests/modules/local/idmapping/gprofiler/main.nf.test b/tests/modules/local/idmapping/gprofiler/main.nf.test index 7592183a..51639da7 100644 --- a/tests/modules/local/idmapping/gprofiler/main.nf.test +++ b/tests/modules/local/idmapping/gprofiler/main.nf.test @@ -4,7 +4,6 @@ nextflow_process { script "modules/local/gprofiler/idmapping/main.nf" process "GPROFILER_IDMAPPING" tag "idmapping" - tag "module" test("Map Ensembl IDs to themselves") { diff --git a/tests/modules/local/normalisation/deseq2/main.nf.test b/tests/modules/local/normalisation/deseq2/main.nf.test index bb17c4c0..a95c2c68 100644 --- a/tests/modules/local/normalisation/deseq2/main.nf.test +++ b/tests/modules/local/normalisation/deseq2/main.nf.test @@ -4,8 +4,6 @@ nextflow_process { script "modules/local/normalisation/deseq2/main.nf" process "NORMALISATION_DESEQ2" tag "deseq2" - tag "normalise" - tag "module" test("Very small dataset") { diff --git a/tests/modules/local/normalisation/edger/main.nf.test b/tests/modules/local/normalisation/edger/main.nf.test index 0d3b1ca0..d6380c74 100644 --- a/tests/modules/local/normalisation/edger/main.nf.test +++ b/tests/modules/local/normalisation/edger/main.nf.test @@ -4,8 +4,6 @@ nextflow_process { script "modules/local/normalisation/edger/main.nf" process "NORMALISATION_EDGER" tag "edger" - tag "normalisation" - tag "module" test("Very small dataset") { diff --git a/tests/modules/local/quantile_normalisation/main.nf.test b/tests/modules/local/quantile_normalisation/main.nf.test index 22cdd764..469c52bb 100644 --- a/tests/modules/local/quantile_normalisation/main.nf.test +++ b/tests/modules/local/quantile_normalisation/main.nf.test @@ -4,7 +4,6 @@ nextflow_process { script "modules/local/quantile_normalisation/main.nf" process "QUANTILE_NORMALISATION" tag "quant_norm" - tag "module" test("Uniform target distribution") { From e02ca8d724b0215cb492018e83cf3d60e213b6c1 Mon Sep 17 00:00:00 2001 From: Olivier Date: Sat, 8 Nov 2025 16:27:24 +0100 Subject: [PATCH 144/258] add missing module tests --- modules/local/clean_count_data/main.nf | 8 +- .../local/aggregate_results/main.nf.test | 56 +++++++++ .../local/aggregate_results/main.nf.test.snap | 100 +++++++++++++++ .../local/clean_count_data/main.nf.test | 56 +++++++++ .../local/clean_count_data/main.nf.test.snap | 83 +++++++++++++ .../compute_base_statistics/main.nf.test | 48 ++++++++ .../compute_base_statistics/main.nf.test.snap | 64 ++++++++++ .../compute_stability_scores/main.nf.test | 52 ++++++++ .../main.nf.test.snap | 64 ++++++++++ tests/modules/local/geo/getdata/main.nf.test | 56 +++++++++ .../local/geo/getdata/main.nf.test.snap | 108 ++++++++++++++++ .../local/get_candidate_genes/main.nf.test | 56 +++++++++ .../get_candidate_genes/main.nf.test.snap | 64 ++++++++++ tests/modules/local/merge_counts/main.nf.test | 78 ++++++++++++ .../local/merge_counts/main.nf.test.snap | 116 ++++++++++++++++++ tests/test_data/aggregate_results/mapping.csv | 4 + .../test_data/aggregate_results/metadata.csv | 4 + .../microarray_stats_all_genes.csv | 10 ++ .../rnaseq_stats_all_genes.csv | 10 ++ .../output/stats_all_genes.csv | 10 ++ .../input/genorm.m_measures.csv | 10 ++ .../input/stability_values.normfinder.csv | 10 ++ .../input/stats_all_genes.csv | 10 ++ .../input/count2.raw.cpm.quant_norm.parquet | Bin 0 -> 6547 bytes .../output/test.dataset_stats.csv | 9 ++ 25 files changed, 1082 insertions(+), 4 deletions(-) create mode 100644 tests/modules/local/aggregate_results/main.nf.test create mode 100644 tests/modules/local/aggregate_results/main.nf.test.snap create mode 100644 tests/modules/local/clean_count_data/main.nf.test create mode 100644 tests/modules/local/clean_count_data/main.nf.test.snap create mode 100644 tests/modules/local/compute_base_statistics/main.nf.test create mode 100644 tests/modules/local/compute_base_statistics/main.nf.test.snap create mode 100644 tests/modules/local/compute_stability_scores/main.nf.test create mode 100644 tests/modules/local/compute_stability_scores/main.nf.test.snap create mode 100644 tests/modules/local/geo/getdata/main.nf.test create mode 100644 tests/modules/local/geo/getdata/main.nf.test.snap create mode 100644 tests/modules/local/get_candidate_genes/main.nf.test create mode 100644 tests/modules/local/get_candidate_genes/main.nf.test.snap create mode 100644 tests/modules/local/merge_counts/main.nf.test create mode 100644 tests/modules/local/merge_counts/main.nf.test.snap create mode 100644 tests/test_data/aggregate_results/mapping.csv create mode 100644 tests/test_data/aggregate_results/metadata.csv create mode 100644 tests/test_data/aggregate_results/microarray_stats_all_genes.csv create mode 100644 tests/test_data/aggregate_results/rnaseq_stats_all_genes.csv create mode 100644 tests/test_data/base_statistics/output/stats_all_genes.csv create mode 100644 tests/test_data/compute_stability_scores/input/genorm.m_measures.csv create mode 100644 tests/test_data/compute_stability_scores/input/stability_values.normfinder.csv create mode 100644 tests/test_data/compute_stability_scores/input/stats_all_genes.csv create mode 100644 tests/test_data/dataset_statistics/input/count2.raw.cpm.quant_norm.parquet create mode 100644 tests/test_data/dataset_statistics/output/test.dataset_stats.csv diff --git a/modules/local/clean_count_data/main.nf b/modules/local/clean_count_data/main.nf index e3f64f46..3d988f62 100644 --- a/modules/local/clean_count_data/main.nf +++ b/modules/local/clean_count_data/main.nf @@ -14,10 +14,10 @@ process CLEAN_COUNT_DATA { val ks_pvalue_threshold output: - tuple val(meta), path('cleaned_counts_filtered.parquet'), emit: counts - tuple val(meta.dataset), path("failure_reason.txt"), optional: true, topic: clean_count_failure_reason - tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions - tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions + tuple val(meta), path('cleaned_counts_filtered.parquet'), optional: true, emit: counts + tuple val(meta.dataset), path("failure_reason.txt"), optional: true, topic: clean_count_failure_reason + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions + tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions script: """ diff --git a/tests/modules/local/aggregate_results/main.nf.test b/tests/modules/local/aggregate_results/main.nf.test new file mode 100644 index 00000000..5d4a0bf9 --- /dev/null +++ b/tests/modules/local/aggregate_results/main.nf.test @@ -0,0 +1,56 @@ +nextflow_process { + + name "Test Process AGGREGATE_RESULTS" + script "modules/local/aggregate_results/main.nf" + process "AGGREGATE_RESULTS" + tag "aggregate_results" + + test("Without microarray") { + + when { + process { + """ + input[0] = file( '$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet', checkIfExists: true) + input[1] = file( '$projectDir/tests/test_data/base_statistics/output/stats_all_genes.csv', checkIfExists: true) + input[2] = file( '$projectDir/tests/test_data/aggregate_results/rnaseq_stats_all_genes.csv', checkIfExists: true) + input[3] = [] + input[4] = file( '$projectDir/tests/test_data/aggregate_results/metadata.csv', checkIfExists: true) + input[5] = file( '$projectDir/tests/test_data/aggregate_results/mapping.csv', checkIfExists: true) + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("With microarray") { + + when { + process { + """ + input[0] = file( '$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet', checkIfExists: true) + input[1] = file( '$projectDir/tests/test_data/base_statistics/output/stats_all_genes.csv', checkIfExists: true) + input[2] = file( '$projectDir/tests/test_data/aggregate_results/rnaseq_stats_all_genes.csv', checkIfExists: true) + input[3] = file( '$projectDir/tests/test_data/aggregate_results/microarray_stats_all_genes.csv', checkIfExists: true) + input[4] = file( '$projectDir/tests/test_data/aggregate_results/metadata.csv', checkIfExists: true) + input[5] = file( '$projectDir/tests/test_data/aggregate_results/mapping.csv', checkIfExists: true) + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + +} diff --git a/tests/modules/local/aggregate_results/main.nf.test.snap b/tests/modules/local/aggregate_results/main.nf.test.snap new file mode 100644 index 00000000..ba15af1f --- /dev/null +++ b/tests/modules/local/aggregate_results/main.nf.test.snap @@ -0,0 +1,100 @@ +{ + "Without microarray": { + "content": [ + { + "0": [ + "all_genes_summary.csv:md5,3b49b24a0cb36b9e35b37410917de0b1" + ], + "1": [ + "top_stable_genes_summary.csv:md5,ba17539a8a7b462f0e455dd4f81c5e62" + ], + "2": [ + "all_counts_filtered.parquet:md5,286e7595b3a7b961e39976585d3c106e" + ], + "3": [ + "top_stable_genes_transposed_counts_filtered.csv:md5,0380b330c64ce9efe9644f84e7a5cddb" + ], + "4": [ + [ + "AGGREGATE_RESULTS", + "python", + "3.12.8" + ] + ], + "5": [ + [ + "AGGREGATE_RESULTS", + "polars", + "1.17.1" + ] + ], + "all_counts_filtered": [ + "all_counts_filtered.parquet:md5,286e7595b3a7b961e39976585d3c106e" + ], + "all_genes_summary": [ + "all_genes_summary.csv:md5,3b49b24a0cb36b9e35b37410917de0b1" + ], + "top_stable_genes_summary": [ + "top_stable_genes_summary.csv:md5,ba17539a8a7b462f0e455dd4f81c5e62" + ], + "top_stable_genes_transposed_counts_filtered": [ + "top_stable_genes_transposed_counts_filtered.csv:md5,0380b330c64ce9efe9644f84e7a5cddb" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-08T16:26:52.3050561" + }, + "With microarray": { + "content": [ + { + "0": [ + "all_genes_summary.csv:md5,da32872124a121e27f3dc1c784c8601b" + ], + "1": [ + "top_stable_genes_summary.csv:md5,da32872124a121e27f3dc1c784c8601b" + ], + "2": [ + "all_counts_filtered.parquet:md5,286e7595b3a7b961e39976585d3c106e" + ], + "3": [ + "top_stable_genes_transposed_counts_filtered.csv:md5,0380b330c64ce9efe9644f84e7a5cddb" + ], + "4": [ + [ + "AGGREGATE_RESULTS", + "python", + "3.12.8" + ] + ], + "5": [ + [ + "AGGREGATE_RESULTS", + "polars", + "1.17.1" + ] + ], + "all_counts_filtered": [ + "all_counts_filtered.parquet:md5,286e7595b3a7b961e39976585d3c106e" + ], + "all_genes_summary": [ + "all_genes_summary.csv:md5,da32872124a121e27f3dc1c784c8601b" + ], + "top_stable_genes_summary": [ + "top_stable_genes_summary.csv:md5,da32872124a121e27f3dc1c784c8601b" + ], + "top_stable_genes_transposed_counts_filtered": [ + "top_stable_genes_transposed_counts_filtered.csv:md5,0380b330c64ce9efe9644f84e7a5cddb" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-08T16:26:56.797009735" + } +} \ No newline at end of file diff --git a/tests/modules/local/clean_count_data/main.nf.test b/tests/modules/local/clean_count_data/main.nf.test new file mode 100644 index 00000000..12967ff4 --- /dev/null +++ b/tests/modules/local/clean_count_data/main.nf.test @@ -0,0 +1,56 @@ +nextflow_process { + + name "Test Process CLEAN_COUNT_DATA" + script "modules/local/clean_count_data/main.nf" + process "CLEAN_COUNT_DATA" + tag "clean_count" + + test("Runs ok") { + + when { + process { + """ + input[0] = [ + [dataset: 'test'], + file( '$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet', checkIfExists: true), + file( '$projectDir/tests/test_data/dataset_statistics/output/test.dataset_stats.csv', checkIfExists: true) + ] + input[1] = 0 + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("No sample left") { + + when { + process { + """ + input[0] = [ + [dataset: 'test'], + file( '$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet', checkIfExists: true), + file( '$projectDir/tests/test_data/dataset_statistics/input/output/test.dataset_stats.csv', checkIfExists: true) + ] + input[1] = 0.1 + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + +} diff --git a/tests/modules/local/clean_count_data/main.nf.test.snap b/tests/modules/local/clean_count_data/main.nf.test.snap new file mode 100644 index 00000000..0267a184 --- /dev/null +++ b/tests/modules/local/clean_count_data/main.nf.test.snap @@ -0,0 +1,83 @@ +{ + "No sample left": { + "content": [ + { + "0": [ + + ], + "1": [ + [ + "test", + "failure_reason.txt:md5,8b7b701a2f7e1540901e9aab371a1421" + ] + ], + "2": [ + [ + "CLEAN_COUNT_DATA", + "python", + "3.12.8" + ] + ], + "3": [ + [ + "CLEAN_COUNT_DATA", + "polars", + "1.17.1" + ] + ], + "counts": [ + + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-08T14:34:33.283176679" + }, + "Runs ok": { + "content": [ + { + "0": [ + [ + { + "dataset": "test" + }, + "cleaned_counts_filtered.parquet:md5,93c0abe8562314fe416769aa160d094a" + ] + ], + "1": [ + + ], + "2": [ + [ + "CLEAN_COUNT_DATA", + "python", + "3.12.8" + ] + ], + "3": [ + [ + "CLEAN_COUNT_DATA", + "polars", + "1.17.1" + ] + ], + "counts": [ + [ + { + "dataset": "test" + }, + "cleaned_counts_filtered.parquet:md5,93c0abe8562314fe416769aa160d094a" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-08T14:33:31.956184265" + } +} \ No newline at end of file diff --git a/tests/modules/local/compute_base_statistics/main.nf.test b/tests/modules/local/compute_base_statistics/main.nf.test new file mode 100644 index 00000000..57474fd4 --- /dev/null +++ b/tests/modules/local/compute_base_statistics/main.nf.test @@ -0,0 +1,48 @@ +nextflow_process { + + name "Test Process COMPUTE_BASE_STATISTICS" + script "modules/local/compute_base_statistics/main.nf" + process "COMPUTE_BASE_STATISTICS" + tag "base_stats" + + test("No platform") { + + when { + process { + """ + input[0] = file( '$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet', checkIfExists: true) + input[1] = [] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("RNAseq platform") { + + when { + process { + """ + input[0] = file( '$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet', checkIfExists: true) + input[1] = 'rnaseq' + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + +} diff --git a/tests/modules/local/compute_base_statistics/main.nf.test.snap b/tests/modules/local/compute_base_statistics/main.nf.test.snap new file mode 100644 index 00000000..854efb21 --- /dev/null +++ b/tests/modules/local/compute_base_statistics/main.nf.test.snap @@ -0,0 +1,64 @@ +{ + "No platform": { + "content": [ + { + "0": [ + "stats_all_genes.csv:md5,db4da5a9d006a3bb2552bc4c3653c894" + ], + "1": [ + [ + "COMPUTE_BASE_STATISTICS", + "python", + "3.12.8" + ] + ], + "2": [ + [ + "COMPUTE_BASE_STATISTICS", + "polars", + "1.17.1" + ] + ], + "stats": [ + "stats_all_genes.csv:md5,db4da5a9d006a3bb2552bc4c3653c894" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-08T14:38:59.179553063" + }, + "RNAseq platform": { + "content": [ + { + "0": [ + "rnaseq.stats_all_genes.csv:md5,dea1294e04f0571cca9d5a8301b2a711" + ], + "1": [ + [ + "COMPUTE_BASE_STATISTICS", + "python", + "3.12.8" + ] + ], + "2": [ + [ + "COMPUTE_BASE_STATISTICS", + "polars", + "1.17.1" + ] + ], + "stats": [ + "rnaseq.stats_all_genes.csv:md5,dea1294e04f0571cca9d5a8301b2a711" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-08T14:39:03.817265373" + } +} \ No newline at end of file diff --git a/tests/modules/local/compute_stability_scores/main.nf.test b/tests/modules/local/compute_stability_scores/main.nf.test new file mode 100644 index 00000000..c0f5b44c --- /dev/null +++ b/tests/modules/local/compute_stability_scores/main.nf.test @@ -0,0 +1,52 @@ +nextflow_process { + + name "Test Process COMPUTE_STABILITY_SCORES" + script "modules/local/compute_stability_scores/main.nf" + process "COMPUTE_STABILITY_SCORES" + tag "stability_scores" + + test("Without Genorm") { + + when { + process { + """ + input[0] = file( '$projectDir/tests/test_data/compute_stability_scores/input/stats_all_genes.csv', checkIfExists: true) + input[1] = "0.8,0.1,0.1,0" + input[2] = file( '$projectDir/tests/test_data/compute_stability_scores/input/stability_values.normfinder.csv', checkIfExists: true) + input[3] = [] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("With Genorm") { + + when { + process { + """ + input[0] = file( '$projectDir/tests/test_data/compute_stability_scores/input/stats_all_genes.csv', checkIfExists: true) + input[1] = "0.8,0.1,0.1,0.1" + input[2] = file( '$projectDir/tests/test_data/compute_stability_scores/input/stability_values.normfinder.csv', checkIfExists: true) + input[3] = file( '$projectDir/tests/test_data/compute_stability_scores/input/genorm.m_measures.csv', checkIfExists: true) + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + +} diff --git a/tests/modules/local/compute_stability_scores/main.nf.test.snap b/tests/modules/local/compute_stability_scores/main.nf.test.snap new file mode 100644 index 00000000..cbcf0c8a --- /dev/null +++ b/tests/modules/local/compute_stability_scores/main.nf.test.snap @@ -0,0 +1,64 @@ +{ + "With Genorm": { + "content": [ + { + "0": [ + "stats_with_scores.csv:md5,dcf0165aebf5905242fc375f2d734c1a" + ], + "1": [ + [ + "COMPUTE_STABILITY_SCORES", + "python", + "3.13.7" + ] + ], + "2": [ + [ + "COMPUTE_STABILITY_SCORES", + "polars", + "1.33.1" + ] + ], + "stats_with_stability_scores": [ + "stats_with_scores.csv:md5,dcf0165aebf5905242fc375f2d734c1a" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-08T15:23:36.939080125" + }, + "Without Genorm": { + "content": [ + { + "0": [ + "stats_with_scores.csv:md5,2c5c81309a2b09eb44163d8a02393ce0" + ], + "1": [ + [ + "COMPUTE_STABILITY_SCORES", + "python", + "3.13.7" + ] + ], + "2": [ + [ + "COMPUTE_STABILITY_SCORES", + "polars", + "1.33.1" + ] + ], + "stats_with_stability_scores": [ + "stats_with_scores.csv:md5,2c5c81309a2b09eb44163d8a02393ce0" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-08T15:23:31.460651402" + } +} \ No newline at end of file diff --git a/tests/modules/local/geo/getdata/main.nf.test b/tests/modules/local/geo/getdata/main.nf.test new file mode 100644 index 00000000..01f89ffb --- /dev/null +++ b/tests/modules/local/geo/getdata/main.nf.test @@ -0,0 +1,56 @@ +nextflow_process { + + name "Test Process GEO_GETDATA" + script "modules/local/geo/getdata/main.nf" + process "GEO_GETDATA" + tag "geo_getdata" + + test("Beta vulgaris - Small RNA of sugar beet in response to drought stress") { + + when { + + process { + """ + input[0] = [ + [ id: "test" ], + "GSE205328" + ] + input[1] = "beta vulgaris" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("Accession does not exist") { + + when { + + process { + """ + input[0] = [ + [ ], + "GSE568945478" + ] + input[1] = "blabla" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + +} diff --git a/tests/modules/local/geo/getdata/main.nf.test.snap b/tests/modules/local/geo/getdata/main.nf.test.snap new file mode 100644 index 00000000..a987b657 --- /dev/null +++ b/tests/modules/local/geo/getdata/main.nf.test.snap @@ -0,0 +1,108 @@ +{ + "Accession does not exist": { + "content": [ + { + "0": [ + + ], + "1": [ + + ], + "2": [ + [ + "GSE568945478", + "failure_reason.txt:md5,2631bb8c3ae982a1b869107f8dbfa107" + ] + ], + "3": [ + + ], + "4": [ + [ + "GEO_GETDATA", + "R", + "4.4.3 (2025-02-28)" + ] + ], + "5": [ + [ + "GEO_GETDATA", + "GEOquery", + "2.74.0" + ] + ], + "6": [ + [ + "GEO_GETDATA", + "dplyr", + "1.1.4" + ] + ], + "counts": [ + + ], + "design": [ + + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-08T13:40:59.903657365" + }, + "Beta vulgaris - Small RNA of sugar beet in response to drought stress": { + "content": [ + { + "0": [ + + ], + "1": [ + + ], + "2": [ + [ + "GSE205328", + "failure_reason.txt:md5,2631bb8c3ae982a1b869107f8dbfa107" + ] + ], + "3": [ + + ], + "4": [ + [ + "GEO_GETDATA", + "R", + "4.4.3 (2025-02-28)" + ] + ], + "5": [ + [ + "GEO_GETDATA", + "GEOquery", + "2.74.0" + ] + ], + "6": [ + [ + "GEO_GETDATA", + "dplyr", + "1.1.4" + ] + ], + "counts": [ + + ], + "design": [ + + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-08T13:43:46.551738497" + } +} \ No newline at end of file diff --git a/tests/modules/local/get_candidate_genes/main.nf.test b/tests/modules/local/get_candidate_genes/main.nf.test new file mode 100644 index 00000000..54c1c1b0 --- /dev/null +++ b/tests/modules/local/get_candidate_genes/main.nf.test @@ -0,0 +1,56 @@ +nextflow_process { + + name "Test Process GET_CANDIDATE_GENES" + script "modules/local/get_candidate_genes/main.nf" + process "GET_CANDIDATE_GENES" + tag "get_candidate_genes" + + test("With coefficient of variation") { + + when { + + process { + """ + input[0] = file('$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet', checkIfExists: true) + input[1] = file('$projectDir/tests/test_data/base_statistics/output/stats_all_genes.csv', checkIfExists: true) + input[2] = "cv" + input[3] = 10 + input[4] = 0.2 + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("With RCVm and no filter on expression level") { + + when { + + process { + """ + input[0] = file('$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet', checkIfExists: true) + input[1] = file('$projectDir/tests/test_data/base_statistics/output/stats_all_genes.csv', checkIfExists: true) + input[2] = "rcvm" + input[3] = 10 + input[4] = 0 + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + +} diff --git a/tests/modules/local/get_candidate_genes/main.nf.test.snap b/tests/modules/local/get_candidate_genes/main.nf.test.snap new file mode 100644 index 00000000..d99ca2bb --- /dev/null +++ b/tests/modules/local/get_candidate_genes/main.nf.test.snap @@ -0,0 +1,64 @@ +{ + "With coefficient of variation": { + "content": [ + { + "0": [ + "candidate_counts.parquet:md5,a671f20b4818b914ca454dce84308287" + ], + "1": [ + [ + "GET_CANDIDATE_GENES", + "python", + "3.12.8" + ] + ], + "2": [ + [ + "GET_CANDIDATE_GENES", + "polars", + "1.17.1" + ] + ], + "counts": [ + "candidate_counts.parquet:md5,a671f20b4818b914ca454dce84308287" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-08T14:46:13.79462279" + }, + "With RCVm and no filter on expression level": { + "content": [ + { + "0": [ + "candidate_counts.parquet:md5,93c0abe8562314fe416769aa160d094a" + ], + "1": [ + [ + "GET_CANDIDATE_GENES", + "python", + "3.12.8" + ] + ], + "2": [ + [ + "GET_CANDIDATE_GENES", + "polars", + "1.17.1" + ] + ], + "counts": [ + "candidate_counts.parquet:md5,93c0abe8562314fe416769aa160d094a" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-08T14:46:41.537978144" + } +} \ No newline at end of file diff --git a/tests/modules/local/merge_counts/main.nf.test b/tests/modules/local/merge_counts/main.nf.test new file mode 100644 index 00000000..315f96ae --- /dev/null +++ b/tests/modules/local/merge_counts/main.nf.test @@ -0,0 +1,78 @@ +nextflow_process { + + name "Test Process MERGE_COUNTS" + script "modules/local/merge_counts/main.nf" + process "MERGE_COUNTS" + tag "merge_counts" + + test("2 files") { + + when { + + process { + """ + input[0] = [ + file("$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet", checkIfExists: true), + file( "$projectDir/tests/test_data/dataset_statistics/input/count2.raw.cpm.quant_norm.parquet", checkIfExists: true) + ] + input[1] = 100 + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("2 identical files") { + + when { + + process { + """ + input[0] = [ + file("$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet", checkIfExists: true), + file( "$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet", checkIfExists: true) + ] + input[1] = 100 + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("1 file") { + + when { + + process { + """ + input[0] = [ + file("$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet", checkIfExists: true) + ] + input[1] = 100 + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } +} diff --git a/tests/modules/local/merge_counts/main.nf.test.snap b/tests/modules/local/merge_counts/main.nf.test.snap new file mode 100644 index 00000000..167e0bb8 --- /dev/null +++ b/tests/modules/local/merge_counts/main.nf.test.snap @@ -0,0 +1,116 @@ +{ + "2 files": { + "content": [ + { + "0": [ + "all_counts.parquet:md5,d23e002ab08e544a259e5bab8fa70d71" + ], + "1": [ + [ + "MERGE_COUNTS", + "python", + "3.14.0" + ] + ], + "2": [ + [ + "MERGE_COUNTS", + "polars", + "1.34.0" + ] + ], + "3": [ + [ + "MERGE_COUNTS", + "tqdm", + "4.67.1" + ] + ], + "counts": [ + "all_counts.parquet:md5,d23e002ab08e544a259e5bab8fa70d71" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-08T15:04:53.721960825" + }, + "2 identical files": { + "content": [ + { + "0": [ + "all_counts.parquet:md5,9ab0a1f56fbdefb41bba263d08833a19" + ], + "1": [ + [ + "MERGE_COUNTS", + "python", + "3.14.0" + ] + ], + "2": [ + [ + "MERGE_COUNTS", + "polars", + "1.34.0" + ] + ], + "3": [ + [ + "MERGE_COUNTS", + "tqdm", + "4.67.1" + ] + ], + "counts": [ + "all_counts.parquet:md5,9ab0a1f56fbdefb41bba263d08833a19" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-08T15:04:58.602015048" + }, + "1 file": { + "content": [ + { + "0": [ + "all_counts.parquet:md5,a40507b974dbda63838b52a2458c5220" + ], + "1": [ + [ + "MERGE_COUNTS", + "python", + "3.14.0" + ] + ], + "2": [ + [ + "MERGE_COUNTS", + "polars", + "1.34.0" + ] + ], + "3": [ + [ + "MERGE_COUNTS", + "tqdm", + "4.67.1" + ] + ], + "counts": [ + "all_counts.parquet:md5,a40507b974dbda63838b52a2458c5220" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-08T15:05:44.418436182" + } +} \ No newline at end of file diff --git a/tests/test_data/aggregate_results/mapping.csv b/tests/test_data/aggregate_results/mapping.csv new file mode 100644 index 00000000..51303291 --- /dev/null +++ b/tests/test_data/aggregate_results/mapping.csv @@ -0,0 +1,4 @@ +original_gene_id,ensembl_gene_id +ENSRNA049434199,ENSRNA049454747 +ENSRNA049434246,ENSRNA049454887 +ENSRNA049434252,SNSRNA049434252 diff --git a/tests/test_data/aggregate_results/metadata.csv b/tests/test_data/aggregate_results/metadata.csv new file mode 100644 index 00000000..ec53b6cc --- /dev/null +++ b/tests/test_data/aggregate_results/metadata.csv @@ -0,0 +1,4 @@ +ensembl_gene_id,name,description +ENSRNA049454747,geneA,descriptionA +ENSRNA049454887,geneB,descriptionB +ENSRNA049454747,geneC,descriptionC diff --git a/tests/test_data/aggregate_results/microarray_stats_all_genes.csv b/tests/test_data/aggregate_results/microarray_stats_all_genes.csv new file mode 100644 index 00000000..d1d2fecb --- /dev/null +++ b/tests/test_data/aggregate_results/microarray_stats_all_genes.csv @@ -0,0 +1,10 @@ +ensembl_gene_id,microarray_mean,microarray_standard_deviation,microarray_median,microarray_median_absolute_deviation,microarray_coefficient_of_variation,microarray_robust_coefficient_of_variation_median,microarray_ratio_nulls_in_all_samples,microarray_ratio_nulls_in_valid_samples,microarray_ratio_zeros,microarray_expression_level_quantile_interval +ENSRNA049454747,0.9375,0.11572751247156893,1.0,0.0,0.12344267996967352,0.0,0.0,0.0,0.0,99 +ENSRNA049454887,0.140625,0.15580293184477811,0.125,0.125,1.1079319597850887,1.4826,0.0,0.0,0.5,11 +ENSRNA049454931,0.4453125,0.12246309575308217,0.4375,0.0625,0.2750048466034126,0.2118,0.0,0.0,0.0,66 +ENSRNA049454947,0.3984375,0.1887975933374152,0.375,0.125,0.4738449401409636,0.4942,0.0,0.0,0.0,44 +ENSRNA049454955,0.421875,0.18525441001112883,0.4375,0.15625,0.4391215644708239,0.5295,0.0,0.0,0.0,55 +ENSRNA049454963,0.78125,0.08838834764831845,0.75,0.0625,0.1131370849898476,0.12355,0.0,0.0,0.0,77 +ENSRNA049454974,0.859375,0.12387890112063936,0.875,0.0625,0.14414999403128945,0.1059,0.0,0.0,0.0,88 +ENSRNA049455639,0.15625,0.20863074009907004,0.125,0.125,1.3352367366340483,1.4826,0.0,0.0,0.375,22 +ENSRNA049455690,0.328125,0.34028283928856257,0.3125,0.3125,1.0370524625937145,1.4826,0.0,0.0,0.375,33 diff --git a/tests/test_data/aggregate_results/rnaseq_stats_all_genes.csv b/tests/test_data/aggregate_results/rnaseq_stats_all_genes.csv new file mode 100644 index 00000000..87a27785 --- /dev/null +++ b/tests/test_data/aggregate_results/rnaseq_stats_all_genes.csv @@ -0,0 +1,10 @@ +ensembl_gene_id,rnaseq_mean,rnaseq_standard_deviation,rnaseq_median,rnaseq_median_absolute_deviation,rnaseq_coefficient_of_variation,rnaseq_robust_coefficient_of_variation_median,rnaseq_ratio_nulls_in_all_samples,rnaseq_ratio_nulls_in_valid_samples,rnaseq_ratio_zeros,rnaseq_expression_level_quantile_interval +ENSRNA049454747,0.9375,0.11572751247156893,1.0,0.0,0.12344267996967352,0.0,0.0,0.0,0.0,99 +ENSRNA049454887,0.140625,0.15580293184477811,0.125,0.125,1.1079319597850887,1.4826,0.0,0.0,0.5,11 +ENSRNA049454931,0.4453125,0.12246309575308217,0.4375,0.0625,0.2750048466034126,0.2118,0.0,0.0,0.0,66 +ENSRNA049454947,0.3984375,0.1887975933374152,0.375,0.125,0.4738449401409636,0.4942,0.0,0.0,0.0,44 +ENSRNA049454955,0.421875,0.18525441001112883,0.4375,0.15625,0.4391215644708239,0.5295,0.0,0.0,0.0,55 +ENSRNA049454963,0.78125,0.08838834764831845,0.75,0.0625,0.1131370849898476,0.12355,0.0,0.0,0.0,77 +ENSRNA049454974,0.859375,0.12387890112063936,0.875,0.0625,0.14414999403128945,0.1059,0.0,0.0,0.0,88 +ENSRNA049455639,0.15625,0.20863074009907004,0.125,0.125,1.3352367366340483,1.4826,0.0,0.0,0.375,22 +ENSRNA049455690,0.328125,0.34028283928856257,0.3125,0.3125,1.0370524625937145,1.4826,0.0,0.0,0.375,33 diff --git a/tests/test_data/base_statistics/output/stats_all_genes.csv b/tests/test_data/base_statistics/output/stats_all_genes.csv new file mode 100644 index 00000000..cda6fe60 --- /dev/null +++ b/tests/test_data/base_statistics/output/stats_all_genes.csv @@ -0,0 +1,10 @@ +ensembl_gene_id,mean,standard_deviation,median,median_absolute_deviation,coefficient_of_variation,robust_coefficient_of_variation_median,ratio_nulls_in_all_samples,ratio_nulls_in_valid_samples,ratio_zeros,expression_level_quantile_interval +ENSRNA049454747,0.9375,0.11572751247156893,1.0,0.0,0.12344267996967352,0.0,0.0,0.0,0.0,99 +ENSRNA049454887,0.140625,0.15580293184477811,0.125,0.125,1.1079319597850887,1.4826,0.0,0.0,0.5,11 +ENSRNA049454931,0.4453125,0.12246309575308217,0.4375,0.0625,0.2750048466034126,0.2118,0.0,0.0,0.0,66 +ENSRNA049454947,0.3984375,0.1887975933374152,0.375,0.125,0.4738449401409636,0.4942,0.0,0.0,0.0,44 +ENSRNA049454955,0.421875,0.18525441001112883,0.4375,0.15625,0.4391215644708239,0.5295,0.0,0.0,0.0,55 +ENSRNA049454963,0.78125,0.08838834764831845,0.75,0.0625,0.1131370849898476,0.12355,0.0,0.0,0.0,77 +ENSRNA049454974,0.859375,0.12387890112063936,0.875,0.0625,0.14414999403128945,0.1059,0.0,0.0,0.0,88 +ENSRNA049455639,0.15625,0.20863074009907004,0.125,0.125,1.3352367366340483,1.4826,0.0,0.0,0.375,22 +ENSRNA049455690,0.328125,0.34028283928856257,0.3125,0.3125,1.0370524625937145,1.4826,0.0,0.0,0.375,33 diff --git a/tests/test_data/compute_stability_scores/input/genorm.m_measures.csv b/tests/test_data/compute_stability_scores/input/genorm.m_measures.csv new file mode 100644 index 00000000..9a7ab4e1 --- /dev/null +++ b/tests/test_data/compute_stability_scores/input/genorm.m_measures.csv @@ -0,0 +1,10 @@ +ensembl_gene_id,genorm_m_measure +ENSRNA049454747,0.16034699963469335 +ENSRNA049454887,0.525024672172669794 +ENSRNA049454931,0.264017707597323344 +ENSRNA049454947,0.037074358179388235 +ENSRNA049454955,0.65294154739420848 +ENSRNA049454963,0.213698246698642331 +ENSRNA049454974,0.16807095772646336 +ENSRNA049455639,0.02698654413301954 +ENSRNA049455690,0.57785261216485885 diff --git a/tests/test_data/compute_stability_scores/input/stability_values.normfinder.csv b/tests/test_data/compute_stability_scores/input/stability_values.normfinder.csv new file mode 100644 index 00000000..c9a22636 --- /dev/null +++ b/tests/test_data/compute_stability_scores/input/stability_values.normfinder.csv @@ -0,0 +1,10 @@ +ensembl_gene_id,normfinder_stability_value +ENSRNA049454747,0.036034699963469335 +ENSRNA049454887,0.05024672172669794 +ENSRNA049454931,0.014017707597323344 +ENSRNA049454947,0.037074358179388235 +ENSRNA049454955,0.03294154739420848 +ENSRNA049454963,0.03698246698642331 +ENSRNA049454974,0.06807095772646336 +ENSRNA049455639,0.02698654413301954 +ENSRNA049455690,0.07785261216485885 diff --git a/tests/test_data/compute_stability_scores/input/stats_all_genes.csv b/tests/test_data/compute_stability_scores/input/stats_all_genes.csv new file mode 100644 index 00000000..cda6fe60 --- /dev/null +++ b/tests/test_data/compute_stability_scores/input/stats_all_genes.csv @@ -0,0 +1,10 @@ +ensembl_gene_id,mean,standard_deviation,median,median_absolute_deviation,coefficient_of_variation,robust_coefficient_of_variation_median,ratio_nulls_in_all_samples,ratio_nulls_in_valid_samples,ratio_zeros,expression_level_quantile_interval +ENSRNA049454747,0.9375,0.11572751247156893,1.0,0.0,0.12344267996967352,0.0,0.0,0.0,0.0,99 +ENSRNA049454887,0.140625,0.15580293184477811,0.125,0.125,1.1079319597850887,1.4826,0.0,0.0,0.5,11 +ENSRNA049454931,0.4453125,0.12246309575308217,0.4375,0.0625,0.2750048466034126,0.2118,0.0,0.0,0.0,66 +ENSRNA049454947,0.3984375,0.1887975933374152,0.375,0.125,0.4738449401409636,0.4942,0.0,0.0,0.0,44 +ENSRNA049454955,0.421875,0.18525441001112883,0.4375,0.15625,0.4391215644708239,0.5295,0.0,0.0,0.0,55 +ENSRNA049454963,0.78125,0.08838834764831845,0.75,0.0625,0.1131370849898476,0.12355,0.0,0.0,0.0,77 +ENSRNA049454974,0.859375,0.12387890112063936,0.875,0.0625,0.14414999403128945,0.1059,0.0,0.0,0.0,88 +ENSRNA049455639,0.15625,0.20863074009907004,0.125,0.125,1.3352367366340483,1.4826,0.0,0.0,0.375,22 +ENSRNA049455690,0.328125,0.34028283928856257,0.3125,0.3125,1.0370524625937145,1.4826,0.0,0.0,0.375,33 diff --git a/tests/test_data/dataset_statistics/input/count2.raw.cpm.quant_norm.parquet b/tests/test_data/dataset_statistics/input/count2.raw.cpm.quant_norm.parquet new file mode 100644 index 0000000000000000000000000000000000000000..5e09a87aec81da6dd569c7e18581bad5002cd4cd GIT binary patch literal 6547 zcmc&(O>7fK6rLC-1R5YsSl5Vz=3O9jy+XX#i2qy^uVD)J)$ZH4pib$_0Ut_o3#@=i3w?AWtH8X zH*enizIku<%_Nx zi=$*P;BjAJ2?+!P(B^rOC03sY6xK=arBO1(`#b@Z0xpIydSa0L4)+VV zpToTY_sbK5XC7Y#J9=$rVMmW}y}H_Jk-Ii>ZkJvdv@4gpeay`W%n5__{x&Xu06y{o z4#?>YBH{J;D)1O%#M%BxPSTb`1xlraKYL36en~kef44n?zprfXL3gyr63Y^MS!=Umvg9*T;e6<;g#0 zUb?YpF+eu0RoXiiU6b-0R!93s)Bdq+^d>3MogWJ<(+wc<8^ofuL|b1hU7`n)5uf>` zI3DrDK2TPdwYHM#SZrTg*e6HWUoB(%C=WZ(-hCYuN1?vA z*gm(g&-&R7Yww^uty$f-up39&R|CC+^0q^CW#^^w4{keGp$}Tll5Au(=ft?vkn?11k>Llb*?qtc6wRTwOnSgv(KcpSg98g&RI2dwU99xRP)99rlO#%XlezS z<+X~Okt;F`zwOiv7_J*xw7qpRc;4^4(?+I~1q|+}wdkya(ryF(7e9>Noe7}XoU^l4 z(RV_(Yqnu7rJCM=@?B?2 zHN64#bp@rG-hfVa1*Mwafcm?FQq2K?(i&FM*dIl zpsSwPvTDpjpHOScYwDEbldi3nrKy=lCKRuspV_24gZdN~C}$GxYLYXt)5%TDYAWH* z;aJ6pmz62ROBd6L_0=S|3vMU~+=GpzC&sHG2Bj}#H(^5x|W7HV*DUB6*Zh&%7k+@ zn5Pfsxt4<83sRnWQ=f>tQi<4dCN#a7r=^8j7IL~7U&)9rD@n1Oj{DcUaa6frMGdXz zl8tHVSKTGv>HPXyDml+Z&3Nt-?{rz=g0ASwf8ti(o11i4+!}jxn>{RUuD!XL`8b4} zy7%U0uJc3S#_i25byzv|?9EL*ENjQ1=3L+(V$=mF2`tJ(Zl{ z&3#Pdp&#H-PGxRy)-v@@gi>5^Rsm4#)H7t zR*&{@g*WT97eyfwc@GZ_xV4w|XYZl*H@56O`?Ke$y%?c~7R0|c2hBI<@22+CTlRDN zv!{U Date: Sun, 9 Nov 2025 08:57:49 +0100 Subject: [PATCH 145/258] allow tsv file as input dataset; modify id mapping script to allow that --- assets/multiqc_config.yml | 2 +- assets/schema_datasets.json | 4 +- bin/map_ids_to_ensembl.py | 23 +- conf/test_dataset_only.config | 24 + conf/test_full.config | 2 +- conf/test_local_and_downloaded.config | 33 - conf/test_one_rnaseq_one_microarray.config | 24 - nextflow.config | 3 +- tests/default.nf.test | 74 +- tests/default.nf.test.snap | 2527 ++++++++++++++++- .../local/aggregate_results/main.nf.test.snap | 100 - .../local/clean_count_data/main.nf.test | 3 +- .../local/idmapping/gprofiler/main.nf.test | 29 + .../idmapping/gprofiler/main.nf.test.snap | 60 + .../main.nf.test.snap | 26 +- .../idmapping/base/counts.ensembl_ids.csv | 2 +- .../idmapping/tsv/counts.ensembl_ids.tsv | 4 + tests/test_data/idmapping/tsv/mapping.tsv | 4 + tests/test_data/idmapping/tsv/metadata.tsv | 4 + tests/test_data/input_datasets/input_big.yaml | 4 + tests/test_data/input_datasets/mapping.csv | 10 + tests/test_data/input_datasets/metadata.csv | 10 + .../input_datasets/microarray.normalised.csv | 2 +- tests/test_data/input_datasets/rnaseq.raw.csv | 2 +- .../input_datasets/rnaseq_big.design.csv | 7 + 25 files changed, 2663 insertions(+), 320 deletions(-) create mode 100644 conf/test_dataset_only.config delete mode 100644 conf/test_local_and_downloaded.config delete mode 100644 conf/test_one_rnaseq_one_microarray.config delete mode 100644 tests/modules/local/aggregate_results/main.nf.test.snap create mode 100644 tests/test_data/idmapping/tsv/counts.ensembl_ids.tsv create mode 100644 tests/test_data/idmapping/tsv/mapping.tsv create mode 100644 tests/test_data/idmapping/tsv/metadata.tsv create mode 100644 tests/test_data/input_datasets/input_big.yaml create mode 100644 tests/test_data/input_datasets/mapping.csv create mode 100644 tests/test_data/input_datasets/metadata.csv create mode 100644 tests/test_data/input_datasets/rnaseq_big.design.csv diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index 0f839b70..e8a971c6 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -308,7 +308,7 @@ custom_data: Ratio of Microarray samples in which the gene has a zero value. expression_distributions_top_stable_genes: - section_name: "Distribution of the normalised expression of the most stable genes (ranked by stability score)" + section_name: "Count distributions" file_format: "csv" pconfig: sort_samples: false diff --git a/assets/schema_datasets.json b/assets/schema_datasets.json index 3f43549d..b9416629 100644 --- a/assets/schema_datasets.json +++ b/assets/schema_datasets.json @@ -11,14 +11,14 @@ "type": "string", "format": "file-path", "exists": true, - "pattern": "^\\S+\\.(csv|dat)$", + "pattern": "^\\S+\\.(csv|tsv|dat)$", "errorMessage": "You must provide a count dataset file" }, "design": { "type": "string", "format": "file-path", "exists": true, - "pattern": "^\\S+\\.(csv|dat)$", + "pattern": "^\\S+\\.(csv|tsv|dat)$", "errorMessage": "You must provide a design file", "meta": ["design"] }, diff --git a/bin/map_ids_to_ensembl.py b/bin/map_ids_to_ensembl.py index 864b1e2f..81e1aae4 100755 --- a/bin/map_ids_to_ensembl.py +++ b/bin/map_ids_to_ensembl.py @@ -40,19 +40,26 @@ def parse_args(): ) parser.add_argument( "--custom-mappings", - type=str, + type=Path, dest="custom_mappings", help="Optional file containing custom mappings", ) parser.add_argument( "--custom-metadata", - type=str, + type=Path, dest="custom_metadata", help="Optional file containing custom metadata", ) return parser.parse_args() +def parse_table(file: Path, **kwargs): + if file.suffix == ".csv": + return pd.read_csv(file, header=0, **kwargs) + else: # .tsv + return pd.read_csv(file, header=0, sep="\t", **kwargs) + + ################################################################## # MAIN ################################################################## @@ -72,7 +79,9 @@ def main(): ############################################################# # PARSING FILES ############################################################# - df = pd.read_csv(count_file, header=0, index_col=0) + + df = parse_table(count_file, index_col=0) + df.index.rename(config.ENSEMBL_GENE_ID_COLNAME, inplace=True) if df.empty: msg = "COUNT FILE IS EMPTY" @@ -86,7 +95,7 @@ def main(): custom_mappings_dict = {} if custom_mapping_file: - custom_mapping_df = pd.read_csv(custom_mapping_file) + custom_mapping_df = parse_table(custom_mapping_file) custom_mappings_dict = custom_mapping_df.set_index( config.ORIGINAL_GENE_ID_COLNAME )[config.ENSEMBL_GENE_ID_COLNAME].to_dict() @@ -96,13 +105,12 @@ def main(): ] logger.info(f"Number of genes left to map: {len(gene_ids_left_to_map)}") - gene_metadata_dfs = [] - ############################################################# # QUERYING g:PROFILER SERVER ############################################################# gprofiler_mapping_dict = {} + gene_metadata_dfs = [] try: if gene_ids_left_to_map: @@ -138,7 +146,6 @@ def main(): # renaming gene names to mapped ids using mapping dict df.index = df.index.map(mapping_dict) df.reset_index(inplace=True) - df.rename(columns={"index": config.ENSEMBL_GENE_ID_COLNAME}, inplace=True) # TODO: check is there is another way to avoid duplicate gene names # sometimes different gene names have the same ensembl ID @@ -154,7 +161,7 @@ def main(): # if the user provides custom metadata file if custom_metadata_file: - custom_metadata_df = pd.read_csv(custom_metadata_file) + custom_metadata_df = parse_table(custom_metadata_file) # prepending custom metadata in gene metadata gene_metadata_dfs = [custom_metadata_df] + gene_metadata_dfs diff --git a/conf/test_dataset_only.config b/conf/test_dataset_only.config new file mode 100644 index 00000000..35f517a3 --- /dev/null +++ b/conf/test_dataset_only.config @@ -0,0 +1,24 @@ +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Nextflow config file for running full-size tests +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Defines input files and everything required to run a full size pipeline test. + This tests the capacity of the pipeline to process a full size dataset. + + Use as follows: + nextflow run nf-core/stableexpression -profile test_full, --outdir + +---------------------------------------------------------------------------------------- +*/ + +params { + config_profile_name = 'Full test profile' + config_profile_description = 'Full test dataset to check pipeline function' + + // Input data + species = 'mus_musculus' + skip_fetch_eatlas_accessions = true + skip_fetch_geo_accessions = true + datasets = 'tests/test_data/input_datasets/input_big.yaml' + outdir = "results/test_dataset_only" +} diff --git a/conf/test_full.config b/conf/test_full.config index 6ed4fd55..20b51f29 100644 --- a/conf/test_full.config +++ b/conf/test_full.config @@ -16,6 +16,6 @@ params { config_profile_description = 'Full test dataset to check pipeline function' // Input data - species = 'solanum_tuberosum' + species = 'arabidopsis_lyrata' outdir = "results/test_full" } diff --git a/conf/test_local_and_downloaded.config b/conf/test_local_and_downloaded.config deleted file mode 100644 index d120153f..00000000 --- a/conf/test_local_and_downloaded.config +++ /dev/null @@ -1,33 +0,0 @@ -/* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Nextflow config file for running minimal tests -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Defines input files and everything required to run a fast and simple pipeline test. - It tests the different ways to use the pipeline, with small data - - Use as follows: - nextflow run nf-core/stableexpression -profile test_local_and_downloaded, --outdir - ----------------------------------------------------------------------------------------- -*/ - -process { - resourceLimits = [ - cpus: 4, - memory: '15.GB', - time: '1.h' - ] -} - -params { - config_profile_name = 'Test profile' - config_profile_description = 'Minimal test dataset to check pipeline function' - - // Input data - species = 'solanum tuberosum' - eatlas_accessions = "E-MTAB-7711" - skip_fetch_eatlas_accessions = true - skip_fetch_geo_accessions = true - datasets = "tests/test_data/input_datasets/input.csv" - outdir = "results/test_local_and_downloaded" -} diff --git a/conf/test_one_rnaseq_one_microarray.config b/conf/test_one_rnaseq_one_microarray.config deleted file mode 100644 index fdb8fc92..00000000 --- a/conf/test_one_rnaseq_one_microarray.config +++ /dev/null @@ -1,24 +0,0 @@ -/* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Nextflow config file for running minimal tests -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Defines input files and everything required to run a fast and simple pipeline test. - It tests the different ways to use the pipeline, with small data - - Use as follows: - nextflow run nf-core/stableexpression -profile test_one_rnaseq_one_microarray, --outdir - ----------------------------------------------------------------------------------------- -*/ - -params { - config_profile_name = 'Test dataset custom gene data profile' - config_profile_description = 'Minimal test dataset with custom gene metadata to check pipeline function' - - // Input data - species = 'arabidopsis thaliana' - eatlas_accessions = 'E-GEOD-52806,E-GEOD-21945' - skip_fetch_eatlas_accessions = true - skip_fetch_geo_accessions = true - outdir = "results/test_one_rnaseq_one_microarray" -} diff --git a/nextflow.config b/nextflow.config index a38e0f9c..494a057d 100644 --- a/nextflow.config +++ b/nextflow.config @@ -213,8 +213,7 @@ profiles { test { includeConfig 'conf/test.config' } test_full { includeConfig 'conf/test_full.config' } - test_local_and_downloaded { includeConfig 'conf/test_local_and_downloaded.config' } - test_one_rnaseq_one_microarray { includeConfig 'conf/test_one_rnaseq_one_microarray.config' } + test_dataset_only { includeConfig 'conf/test_dataset_only.config' } local { includeConfig 'conf/local.config' } } // Load nf-core custom profiles from different institutions diff --git a/tests/default.nf.test b/tests/default.nf.test index 1521e78e..6c410219 100644 --- a/tests/default.nf.test +++ b/tests/default.nf.test @@ -33,41 +33,39 @@ nextflow_pipeline { } } - test("-profile test_accessions_only") { + test("-profile test_dataset_only") { when { params { - species = 'beta vulgaris' - accessions_only = true + species = 'mus musculus' + skip_fetch_eatlas_accessions = true + skip_fetch_geo_accessions = true + datasets = 'tests/test_data/input_datasets/input_big.yaml' outdir = "$outputDir" } } then { - // stable_name: All files + folders in ${params.outdir}/ with a stable name def stable_name = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}']) - // stable_path: All files in ${params.outdir}/ with stable content def stable_path = getAllFilesFromDir(params.outdir, ignoreFile: 'tests/.nftignore') assertAll( { assert workflow.success}, { 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_stableexpression_software_mqc_versions.yml"), - // All stable path name, with a relative path stable_name, - // All files with stable contents stable_path ).match() } ) } } - test("-profile test_download_only") { + + test("-profile test_accessions_only") { when { params { species = 'beta vulgaris' - download_only = true + accessions_only = true outdir = "$outputDir" } } @@ -91,26 +89,29 @@ nextflow_pipeline { } } - test("-profile test_one_rnaseq_one_microarray") { + test("-profile test_download_only") { when { params { - species = 'arabidopsis thaliana' - eatlas_accessions = 'E-GEOD-52806,E-GEOD-21945' - skip_fetch_eatlas_accessions = true - skip_fetch_geo_accessions = true + species = 'beta vulgaris' + download_only = true outdir = "$outputDir" } } then { + // stable_name: All files + folders in ${params.outdir}/ with a stable name def stable_name = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}']) + // stable_path: All files in ${params.outdir}/ with stable content def stable_path = getAllFilesFromDir(params.outdir, ignoreFile: 'tests/.nftignore') assertAll( { assert workflow.success}, { 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_stableexpression_software_mqc_versions.yml"), + // All stable path name, with a relative path stable_name, + // All files with stable contents stable_path ).match() } ) @@ -123,6 +124,8 @@ nextflow_pipeline { params { species = 'arabidopsis thaliana' eatlas_accessions = "E-GEOD-51720" + skip_fetch_eatlas_accessions = true + skip_fetch_geo_accessions = true outdir = "results/test_one_accession_low_gene_count" outdir = "$outputDir" } @@ -147,8 +150,9 @@ nextflow_pipeline { when { params { species = 'solanum tuberosum' - keywords = "potato,stress" - eatlas_accessions = "E-MTAB-552" + skip_fetch_eatlas_accessions = true + skip_fetch_geo_accessions = true + eatlas_accessions = "E-MTAB-7711" datasets = "${projectDir}/tests/test_data/input_datasets/input.csv" outdir = "$outputDir" } @@ -168,39 +172,11 @@ nextflow_pipeline { } } - test("-profile test_ignore_errors") { + test("-profile test_run_genorm") { when { params { species = 'beta vulgaris' - keywords = "leaf" - datasets = "${projectDir}/tests/test_data/input_datasets/input.csv" - outdir = "$outputDir" - } - } - - then { - def stable_name = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}']) - def stable_path = getAllFilesFromDir(params.outdir, ignoreFile: 'tests/.nftignore') - assertAll( - { assert workflow.success}, - { assert snapshot( - removeNextflowVersion("$outputDir/pipeline_info/nf_core_stableexpression_software_mqc_versions.yml"), - stable_name, - stable_path - ).match() } - ) - } - } - - test("-profile test_run_normfinder_genorm") { - - when { - params { - species = 'solanum tuberosum' - eatlas_accessions = "E-MTAB-7711" - skip_fetch_eatlas_accessions = true - skip_fetch_geo_accessions = true run_genorm = true outdir = "$outputDir" } @@ -224,8 +200,8 @@ nextflow_pipeline { when { params { - species = 'solanum tuberosum' - keywords = "potato,stress" + species = 'beta vulgaris' + keywords = "leaf" skip_fetch_geo_accessions = true outdir = "$outputDir" } @@ -276,7 +252,7 @@ nextflow_pipeline { when { params { - species = 'solanum_tuberosum' + species = 'arabidopsis_lyrata' outdir = "$outputDir" } } diff --git a/tests/default.nf.test.snap b/tests/default.nf.test.snap index 82c2c944..eda17089 100644 --- a/tests/default.nf.test.snap +++ b/tests/default.nf.test.snap @@ -1,8 +1,432 @@ { + "-profile test_eatlas_only_with_keywords": { + "content": [ + null, + [ + "aggregate_results", + "aggregate_results/all_counts_filtered.parquet", + "aggregate_results/all_genes_summary.csv", + "aggregate_results/top_stable_genes_summary.csv", + "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", + "base_statistics", + "base_statistics/all", + "base_statistics/all/stats_all_genes.csv", + "base_statistics/rnaseq", + "base_statistics/rnaseq/rnaseq.stats_all_genes.csv", + "clean_count_data", + "clean_count_data/cleaned_counts_filtered.parquet", + "compute_stability_scores", + "compute_stability_scores/stats_with_scores.csv", + "dash_app", + "dash_app/app.py", + "dash_app/assets", + "dash_app/assets/style.css", + "dash_app/data", + "dash_app/data/all_counts.parquet", + "dash_app/data/all_genes_summary.csv", + "dash_app/data/whole_design.csv", + "dash_app/file_system_backend", + "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", + "dash_app/spec-file.txt", + "dash_app/src", + "dash_app/src/callbacks", + "dash_app/src/callbacks/__pycache__", + "dash_app/src/callbacks/__pycache__/common.cpython-313.pyc", + "dash_app/src/callbacks/__pycache__/genes.cpython-313.pyc", + "dash_app/src/callbacks/__pycache__/samples.cpython-313.pyc", + "dash_app/src/callbacks/common.py", + "dash_app/src/callbacks/genes.py", + "dash_app/src/callbacks/samples.py", + "dash_app/src/components", + "dash_app/src/components/__pycache__", + "dash_app/src/components/__pycache__/graphs.cpython-313.pyc", + "dash_app/src/components/__pycache__/right_sidebar.cpython-313.pyc", + "dash_app/src/components/__pycache__/stores.cpython-313.pyc", + "dash_app/src/components/__pycache__/tables.cpython-313.pyc", + "dash_app/src/components/__pycache__/tooltips.cpython-313.pyc", + "dash_app/src/components/__pycache__/top.cpython-313.pyc", + "dash_app/src/components/graphs.py", + "dash_app/src/components/icons.py", + "dash_app/src/components/right_sidebar.py", + "dash_app/src/components/settings", + "dash_app/src/components/settings/__pycache__", + "dash_app/src/components/settings/__pycache__/genes.cpython-313.pyc", + "dash_app/src/components/settings/__pycache__/samples.cpython-313.pyc", + "dash_app/src/components/settings/genes.py", + "dash_app/src/components/settings/samples.py", + "dash_app/src/components/stores.py", + "dash_app/src/components/tables.py", + "dash_app/src/components/tooltips.py", + "dash_app/src/components/top.py", + "dash_app/src/utils", + "dash_app/src/utils/__pycache__", + "dash_app/src/utils/__pycache__/config.cpython-313.pyc", + "dash_app/src/utils/__pycache__/data_management.cpython-313.pyc", + "dash_app/src/utils/__pycache__/style.cpython-313.pyc", + "dash_app/src/utils/config.py", + "dash_app/src/utils/data_management.py", + "dash_app/src/utils/style.py", + "dash_app/versions.yml", + "dataset_statistics", + "dataset_statistics/E_GEOD_61690_rnaseq.dataset_stats.csv", + "dataset_statistics/E_GEOD_77826_rnaseq.dataset_stats.csv", + "dataset_statistics/E_MTAB_4251_rnaseq.dataset_stats.csv", + "dataset_statistics/E_MTAB_4301_rnaseq.dataset_stats.csv", + "dataset_statistics/E_MTAB_5038_rnaseq.dataset_stats.csv", + "dataset_statistics/E_MTAB_5215_rnaseq.dataset_stats.csv", + "dataset_statistics/E_MTAB_552_rnaseq.dataset_stats.csv", + "dataset_statistics/E_MTAB_7711_rnaseq.dataset_stats.csv", + "errors", + "expression_atlas", + "expression_atlas/accessions", + "expression_atlas/accessions/accessions.txt", + "expression_atlas/accessions/selected_experiments.metadata.tsv", + "expression_atlas/accessions/species_experiments.metadata.tsv", + "expression_atlas/datasets", + "expression_atlas/datasets/E_GEOD_61690_rnaseq.design.csv", + "expression_atlas/datasets/E_GEOD_61690_rnaseq.rnaseq.raw.counts.csv", + "expression_atlas/datasets/E_GEOD_77826_rnaseq.design.csv", + "expression_atlas/datasets/E_GEOD_77826_rnaseq.rnaseq.raw.counts.csv", + "expression_atlas/datasets/E_MTAB_4251_rnaseq.design.csv", + "expression_atlas/datasets/E_MTAB_4251_rnaseq.rnaseq.raw.counts.csv", + "expression_atlas/datasets/E_MTAB_4301_rnaseq.design.csv", + "expression_atlas/datasets/E_MTAB_4301_rnaseq.rnaseq.raw.counts.csv", + "expression_atlas/datasets/E_MTAB_5038_rnaseq.design.csv", + "expression_atlas/datasets/E_MTAB_5038_rnaseq.rnaseq.raw.counts.csv", + "expression_atlas/datasets/E_MTAB_5215_rnaseq.design.csv", + "expression_atlas/datasets/E_MTAB_5215_rnaseq.rnaseq.raw.counts.csv", + "expression_atlas/datasets/E_MTAB_552_rnaseq.design.csv", + "expression_atlas/datasets/E_MTAB_552_rnaseq.rnaseq.raw.counts.csv", + "expression_atlas/datasets/E_MTAB_7711_rnaseq.design.csv", + "expression_atlas/datasets/E_MTAB_7711_rnaseq.rnaseq.raw.counts.csv", + "geo", + "geo/excluded_geo_accessions.txt", + "get_candidate_genes", + "get_candidate_genes/candidate_counts.parquet", + "idmapping", + "idmapping/datasets", + "idmapping/datasets/E_GEOD_61690_rnaseq.rnaseq.raw.counts.mapping.csv", + "idmapping/datasets/E_GEOD_61690_rnaseq.rnaseq.raw.counts.metadata.csv", + "idmapping/datasets/E_GEOD_61690_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/datasets/E_GEOD_77826_rnaseq.rnaseq.raw.counts.mapping.csv", + "idmapping/datasets/E_GEOD_77826_rnaseq.rnaseq.raw.counts.metadata.csv", + "idmapping/datasets/E_GEOD_77826_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/datasets/E_MTAB_4251_rnaseq.rnaseq.raw.counts.mapping.csv", + "idmapping/datasets/E_MTAB_4251_rnaseq.rnaseq.raw.counts.metadata.csv", + "idmapping/datasets/E_MTAB_4251_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/datasets/E_MTAB_4301_rnaseq.rnaseq.raw.counts.mapping.csv", + "idmapping/datasets/E_MTAB_4301_rnaseq.rnaseq.raw.counts.metadata.csv", + "idmapping/datasets/E_MTAB_4301_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/datasets/E_MTAB_5038_rnaseq.rnaseq.raw.counts.mapping.csv", + "idmapping/datasets/E_MTAB_5038_rnaseq.rnaseq.raw.counts.metadata.csv", + "idmapping/datasets/E_MTAB_5038_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/datasets/E_MTAB_5215_rnaseq.rnaseq.raw.counts.mapping.csv", + "idmapping/datasets/E_MTAB_5215_rnaseq.rnaseq.raw.counts.metadata.csv", + "idmapping/datasets/E_MTAB_5215_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/datasets/E_MTAB_552_rnaseq.rnaseq.raw.counts.mapping.csv", + "idmapping/datasets/E_MTAB_552_rnaseq.rnaseq.raw.counts.metadata.csv", + "idmapping/datasets/E_MTAB_552_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/datasets/E_MTAB_7711_rnaseq.rnaseq.raw.counts.mapping.csv", + "idmapping/datasets/E_MTAB_7711_rnaseq.rnaseq.raw.counts.metadata.csv", + "idmapping/datasets/E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/whole_gene_id_mapping.csv", + "idmapping/whole_gene_metadata.csv", + "merged_datasets", + "merged_datasets/all", + "merged_datasets/all/all_counts.parquet", + "merged_datasets/rnaseq", + "merged_datasets/rnaseq/all_counts.parquet", + "merged_datasets/whole_design.csv", + "multiqc", + "multiqc/multiqc_data", + "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_eatlas_all_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", + "multiqc/multiqc_data/multiqc_gene_statistics.txt", + "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", + "multiqc/multiqc_data/multiqc_software_versions.txt", + "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_plots", + "multiqc/multiqc_plots/pdf", + "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", + "multiqc/multiqc_plots/pdf/gene_statistics.pdf", + "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", + "multiqc/multiqc_plots/png", + "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", + "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", + "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", + "multiqc/multiqc_plots/png/gene_statistics.png", + "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", + "multiqc/multiqc_plots/svg", + "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", + "multiqc/multiqc_plots/svg/gene_statistics.svg", + "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", + "multiqc/multiqc_report.html", + "multiqc/versions.yml", + "normalised", + "normalised/E_GEOD_61690_rnaseq", + "normalised/E_GEOD_61690_rnaseq/normalisation_deseq2", + "normalised/E_GEOD_61690_rnaseq/normalisation_deseq2/E_GEOD_61690_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normalised/E_GEOD_77826_rnaseq", + "normalised/E_GEOD_77826_rnaseq/normalisation_deseq2", + "normalised/E_GEOD_77826_rnaseq/normalisation_deseq2/E_GEOD_77826_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normalised/E_MTAB_4251_rnaseq", + "normalised/E_MTAB_4251_rnaseq/normalisation_deseq2", + "normalised/E_MTAB_4251_rnaseq/normalisation_deseq2/E_MTAB_4251_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normalised/E_MTAB_4301_rnaseq", + "normalised/E_MTAB_4301_rnaseq/normalisation_deseq2", + "normalised/E_MTAB_4301_rnaseq/normalisation_deseq2/E_MTAB_4301_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normalised/E_MTAB_5038_rnaseq", + "normalised/E_MTAB_5038_rnaseq/normalisation_deseq2", + "normalised/E_MTAB_5038_rnaseq/normalisation_deseq2/E_MTAB_5038_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normalised/E_MTAB_5215_rnaseq", + "normalised/E_MTAB_5215_rnaseq/normalisation_deseq2", + "normalised/E_MTAB_5215_rnaseq/normalisation_deseq2/E_MTAB_5215_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normalised/E_MTAB_552_rnaseq", + "normalised/E_MTAB_552_rnaseq/normalisation_deseq2", + "normalised/E_MTAB_552_rnaseq/normalisation_deseq2/E_MTAB_552_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normalised/E_MTAB_7711_rnaseq", + "normalised/E_MTAB_7711_rnaseq/normalisation_deseq2", + "normalised/E_MTAB_7711_rnaseq/normalisation_deseq2/E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "pipeline_info", + "pipeline_info/software_mqc_versions.yml", + "quantile_normalised", + "quantile_normalised/E_GEOD_61690_rnaseq", + "quantile_normalised/E_GEOD_61690_rnaseq/E_GEOD_61690_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", + "quantile_normalised/E_GEOD_77826_rnaseq", + "quantile_normalised/E_GEOD_77826_rnaseq/E_GEOD_77826_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", + "quantile_normalised/E_MTAB_4251_rnaseq", + "quantile_normalised/E_MTAB_4251_rnaseq/E_MTAB_4251_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", + "quantile_normalised/E_MTAB_4301_rnaseq", + "quantile_normalised/E_MTAB_4301_rnaseq/E_MTAB_4301_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", + "quantile_normalised/E_MTAB_5038_rnaseq", + "quantile_normalised/E_MTAB_5038_rnaseq/E_MTAB_5038_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", + "quantile_normalised/E_MTAB_5215_rnaseq", + "quantile_normalised/E_MTAB_5215_rnaseq/E_MTAB_5215_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", + "quantile_normalised/E_MTAB_552_rnaseq", + "quantile_normalised/E_MTAB_552_rnaseq/E_MTAB_552_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", + "quantile_normalised/E_MTAB_7711_rnaseq", + "quantile_normalised/E_MTAB_7711_rnaseq/E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", + "stability_scoring", + "stability_scoring/normfinder", + "stability_scoring/normfinder/stability_values.normfinder.csv", + "warnings" + ], + [ + "all_counts_filtered.parquet:md5,0c137b525500950f567679844f65949d", + "all_genes_summary.csv:md5,c63a5b24e8fc4a17d71ca8c2b9d503e8", + "top_stable_genes_summary.csv:md5,20dbabfbf15ccca9ba5ea0f935299d71", + "top_stable_genes_transposed_counts_filtered.csv:md5,005bd9630f3d6ee27ddaf81a1f6d4e69", + "stats_all_genes.csv:md5,6efef4640852629afe0b445016643032", + "rnaseq.stats_all_genes.csv:md5,c94a361119c2356fd5791c2bdd1bb682", + "cleaned_counts_filtered.parquet:md5,ea8d067bc05c52c5a28b3fc0ba8376c2", + "stats_with_scores.csv:md5,e84c1d840dc02efc686c5bad67ee54cd", + "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", + "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", + "all_counts.parquet:md5,a9964d8e753fc14019a91efc7b7b5d80", + "all_genes_summary.csv:md5,c63a5b24e8fc4a17d71ca8c2b9d503e8", + "whole_design.csv:md5,112168cfd2cc4aad6154fd0aefa7e8fa", + "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", + "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", + "common.cpython-313.pyc:md5,958a2fb58c8a1eb49c1f53be5bde113a", + "genes.cpython-313.pyc:md5,1f940d227f2468334516013ce1848f7c", + "samples.cpython-313.pyc:md5,bb3b1848128474bb1e36882760a6498a", + "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", + "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", + "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", + "graphs.cpython-313.pyc:md5,6e0e3892ca18d05cab7084f480439cda", + "right_sidebar.cpython-313.pyc:md5,20ec462058c57130125f6bbf438ac263", + "stores.cpython-313.pyc:md5,a243e0fa3bb2a1b0abf2ddc868ed233a", + "tables.cpython-313.pyc:md5,7eeb96e1483ef3ba3493d0da4f88ac8b", + "tooltips.cpython-313.pyc:md5,b0cbabb5a65a68425cabe8c138b2f3a7", + "top.cpython-313.pyc:md5,00794b788a7fdccfff4d55d1c2157b76", + "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", + "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", + "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", + "genes.cpython-313.pyc:md5,01dbdd80898244a0f08c192ad96fb211", + "samples.cpython-313.pyc:md5,26a2e6920275a87c9fa17f629345266d", + "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", + "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", + "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", + "tables.py:md5,84d17ad7f43458412e550f74a24560e5", + "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", + "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", + "config.cpython-313.pyc:md5,1268d2fbbae804fb7e9b310d3dabcbf7", + "data_management.cpython-313.pyc:md5,0f9244fa1a171c4ffaccf810adbb749e", + "style.cpython-313.pyc:md5,b705b5dc7b559296968085945b5e110e", + "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", + "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", + "style.py:md5,b9f1207f06464e43c05e6e3912f12731", + "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", + "E_GEOD_61690_rnaseq.dataset_stats.csv:md5,0f8770070155190203b6f75a3459f909", + "E_GEOD_77826_rnaseq.dataset_stats.csv:md5,e27c4f8df94bcf0078cbd6a2d1e7c62a", + "E_MTAB_4251_rnaseq.dataset_stats.csv:md5,ada6698eef0e01eb826499e3dc8d4f0c", + "E_MTAB_4301_rnaseq.dataset_stats.csv:md5,16b789cc315f71e70214a27be5138429", + "E_MTAB_5038_rnaseq.dataset_stats.csv:md5,2938b1557ef677021c956b8a56c2559c", + "E_MTAB_5215_rnaseq.dataset_stats.csv:md5,aed791a4e0083904594d55411fc2beb6", + "E_MTAB_552_rnaseq.dataset_stats.csv:md5,8d4ad8f52274943b9bfa3563f26cc0d7", + "E_MTAB_7711_rnaseq.dataset_stats.csv:md5,fc513b84eee3cc48466d260b8cfec4ac", + "accessions.txt:md5,dad63ac7a1715277fa44567bc40b5872", + "selected_experiments.metadata.tsv:md5,15baa430176fcadf7bf03034fa9e95c6", + "species_experiments.metadata.tsv:md5,15baa430176fcadf7bf03034fa9e95c6", + "E_GEOD_61690_rnaseq.design.csv:md5,ba807caf74e0b55f5c3fe23810a89560", + "E_GEOD_61690_rnaseq.rnaseq.raw.counts.csv:md5,2e3f1a125b3d41d622e2d24447620eb3", + "E_GEOD_77826_rnaseq.design.csv:md5,5aa61df754aa9c6c107b247c642d2e53", + "E_GEOD_77826_rnaseq.rnaseq.raw.counts.csv:md5,85cea79c602a9924d5a4d6b597ef5530", + "E_MTAB_4251_rnaseq.design.csv:md5,4f3ef7b76ca6ed1ec3157295bc4a8d84", + "E_MTAB_4251_rnaseq.rnaseq.raw.counts.csv:md5,5cf27be0e00b93d5d431754ba8058687", + "E_MTAB_4301_rnaseq.design.csv:md5,165eeef7d612c01fd62baae1a3f296ae", + "E_MTAB_4301_rnaseq.rnaseq.raw.counts.csv:md5,1ab49feea238e7b1419937b5037952b5", + "E_MTAB_5038_rnaseq.design.csv:md5,352ed3163d7deef2be35d899418d5ad4", + "E_MTAB_5038_rnaseq.rnaseq.raw.counts.csv:md5,b4acb3d7c39cdb2bd6cef6c9314c5b2a", + "E_MTAB_5215_rnaseq.design.csv:md5,2741dcd5b45bacce865db632f626a273", + "E_MTAB_5215_rnaseq.rnaseq.raw.counts.csv:md5,273704bdf762c342271b33958a84d1e7", + "E_MTAB_552_rnaseq.design.csv:md5,b81490696f638e90c1cf14236bb0c08c", + "E_MTAB_552_rnaseq.rnaseq.raw.counts.csv:md5,830f50b60b17b62f9ca2f6a163a2879f", + "E_MTAB_7711_rnaseq.design.csv:md5,3e7748b54a0c25c008d9bd2ddbf1bf00", + "E_MTAB_7711_rnaseq.rnaseq.raw.counts.csv:md5,3c02cf432c29d3751c978439539df388", + "excluded_geo_accessions.txt:md5,3b29ba0fe90a301ef71639760fc8e5a9", + "candidate_counts.parquet:md5,93a3bcbeabf1afa3cb0e7fe75cb0000c", + "E_GEOD_61690_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", + "E_GEOD_61690_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", + "E_GEOD_61690_rnaseq.rnaseq.raw.counts.renamed.csv:md5,ccb6efdfb49bc4057618a2a10ec880b7", + "E_GEOD_77826_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", + "E_GEOD_77826_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", + "E_GEOD_77826_rnaseq.rnaseq.raw.counts.renamed.csv:md5,9f933a357deca364f73ca96aa6fa8c5a", + "E_MTAB_4251_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", + "E_MTAB_4251_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", + "E_MTAB_4251_rnaseq.rnaseq.raw.counts.renamed.csv:md5,74aa87fe4102ae828b1bfb0dc7e2bb3a", + "E_MTAB_4301_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", + "E_MTAB_4301_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", + "E_MTAB_4301_rnaseq.rnaseq.raw.counts.renamed.csv:md5,d7a9d4f33e612a803d0032cf1a8b6677", + "E_MTAB_5038_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", + "E_MTAB_5038_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", + "E_MTAB_5038_rnaseq.rnaseq.raw.counts.renamed.csv:md5,07fb1b79dff4a159c9814225dd947d30", + "E_MTAB_5215_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", + "E_MTAB_5215_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", + "E_MTAB_5215_rnaseq.rnaseq.raw.counts.renamed.csv:md5,f3a308689af31ba086fc5f341f0589a4", + "E_MTAB_552_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", + "E_MTAB_552_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", + "E_MTAB_552_rnaseq.rnaseq.raw.counts.renamed.csv:md5,449f7c179fc675784f15f58735196644", + "E_MTAB_7711_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", + "E_MTAB_7711_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", + "E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.csv:md5,3856796c675c7ee3bb6975185f1b869b", + "whole_gene_id_mapping.csv:md5,87c58803a087a768eff2403b40868614", + "whole_gene_metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", + "all_counts.parquet:md5,a9964d8e753fc14019a91efc7b7b5d80", + "all_counts.parquet:md5,1d9b59f7d379961c4939a1bdf6b0deca", + "whole_design.csv:md5,112168cfd2cc4aad6154fd0aefa7e8fa", + "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", + "multiqc_eatlas_all_experiments_metadata.txt:md5,a2b6cf46faf4b8c7dcdae06e17b35432", + "multiqc_eatlas_selected_experiments_metadata.txt:md5,a2b6cf46faf4b8c7dcdae06e17b35432", + "multiqc_expression_distributions_top_stable_genes.txt:md5,4e75993c1e1af48e9cfcb036e4aa517e", + "multiqc_gene_statistics.txt:md5,74568ad71d674619779988bdf43b4ec7", + "multiqc_ranked_top_stable_genes_summary.txt:md5,131c3632500353311f9508dd893ad681", + "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030", + "E_GEOD_61690_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,4bc05ef9eff93062591fc37ae93b830d", + "E_GEOD_77826_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,0e4f0b0a0276f3a835d26d4b518296f2", + "E_MTAB_4251_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,c9d0a14d809fe994abb1280ee7c00612", + "E_MTAB_4301_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,cdd0db06fdcfb4261853a63b5527c4a1", + "E_MTAB_5038_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,7f72ebf195c0dfb26d13c09157ea1914", + "E_MTAB_5215_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,e427282767b3833f899b1a03bc40edc2", + "E_MTAB_552_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,26e7b283f3c901cc417c1fe0e6b9d555", + "E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,f7374c86950c01bc6a6b8d8fd415cc8b", + "E_GEOD_61690_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,31eef9ce4f886a9368a6d9b0d9d8ac24", + "E_GEOD_77826_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,8270dee3af5c4704ece1bbe2e446d7a2", + "E_MTAB_4251_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,c064b8e2023ef043a0d7d210701c7a88", + "E_MTAB_4301_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,63dc1f0932ef000446ed2e9d553fd0d1", + "E_MTAB_5038_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,bd68b4e0e6120301aedd3885210407cd", + "E_MTAB_5215_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,e4f7631cde43c1b876b0f08a8fe0e3c1", + "E_MTAB_552_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,f5432d1b7b1185b08ee899bd620d73f8", + "E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,ac4c45230db9a59247c51063db68eef3", + "stability_values.normfinder.csv:md5,d2cec0958cad90bfaa30795ea3e0d5cc" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-08T17:05:13.978857392" + }, "-profile test": { "content": [ null, [ + "aggregate_results", + "aggregate_results/all_counts_filtered.parquet", + "aggregate_results/all_genes_summary.csv", + "aggregate_results/top_stable_genes_summary.csv", + "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", + "base_statistics", + "base_statistics/all", + "base_statistics/all/stats_all_genes.csv", + "base_statistics/rnaseq", + "base_statistics/rnaseq/rnaseq.stats_all_genes.csv", + "clean_count_data", + "clean_count_data/cleaned_counts_filtered.parquet", + "compute_stability_scores", + "compute_stability_scores/stats_with_scores.csv", + "dash_app", + "dash_app/app.py", + "dash_app/assets", + "dash_app/assets/style.css", + "dash_app/data", + "dash_app/data/all_counts.parquet", + "dash_app/data/all_genes_summary.csv", + "dash_app/data/whole_design.csv", + "dash_app/file_system_backend", + "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", + "dash_app/spec-file.txt", + "dash_app/src", + "dash_app/src/callbacks", + "dash_app/src/callbacks/__pycache__", + "dash_app/src/callbacks/__pycache__/common.cpython-313.pyc", + "dash_app/src/callbacks/__pycache__/genes.cpython-313.pyc", + "dash_app/src/callbacks/__pycache__/samples.cpython-313.pyc", + "dash_app/src/callbacks/common.py", + "dash_app/src/callbacks/genes.py", + "dash_app/src/callbacks/samples.py", + "dash_app/src/components", + "dash_app/src/components/__pycache__", + "dash_app/src/components/__pycache__/graphs.cpython-313.pyc", + "dash_app/src/components/__pycache__/right_sidebar.cpython-313.pyc", + "dash_app/src/components/__pycache__/stores.cpython-313.pyc", + "dash_app/src/components/__pycache__/tables.cpython-313.pyc", + "dash_app/src/components/__pycache__/tooltips.cpython-313.pyc", + "dash_app/src/components/__pycache__/top.cpython-313.pyc", + "dash_app/src/components/graphs.py", + "dash_app/src/components/icons.py", + "dash_app/src/components/right_sidebar.py", + "dash_app/src/components/settings", + "dash_app/src/components/settings/__pycache__", + "dash_app/src/components/settings/__pycache__/genes.cpython-313.pyc", + "dash_app/src/components/settings/__pycache__/samples.cpython-313.pyc", + "dash_app/src/components/settings/genes.py", + "dash_app/src/components/settings/samples.py", + "dash_app/src/components/stores.py", + "dash_app/src/components/tables.py", + "dash_app/src/components/tooltips.py", + "dash_app/src/components/top.py", + "dash_app/src/utils", + "dash_app/src/utils/__pycache__", + "dash_app/src/utils/__pycache__/config.cpython-313.pyc", + "dash_app/src/utils/__pycache__/data_management.cpython-313.pyc", + "dash_app/src/utils/__pycache__/style.cpython-313.pyc", + "dash_app/src/utils/config.py", + "dash_app/src/utils/data_management.py", + "dash_app/src/utils/style.py", + "dash_app/versions.yml", + "dataset_statistics", + "dataset_statistics/E_MTAB_8187_rnaseq.dataset_stats.csv", "errors", "expression_atlas", "expression_atlas/accessions", @@ -13,27 +437,1648 @@ "expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", "expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", "geo", + "geo/accessions", + "geo/accessions/geo_all_datasets.metadata.tsv", + "geo/accessions/geo_rejected_datasets.metadata.tsv", + "get_candidate_genes", + "get_candidate_genes/candidate_counts.parquet", "idmapping", + "idmapping/datasets", + "idmapping/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.mapping.csv", + "idmapping/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.metadata.csv", + "idmapping/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/whole_gene_id_mapping.csv", + "idmapping/whole_gene_metadata.csv", "merged_datasets", + "merged_datasets/all", + "merged_datasets/all/all_counts.parquet", + "merged_datasets/rnaseq", + "merged_datasets/rnaseq/all_counts.parquet", + "merged_datasets/whole_design.csv", + "multiqc", + "multiqc/multiqc_data", + "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_eatlas_all_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", + "multiqc/multiqc_data/multiqc_gene_statistics.txt", + "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_rejected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", + "multiqc/multiqc_data/multiqc_software_versions.txt", + "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_plots", + "multiqc/multiqc_plots/pdf", + "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", + "multiqc/multiqc_plots/pdf/gene_statistics.pdf", + "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_rejected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", + "multiqc/multiqc_plots/png", + "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", + "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", + "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", + "multiqc/multiqc_plots/png/gene_statistics.png", + "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_rejected_experiments_metadata.png", + "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", + "multiqc/multiqc_plots/svg", + "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", + "multiqc/multiqc_plots/svg/gene_statistics.svg", + "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_rejected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", + "multiqc/multiqc_report.html", + "multiqc/versions.yml", + "normalised", + "normalised/E_MTAB_8187_rnaseq", + "normalised/E_MTAB_8187_rnaseq/normalisation_deseq2", + "normalised/E_MTAB_8187_rnaseq/normalisation_deseq2/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "pipeline_info", + "pipeline_info/software_mqc_versions.yml", + "quantile_normalised", + "quantile_normalised/E_MTAB_8187_rnaseq", + "quantile_normalised/E_MTAB_8187_rnaseq/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", + "stability_scoring", + "stability_scoring/normfinder", + "stability_scoring/normfinder/stability_values.normfinder.csv", + "warnings" + ], + [ + "all_counts_filtered.parquet:md5,d66efb71c32e755405c5be7a5a31f66b", + "all_genes_summary.csv:md5,09ce07d606691ea26f5874ff1151829d", + "top_stable_genes_summary.csv:md5,8aac1b65ece21128ee9fbf621ccffbb2", + "top_stable_genes_transposed_counts_filtered.csv:md5,4f6efc781edc2022d5d0aac665f8a553", + "stats_all_genes.csv:md5,9e3ff278dab74d6903da2f0963332626", + "rnaseq.stats_all_genes.csv:md5,13da83b1a202966be37b5d46e1a73acd", + "cleaned_counts_filtered.parquet:md5,2bb5f895eeba97d72a079987ff707434", + "stats_with_scores.csv:md5,a3475ec54e6bc54525be0274d31ac215", + "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", + "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", + "all_counts.parquet:md5,54e3036081c965bbf62134589c8ab233", + "all_genes_summary.csv:md5,09ce07d606691ea26f5874ff1151829d", + "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", + "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", + "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", + "common.cpython-313.pyc:md5,49a00af9f5e82077e5e9adbbb76302a3", + "genes.cpython-313.pyc:md5,03ebd17927065dc8529af62a12777415", + "samples.cpython-313.pyc:md5,bc843403cf03611f3f9f17d493cc9ab7", + "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", + "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", + "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", + "graphs.cpython-313.pyc:md5,f660df258b1b3d3e75a3d470387a322a", + "right_sidebar.cpython-313.pyc:md5,8c2e303516678c1697599797add55b8f", + "stores.cpython-313.pyc:md5,7aba77cce4937589d40329006134b3f9", + "tables.cpython-313.pyc:md5,1bb516368922912fd118d02d609dd33f", + "tooltips.cpython-313.pyc:md5,c2c233eb5535a39b6646ffafcedd2569", + "top.cpython-313.pyc:md5,47075a146bfbb405377c75f0663f5dc7", + "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", + "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", + "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", + "genes.cpython-313.pyc:md5,51c18544b1887ba5bdfdff8cf8f365cd", + "samples.cpython-313.pyc:md5,244c53655495c130f7ff37c2b795c13f", + "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", + "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", + "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", + "tables.py:md5,84d17ad7f43458412e550f74a24560e5", + "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", + "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", + "config.cpython-313.pyc:md5,0d26637d4326ec2e5e7a0314ae13b8d5", + "data_management.cpython-313.pyc:md5,4333c9f3294879374fbccfc575a94f7d", + "style.cpython-313.pyc:md5,861432da7e3a71fae86de3f200965048", + "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", + "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", + "style.py:md5,b9f1207f06464e43c05e6e3912f12731", + "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", + "E_MTAB_8187_rnaseq.dataset_stats.csv:md5,efad119f2f4b3d777a05f0bfe2070411", + "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", + "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", + "geo_all_datasets.metadata.tsv:md5,f561d3c842808e002a4d8809c1ed848f", + "geo_rejected_datasets.metadata.tsv:md5,9fca49d7ebe56660430411a09a3b50e0", + "candidate_counts.parquet:md5,25cfcc61693a546f5576695d01767a06", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.mapping.csv:md5,d95581c2b10beed3ba4a4556c1afc44f", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.metadata.csv:md5,faf9789296e5578fd31efee8d0c40ca9", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv:md5,f8889bf65500b67c37b7b7549df57285", + "whole_gene_id_mapping.csv:md5,d95581c2b10beed3ba4a4556c1afc44f", + "whole_gene_metadata.csv:md5,faf9789296e5578fd31efee8d0c40ca9", + "all_counts.parquet:md5,54e3036081c965bbf62134589c8ab233", + "all_counts.parquet:md5,54e3036081c965bbf62134589c8ab233", + "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", + "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", + "multiqc_eatlas_all_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", + "multiqc_eatlas_selected_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", + "multiqc_expression_distributions_top_stable_genes.txt:md5,443d0c694839b2948f7a1757fab5593e", + "multiqc_gene_statistics.txt:md5,26d99a1e06b450e651677cb22ec41ee0", + "multiqc_geo_all_experiments_metadata.txt:md5,e17accfafbd2fdfcb2710b23ef7e55cf", + "multiqc_geo_rejected_experiments_metadata.txt:md5,633627930268074fa7d81fc8efa7f4d6", + "multiqc_ranked_top_stable_genes_summary.txt:md5,9b5950b2ffdb53b8aee4bf81ec37344e", + "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,285ad46e7f5da70041815ecbcd9b5601", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,f03095b91c3e2a9c5e39bba1ad2b3a45", + "stability_values.normfinder.csv:md5,4996024e5b9f99e7a755aa17e9a20ca8" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-08T16:53:41.161206065" + }, + "-profile test_accessions_only": { + "content": [ + null, + [ + "errors", + "expression_atlas", + "expression_atlas/accessions", + "expression_atlas/accessions/accessions.txt", + "expression_atlas/accessions/selected_experiments.metadata.tsv", + "expression_atlas/accessions/species_experiments.metadata.tsv", + "geo", + "geo/accessions", + "geo/accessions/accessions.tsv", + "geo/accessions/geo_all_datasets.metadata.tsv", + "geo/accessions/geo_rejected_datasets.metadata.tsv", + "geo/accessions/geo_selected_datasets.metadata.tsv", + "multiqc", + "multiqc/multiqc_data", + "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_eatlas_all_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_rejected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_selected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_software_versions.txt", + "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_plots", + "multiqc/multiqc_plots/pdf", + "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_rejected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_selected_experiments_metadata.pdf", + "multiqc/multiqc_plots/png", + "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", + "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_rejected_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_selected_experiments_metadata.png", + "multiqc/multiqc_plots/svg", + "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_rejected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_selected_experiments_metadata.svg", + "multiqc/multiqc_report.html", + "multiqc/versions.yml", + "pipeline_info", + "pipeline_info/software_mqc_versions.yml", + "warnings" + ], + [ + "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", + "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "accessions.tsv:md5,095fb7f3a666b4382d4ba7296053451b", + "geo_all_datasets.metadata.tsv:md5,d77cabbca17e0502e562e2f85632cd26", + "geo_rejected_datasets.metadata.tsv:md5,cf907a70f381df40e044186db1a320a2", + "geo_selected_datasets.metadata.tsv:md5,d77cabbca17e0502e562e2f85632cd26", + "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", + "multiqc_eatlas_all_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", + "multiqc_eatlas_selected_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", + "multiqc_geo_all_experiments_metadata.txt:md5,67a00882973fd2702e4ea1fef3e84c08", + "multiqc_geo_rejected_experiments_metadata.txt:md5,ea6234d7e4c463fc249aebdf91788df8", + "multiqc_geo_selected_experiments_metadata.txt:md5,67a00882973fd2702e4ea1fef3e84c08", + "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-08T16:54:05.938565374" + }, + "-profile test_one_accession_low_gene_count": { + "content": [ + null, + [ + "aggregate_results", + "aggregate_results/all_counts_filtered.parquet", + "aggregate_results/all_genes_summary.csv", + "aggregate_results/top_stable_genes_summary.csv", + "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", + "base_statistics", + "base_statistics/all", + "base_statistics/all/stats_all_genes.csv", + "base_statistics/rnaseq", + "base_statistics/rnaseq/rnaseq.stats_all_genes.csv", + "clean_count_data", + "clean_count_data/cleaned_counts_filtered.parquet", + "compute_stability_scores", + "compute_stability_scores/stats_with_scores.csv", + "dash_app", + "dash_app/app.py", + "dash_app/assets", + "dash_app/assets/style.css", + "dash_app/data", + "dash_app/data/all_counts.parquet", + "dash_app/data/all_genes_summary.csv", + "dash_app/data/whole_design.csv", + "dash_app/file_system_backend", + "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", + "dash_app/spec-file.txt", + "dash_app/src", + "dash_app/src/callbacks", + "dash_app/src/callbacks/__pycache__", + "dash_app/src/callbacks/__pycache__/common.cpython-313.pyc", + "dash_app/src/callbacks/__pycache__/genes.cpython-313.pyc", + "dash_app/src/callbacks/__pycache__/samples.cpython-313.pyc", + "dash_app/src/callbacks/common.py", + "dash_app/src/callbacks/genes.py", + "dash_app/src/callbacks/samples.py", + "dash_app/src/components", + "dash_app/src/components/__pycache__", + "dash_app/src/components/__pycache__/graphs.cpython-313.pyc", + "dash_app/src/components/__pycache__/right_sidebar.cpython-313.pyc", + "dash_app/src/components/__pycache__/stores.cpython-313.pyc", + "dash_app/src/components/__pycache__/tables.cpython-313.pyc", + "dash_app/src/components/__pycache__/tooltips.cpython-313.pyc", + "dash_app/src/components/__pycache__/top.cpython-313.pyc", + "dash_app/src/components/graphs.py", + "dash_app/src/components/icons.py", + "dash_app/src/components/right_sidebar.py", + "dash_app/src/components/settings", + "dash_app/src/components/settings/__pycache__", + "dash_app/src/components/settings/__pycache__/genes.cpython-313.pyc", + "dash_app/src/components/settings/__pycache__/samples.cpython-313.pyc", + "dash_app/src/components/settings/genes.py", + "dash_app/src/components/settings/samples.py", + "dash_app/src/components/stores.py", + "dash_app/src/components/tables.py", + "dash_app/src/components/tooltips.py", + "dash_app/src/components/top.py", + "dash_app/src/utils", + "dash_app/src/utils/__pycache__", + "dash_app/src/utils/__pycache__/config.cpython-313.pyc", + "dash_app/src/utils/__pycache__/data_management.cpython-313.pyc", + "dash_app/src/utils/__pycache__/style.cpython-313.pyc", + "dash_app/src/utils/config.py", + "dash_app/src/utils/data_management.py", + "dash_app/src/utils/style.py", + "dash_app/versions.yml", + "dataset_statistics", + "dataset_statistics/E_GEOD_51720_rnaseq.dataset_stats.csv", + "errors", + "expression_atlas", + "expression_atlas/datasets", + "expression_atlas/datasets/E_GEOD_51720_rnaseq.design.csv", + "expression_atlas/datasets/E_GEOD_51720_rnaseq.rnaseq.raw.counts.csv", + "geo", + "geo/excluded_geo_accessions.txt", + "get_candidate_genes", + "get_candidate_genes/candidate_counts.parquet", + "idmapping", + "idmapping/datasets", + "idmapping/datasets/E_GEOD_51720_rnaseq.rnaseq.raw.counts.mapping.csv", + "idmapping/datasets/E_GEOD_51720_rnaseq.rnaseq.raw.counts.metadata.csv", + "idmapping/datasets/E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/whole_gene_id_mapping.csv", + "idmapping/whole_gene_metadata.csv", + "merged_datasets", + "merged_datasets/all", + "merged_datasets/all/all_counts.parquet", + "merged_datasets/rnaseq", + "merged_datasets/rnaseq/all_counts.parquet", + "merged_datasets/whole_design.csv", + "multiqc", + "multiqc/multiqc_data", + "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_expression_distributions_top_stable_genes.txt", + "multiqc/multiqc_data/multiqc_gene_statistics.txt", + "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", + "multiqc/multiqc_data/multiqc_software_versions.txt", + "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_plots", + "multiqc/multiqc_plots/pdf", + "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", + "multiqc/multiqc_plots/pdf/gene_statistics.pdf", + "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", + "multiqc/multiqc_plots/png", + "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", + "multiqc/multiqc_plots/png/gene_statistics.png", + "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", + "multiqc/multiqc_plots/svg", + "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", + "multiqc/multiqc_plots/svg/gene_statistics.svg", + "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", + "multiqc/multiqc_report.html", + "multiqc/versions.yml", + "normalised", + "normalised/E_GEOD_51720_rnaseq", + "normalised/E_GEOD_51720_rnaseq/normalisation_deseq2", + "normalised/E_GEOD_51720_rnaseq/normalisation_deseq2/E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "pipeline_info", + "pipeline_info/software_mqc_versions.yml", + "quantile_normalised", + "quantile_normalised/E_GEOD_51720_rnaseq", + "quantile_normalised/E_GEOD_51720_rnaseq/E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", + "stability_scoring", + "stability_scoring/normfinder", + "stability_scoring/normfinder/stability_values.normfinder.csv", + "warnings" + ], + [ + "all_counts_filtered.parquet:md5,0eed740e271b9838212fddbd91700198", + "all_genes_summary.csv:md5,b2858858c2db1fc583d2aa91b82fcfb6", + "top_stable_genes_summary.csv:md5,e462975f7e21414420b17e6fc98be70d", + "top_stable_genes_transposed_counts_filtered.csv:md5,34befd9532a2055c22cd82d594b33efd", + "stats_all_genes.csv:md5,28d5526c41e39a26836c862b1d1d96b6", + "rnaseq.stats_all_genes.csv:md5,2c01fc90ce64a89b9c508dc6ef916501", + "cleaned_counts_filtered.parquet:md5,5cb41caa6a4f0a2bbd2c2bc44c0bd4a7", + "stats_with_scores.csv:md5,5ae77ecc6e76e74bde5fc58698816e60", + "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", + "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", + "all_counts.parquet:md5,c2c4a4eea8c2091bd4969d34fd7ff0e1", + "all_genes_summary.csv:md5,b2858858c2db1fc583d2aa91b82fcfb6", + "whole_design.csv:md5,d3aa542c4ad07d0051a84482fe6cd81c", + "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", + "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", + "common.cpython-313.pyc:md5,31ba90c5decc28d3c8db90fc80e69802", + "genes.cpython-313.pyc:md5,701e8f349cf8468b099ca838b1bcbb22", + "samples.cpython-313.pyc:md5,92d9f02865116de32ba5833afe509160", + "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", + "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", + "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", + "graphs.cpython-313.pyc:md5,f594bc74529100a0e3bab48f08ee88e6", + "right_sidebar.cpython-313.pyc:md5,4d9f40f324acef2ff3a1c8d532936ea8", + "stores.cpython-313.pyc:md5,787f503dbdccdd8899ec51c56c9f3eaf", + "tables.cpython-313.pyc:md5,d96e0875cccaa47043e1a230479bfdf8", + "tooltips.cpython-313.pyc:md5,fde0d1296c024c75a78dcb9a1c0d2f41", + "top.cpython-313.pyc:md5,246b2053230d2973c86f3a51a0c0280e", + "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", + "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", + "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", + "genes.cpython-313.pyc:md5,8c67ccad315fde815aacf702177d7e84", + "samples.cpython-313.pyc:md5,d55185e3ef9dfc2f18e44521b05f8275", + "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", + "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", + "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", + "tables.py:md5,84d17ad7f43458412e550f74a24560e5", + "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", + "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", + "config.cpython-313.pyc:md5,f2017f0ac6b76bca39f44ec186dd5d79", + "data_management.cpython-313.pyc:md5,1aacb3c94a7821aa50fdef062f87090e", + "style.cpython-313.pyc:md5,dac4d44bc702b378bc5a5119ffc9ffb6", + "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", + "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", + "style.py:md5,b9f1207f06464e43c05e6e3912f12731", + "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", + "E_GEOD_51720_rnaseq.dataset_stats.csv:md5,428c31aba1b6ba8af014d7a80e21bd97", + "E_GEOD_51720_rnaseq.design.csv:md5,80805afb29837b6fbb73a6aa6f3a461b", + "E_GEOD_51720_rnaseq.rnaseq.raw.counts.csv:md5,07cd448196fc2fea4663bd9705da2b98", + "excluded_geo_accessions.txt:md5,cdbec776e8b1d9dc7d0aa44aaf52aa50", + "candidate_counts.parquet:md5,1d25a87d5e815b78c0cf2aa018130319", + "E_GEOD_51720_rnaseq.rnaseq.raw.counts.mapping.csv:md5,1bbc8cddf18072309e81498806aa73ff", + "E_GEOD_51720_rnaseq.rnaseq.raw.counts.metadata.csv:md5,08861c29159a6a2fed38efe523ed9c56", + "E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.csv:md5,ed0d6193d4f39e5e4000f1ddbee521bf", + "whole_gene_id_mapping.csv:md5,1bbc8cddf18072309e81498806aa73ff", + "whole_gene_metadata.csv:md5,a08ab44a98542a8529d2a4b5a47e4408", + "all_counts.parquet:md5,c2c4a4eea8c2091bd4969d34fd7ff0e1", + "all_counts.parquet:md5,c2c4a4eea8c2091bd4969d34fd7ff0e1", + "whole_design.csv:md5,d3aa542c4ad07d0051a84482fe6cd81c", + "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", + "multiqc_expression_distributions_top_stable_genes.txt:md5,66a44969a7b827943a31406bb6e1eca7", + "multiqc_gene_statistics.txt:md5,2a7dfaf41ff7ea555808d01be43616fb", + "multiqc_ranked_top_stable_genes_summary.txt:md5,f00ef628aa055283744c1653fcb9e5b7", + "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030", + "E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,7b76036297abbe80891c05f2e0af9647", + "E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,3a76d77541b2fe103ed56b7e7b54de8f", + "stability_values.normfinder.csv:md5,c32298f5ba2ab39ea954925fe86d2d64" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-08T16:58:37.385701449" + }, + "-profile test_run_normfinder_genorm": { + "content": [ + null, + [ + "aggregate_results", + "aggregate_results/all_counts_filtered.parquet", + "aggregate_results/all_genes_summary.csv", + "aggregate_results/top_stable_genes_summary.csv", + "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", + "base_statistics", + "base_statistics/all", + "base_statistics/all/stats_all_genes.csv", + "base_statistics/rnaseq", + "base_statistics/rnaseq/rnaseq.stats_all_genes.csv", + "clean_count_data", + "clean_count_data/cleaned_counts_filtered.parquet", + "compute_stability_scores", + "compute_stability_scores/stats_with_scores.csv", + "cross_join", + "cross_join/cross_join.0.0.parquet", + "cross_join/cross_join.0.1.parquet", + "cross_join/cross_join.0.10.parquet", + "cross_join/cross_join.0.11.parquet", + "cross_join/cross_join.0.12.parquet", + "cross_join/cross_join.0.13.parquet", + "cross_join/cross_join.0.14.parquet", + "cross_join/cross_join.0.15.parquet", + "cross_join/cross_join.0.16.parquet", + "cross_join/cross_join.0.2.parquet", + "cross_join/cross_join.0.3.parquet", + "cross_join/cross_join.0.4.parquet", + "cross_join/cross_join.0.5.parquet", + "cross_join/cross_join.0.6.parquet", + "cross_join/cross_join.0.7.parquet", + "cross_join/cross_join.0.8.parquet", + "cross_join/cross_join.0.9.parquet", + "cross_join/cross_join.1.1.parquet", + "cross_join/cross_join.1.10.parquet", + "cross_join/cross_join.1.11.parquet", + "cross_join/cross_join.1.12.parquet", + "cross_join/cross_join.1.13.parquet", + "cross_join/cross_join.1.14.parquet", + "cross_join/cross_join.1.15.parquet", + "cross_join/cross_join.1.16.parquet", + "cross_join/cross_join.1.2.parquet", + "cross_join/cross_join.1.3.parquet", + "cross_join/cross_join.1.4.parquet", + "cross_join/cross_join.1.5.parquet", + "cross_join/cross_join.1.6.parquet", + "cross_join/cross_join.1.7.parquet", + "cross_join/cross_join.1.8.parquet", + "cross_join/cross_join.1.9.parquet", + "cross_join/cross_join.10.10.parquet", + "cross_join/cross_join.10.11.parquet", + "cross_join/cross_join.10.12.parquet", + "cross_join/cross_join.10.13.parquet", + "cross_join/cross_join.10.14.parquet", + "cross_join/cross_join.10.15.parquet", + "cross_join/cross_join.10.16.parquet", + "cross_join/cross_join.10.2.parquet", + "cross_join/cross_join.10.3.parquet", + "cross_join/cross_join.10.4.parquet", + "cross_join/cross_join.10.5.parquet", + "cross_join/cross_join.10.6.parquet", + "cross_join/cross_join.10.7.parquet", + "cross_join/cross_join.10.8.parquet", + "cross_join/cross_join.10.9.parquet", + "cross_join/cross_join.11.11.parquet", + "cross_join/cross_join.11.12.parquet", + "cross_join/cross_join.11.13.parquet", + "cross_join/cross_join.11.14.parquet", + "cross_join/cross_join.11.15.parquet", + "cross_join/cross_join.11.16.parquet", + "cross_join/cross_join.11.2.parquet", + "cross_join/cross_join.11.3.parquet", + "cross_join/cross_join.11.4.parquet", + "cross_join/cross_join.11.5.parquet", + "cross_join/cross_join.11.6.parquet", + "cross_join/cross_join.11.7.parquet", + "cross_join/cross_join.11.8.parquet", + "cross_join/cross_join.11.9.parquet", + "cross_join/cross_join.12.12.parquet", + "cross_join/cross_join.12.13.parquet", + "cross_join/cross_join.12.14.parquet", + "cross_join/cross_join.12.15.parquet", + "cross_join/cross_join.12.16.parquet", + "cross_join/cross_join.12.2.parquet", + "cross_join/cross_join.12.3.parquet", + "cross_join/cross_join.12.4.parquet", + "cross_join/cross_join.12.5.parquet", + "cross_join/cross_join.12.6.parquet", + "cross_join/cross_join.12.7.parquet", + "cross_join/cross_join.12.8.parquet", + "cross_join/cross_join.12.9.parquet", + "cross_join/cross_join.13.13.parquet", + "cross_join/cross_join.13.14.parquet", + "cross_join/cross_join.13.15.parquet", + "cross_join/cross_join.13.16.parquet", + "cross_join/cross_join.13.2.parquet", + "cross_join/cross_join.13.3.parquet", + "cross_join/cross_join.13.4.parquet", + "cross_join/cross_join.13.5.parquet", + "cross_join/cross_join.13.6.parquet", + "cross_join/cross_join.13.7.parquet", + "cross_join/cross_join.13.8.parquet", + "cross_join/cross_join.13.9.parquet", + "cross_join/cross_join.14.14.parquet", + "cross_join/cross_join.14.15.parquet", + "cross_join/cross_join.14.16.parquet", + "cross_join/cross_join.14.2.parquet", + "cross_join/cross_join.14.3.parquet", + "cross_join/cross_join.14.4.parquet", + "cross_join/cross_join.14.5.parquet", + "cross_join/cross_join.14.6.parquet", + "cross_join/cross_join.14.7.parquet", + "cross_join/cross_join.14.8.parquet", + "cross_join/cross_join.14.9.parquet", + "cross_join/cross_join.15.15.parquet", + "cross_join/cross_join.15.16.parquet", + "cross_join/cross_join.15.2.parquet", + "cross_join/cross_join.15.3.parquet", + "cross_join/cross_join.15.4.parquet", + "cross_join/cross_join.15.5.parquet", + "cross_join/cross_join.15.6.parquet", + "cross_join/cross_join.15.7.parquet", + "cross_join/cross_join.15.8.parquet", + "cross_join/cross_join.15.9.parquet", + "cross_join/cross_join.16.16.parquet", + "cross_join/cross_join.16.2.parquet", + "cross_join/cross_join.16.3.parquet", + "cross_join/cross_join.16.4.parquet", + "cross_join/cross_join.16.5.parquet", + "cross_join/cross_join.16.6.parquet", + "cross_join/cross_join.16.7.parquet", + "cross_join/cross_join.16.8.parquet", + "cross_join/cross_join.16.9.parquet", + "cross_join/cross_join.2.2.parquet", + "cross_join/cross_join.2.3.parquet", + "cross_join/cross_join.2.4.parquet", + "cross_join/cross_join.2.5.parquet", + "cross_join/cross_join.2.6.parquet", + "cross_join/cross_join.2.7.parquet", + "cross_join/cross_join.2.8.parquet", + "cross_join/cross_join.2.9.parquet", + "cross_join/cross_join.3.3.parquet", + "cross_join/cross_join.3.4.parquet", + "cross_join/cross_join.3.5.parquet", + "cross_join/cross_join.3.6.parquet", + "cross_join/cross_join.3.7.parquet", + "cross_join/cross_join.3.8.parquet", + "cross_join/cross_join.3.9.parquet", + "cross_join/cross_join.4.4.parquet", + "cross_join/cross_join.4.5.parquet", + "cross_join/cross_join.4.6.parquet", + "cross_join/cross_join.4.7.parquet", + "cross_join/cross_join.4.8.parquet", + "cross_join/cross_join.4.9.parquet", + "cross_join/cross_join.5.5.parquet", + "cross_join/cross_join.5.6.parquet", + "cross_join/cross_join.5.7.parquet", + "cross_join/cross_join.5.8.parquet", + "cross_join/cross_join.5.9.parquet", + "cross_join/cross_join.6.6.parquet", + "cross_join/cross_join.6.7.parquet", + "cross_join/cross_join.6.8.parquet", + "cross_join/cross_join.6.9.parquet", + "cross_join/cross_join.7.7.parquet", + "cross_join/cross_join.7.8.parquet", + "cross_join/cross_join.7.9.parquet", + "cross_join/cross_join.8.8.parquet", + "cross_join/cross_join.8.9.parquet", + "cross_join/cross_join.9.9.parquet", + "dash_app", + "dash_app/app.py", + "dash_app/assets", + "dash_app/assets/style.css", + "dash_app/data", + "dash_app/data/all_counts.parquet", + "dash_app/data/all_genes_summary.csv", + "dash_app/data/whole_design.csv", + "dash_app/file_system_backend", + "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", + "dash_app/spec-file.txt", + "dash_app/src", + "dash_app/src/callbacks", + "dash_app/src/callbacks/__pycache__", + "dash_app/src/callbacks/__pycache__/common.cpython-313.pyc", + "dash_app/src/callbacks/__pycache__/genes.cpython-313.pyc", + "dash_app/src/callbacks/__pycache__/samples.cpython-313.pyc", + "dash_app/src/callbacks/common.py", + "dash_app/src/callbacks/genes.py", + "dash_app/src/callbacks/samples.py", + "dash_app/src/components", + "dash_app/src/components/__pycache__", + "dash_app/src/components/__pycache__/graphs.cpython-313.pyc", + "dash_app/src/components/__pycache__/right_sidebar.cpython-313.pyc", + "dash_app/src/components/__pycache__/stores.cpython-313.pyc", + "dash_app/src/components/__pycache__/tables.cpython-313.pyc", + "dash_app/src/components/__pycache__/tooltips.cpython-313.pyc", + "dash_app/src/components/__pycache__/top.cpython-313.pyc", + "dash_app/src/components/graphs.py", + "dash_app/src/components/icons.py", + "dash_app/src/components/right_sidebar.py", + "dash_app/src/components/settings", + "dash_app/src/components/settings/__pycache__", + "dash_app/src/components/settings/__pycache__/genes.cpython-313.pyc", + "dash_app/src/components/settings/__pycache__/samples.cpython-313.pyc", + "dash_app/src/components/settings/genes.py", + "dash_app/src/components/settings/samples.py", + "dash_app/src/components/stores.py", + "dash_app/src/components/tables.py", + "dash_app/src/components/tooltips.py", + "dash_app/src/components/top.py", + "dash_app/src/utils", + "dash_app/src/utils/__pycache__", + "dash_app/src/utils/__pycache__/config.cpython-313.pyc", + "dash_app/src/utils/__pycache__/data_management.cpython-313.pyc", + "dash_app/src/utils/__pycache__/style.cpython-313.pyc", + "dash_app/src/utils/config.py", + "dash_app/src/utils/data_management.py", + "dash_app/src/utils/style.py", + "dash_app/versions.yml", + "dataset_statistics", + "dataset_statistics/E_MTAB_7711_rnaseq.dataset_stats.csv", + "errors", + "expression_atlas", + "expression_atlas/datasets", + "expression_atlas/datasets/E_MTAB_7711_rnaseq.design.csv", + "expression_atlas/datasets/E_MTAB_7711_rnaseq.rnaseq.raw.counts.csv", + "expression_ratio", + "expression_ratio/ratios.0.0.parquet", + "expression_ratio/ratios.0.1.parquet", + "expression_ratio/ratios.0.10.parquet", + "expression_ratio/ratios.0.11.parquet", + "expression_ratio/ratios.0.12.parquet", + "expression_ratio/ratios.0.13.parquet", + "expression_ratio/ratios.0.14.parquet", + "expression_ratio/ratios.0.15.parquet", + "expression_ratio/ratios.0.16.parquet", + "expression_ratio/ratios.0.2.parquet", + "expression_ratio/ratios.0.3.parquet", + "expression_ratio/ratios.0.4.parquet", + "expression_ratio/ratios.0.5.parquet", + "expression_ratio/ratios.0.6.parquet", + "expression_ratio/ratios.0.7.parquet", + "expression_ratio/ratios.0.8.parquet", + "expression_ratio/ratios.0.9.parquet", + "expression_ratio/ratios.1.1.parquet", + "expression_ratio/ratios.1.10.parquet", + "expression_ratio/ratios.1.11.parquet", + "expression_ratio/ratios.1.12.parquet", + "expression_ratio/ratios.1.13.parquet", + "expression_ratio/ratios.1.14.parquet", + "expression_ratio/ratios.1.15.parquet", + "expression_ratio/ratios.1.16.parquet", + "expression_ratio/ratios.1.2.parquet", + "expression_ratio/ratios.1.3.parquet", + "expression_ratio/ratios.1.4.parquet", + "expression_ratio/ratios.1.5.parquet", + "expression_ratio/ratios.1.6.parquet", + "expression_ratio/ratios.1.7.parquet", + "expression_ratio/ratios.1.8.parquet", + "expression_ratio/ratios.1.9.parquet", + "expression_ratio/ratios.10.10.parquet", + "expression_ratio/ratios.10.11.parquet", + "expression_ratio/ratios.10.12.parquet", + "expression_ratio/ratios.10.13.parquet", + "expression_ratio/ratios.10.14.parquet", + "expression_ratio/ratios.10.15.parquet", + "expression_ratio/ratios.10.16.parquet", + "expression_ratio/ratios.10.2.parquet", + "expression_ratio/ratios.10.3.parquet", + "expression_ratio/ratios.10.4.parquet", + "expression_ratio/ratios.10.5.parquet", + "expression_ratio/ratios.10.6.parquet", + "expression_ratio/ratios.10.7.parquet", + "expression_ratio/ratios.10.8.parquet", + "expression_ratio/ratios.10.9.parquet", + "expression_ratio/ratios.11.11.parquet", + "expression_ratio/ratios.11.12.parquet", + "expression_ratio/ratios.11.13.parquet", + "expression_ratio/ratios.11.14.parquet", + "expression_ratio/ratios.11.15.parquet", + "expression_ratio/ratios.11.16.parquet", + "expression_ratio/ratios.11.2.parquet", + "expression_ratio/ratios.11.3.parquet", + "expression_ratio/ratios.11.4.parquet", + "expression_ratio/ratios.11.5.parquet", + "expression_ratio/ratios.11.6.parquet", + "expression_ratio/ratios.11.7.parquet", + "expression_ratio/ratios.11.8.parquet", + "expression_ratio/ratios.11.9.parquet", + "expression_ratio/ratios.12.12.parquet", + "expression_ratio/ratios.12.13.parquet", + "expression_ratio/ratios.12.14.parquet", + "expression_ratio/ratios.12.15.parquet", + "expression_ratio/ratios.12.16.parquet", + "expression_ratio/ratios.12.2.parquet", + "expression_ratio/ratios.12.3.parquet", + "expression_ratio/ratios.12.4.parquet", + "expression_ratio/ratios.12.5.parquet", + "expression_ratio/ratios.12.6.parquet", + "expression_ratio/ratios.12.7.parquet", + "expression_ratio/ratios.12.8.parquet", + "expression_ratio/ratios.12.9.parquet", + "expression_ratio/ratios.13.13.parquet", + "expression_ratio/ratios.13.14.parquet", + "expression_ratio/ratios.13.15.parquet", + "expression_ratio/ratios.13.16.parquet", + "expression_ratio/ratios.13.2.parquet", + "expression_ratio/ratios.13.3.parquet", + "expression_ratio/ratios.13.4.parquet", + "expression_ratio/ratios.13.5.parquet", + "expression_ratio/ratios.13.6.parquet", + "expression_ratio/ratios.13.7.parquet", + "expression_ratio/ratios.13.8.parquet", + "expression_ratio/ratios.13.9.parquet", + "expression_ratio/ratios.14.14.parquet", + "expression_ratio/ratios.14.15.parquet", + "expression_ratio/ratios.14.16.parquet", + "expression_ratio/ratios.14.2.parquet", + "expression_ratio/ratios.14.3.parquet", + "expression_ratio/ratios.14.4.parquet", + "expression_ratio/ratios.14.5.parquet", + "expression_ratio/ratios.14.6.parquet", + "expression_ratio/ratios.14.7.parquet", + "expression_ratio/ratios.14.8.parquet", + "expression_ratio/ratios.14.9.parquet", + "expression_ratio/ratios.15.15.parquet", + "expression_ratio/ratios.15.16.parquet", + "expression_ratio/ratios.15.2.parquet", + "expression_ratio/ratios.15.3.parquet", + "expression_ratio/ratios.15.4.parquet", + "expression_ratio/ratios.15.5.parquet", + "expression_ratio/ratios.15.6.parquet", + "expression_ratio/ratios.15.7.parquet", + "expression_ratio/ratios.15.8.parquet", + "expression_ratio/ratios.15.9.parquet", + "expression_ratio/ratios.16.16.parquet", + "expression_ratio/ratios.16.2.parquet", + "expression_ratio/ratios.16.3.parquet", + "expression_ratio/ratios.16.4.parquet", + "expression_ratio/ratios.16.5.parquet", + "expression_ratio/ratios.16.6.parquet", + "expression_ratio/ratios.16.7.parquet", + "expression_ratio/ratios.16.8.parquet", + "expression_ratio/ratios.16.9.parquet", + "expression_ratio/ratios.2.2.parquet", + "expression_ratio/ratios.2.3.parquet", + "expression_ratio/ratios.2.4.parquet", + "expression_ratio/ratios.2.5.parquet", + "expression_ratio/ratios.2.6.parquet", + "expression_ratio/ratios.2.7.parquet", + "expression_ratio/ratios.2.8.parquet", + "expression_ratio/ratios.2.9.parquet", + "expression_ratio/ratios.3.3.parquet", + "expression_ratio/ratios.3.4.parquet", + "expression_ratio/ratios.3.5.parquet", + "expression_ratio/ratios.3.6.parquet", + "expression_ratio/ratios.3.7.parquet", + "expression_ratio/ratios.3.8.parquet", + "expression_ratio/ratios.3.9.parquet", + "expression_ratio/ratios.4.4.parquet", + "expression_ratio/ratios.4.5.parquet", + "expression_ratio/ratios.4.6.parquet", + "expression_ratio/ratios.4.7.parquet", + "expression_ratio/ratios.4.8.parquet", + "expression_ratio/ratios.4.9.parquet", + "expression_ratio/ratios.5.5.parquet", + "expression_ratio/ratios.5.6.parquet", + "expression_ratio/ratios.5.7.parquet", + "expression_ratio/ratios.5.8.parquet", + "expression_ratio/ratios.5.9.parquet", + "expression_ratio/ratios.6.6.parquet", + "expression_ratio/ratios.6.7.parquet", + "expression_ratio/ratios.6.8.parquet", + "expression_ratio/ratios.6.9.parquet", + "expression_ratio/ratios.7.7.parquet", + "expression_ratio/ratios.7.8.parquet", + "expression_ratio/ratios.7.9.parquet", + "expression_ratio/ratios.8.8.parquet", + "expression_ratio/ratios.8.9.parquet", + "expression_ratio/ratios.9.9.parquet", + "geo", + "get_candidate_genes", + "get_candidate_genes/candidate_counts.parquet", + "idmapping", + "idmapping/datasets", + "idmapping/datasets/E_MTAB_7711_rnaseq.rnaseq.raw.counts.mapping.csv", + "idmapping/datasets/E_MTAB_7711_rnaseq.rnaseq.raw.counts.metadata.csv", + "idmapping/datasets/E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/whole_gene_id_mapping.csv", + "idmapping/whole_gene_metadata.csv", + "make_chunks", + "make_chunks/count_chunk.0.parquet", + "make_chunks/count_chunk.1.parquet", + "make_chunks/count_chunk.10.parquet", + "make_chunks/count_chunk.11.parquet", + "make_chunks/count_chunk.12.parquet", + "make_chunks/count_chunk.13.parquet", + "make_chunks/count_chunk.14.parquet", + "make_chunks/count_chunk.15.parquet", + "make_chunks/count_chunk.16.parquet", + "make_chunks/count_chunk.2.parquet", + "make_chunks/count_chunk.3.parquet", + "make_chunks/count_chunk.4.parquet", + "make_chunks/count_chunk.5.parquet", + "make_chunks/count_chunk.6.parquet", + "make_chunks/count_chunk.7.parquet", + "make_chunks/count_chunk.8.parquet", + "make_chunks/count_chunk.9.parquet", + "merged_datasets", + "merged_datasets/all", + "merged_datasets/all/all_counts.parquet", + "merged_datasets/rnaseq", + "merged_datasets/rnaseq/all_counts.parquet", + "merged_datasets/whole_design.csv", + "multiqc", + "multiqc/multiqc_data", + "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_expression_distributions_top_stable_genes.txt", + "multiqc/multiqc_data/multiqc_gene_statistics.txt", + "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", + "multiqc/multiqc_data/multiqc_software_versions.txt", + "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_plots", + "multiqc/multiqc_plots/pdf", + "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", + "multiqc/multiqc_plots/pdf/gene_statistics.pdf", + "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", + "multiqc/multiqc_plots/png", + "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", + "multiqc/multiqc_plots/png/gene_statistics.png", + "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", + "multiqc/multiqc_plots/svg", + "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", + "multiqc/multiqc_plots/svg/gene_statistics.svg", + "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", + "multiqc/multiqc_report.html", + "multiqc/versions.yml", + "normalised", + "normalised/E_MTAB_7711_rnaseq", + "normalised/E_MTAB_7711_rnaseq/normalisation_deseq2", + "normalised/E_MTAB_7711_rnaseq/normalisation_deseq2/E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", "pipeline_info", "pipeline_info/software_mqc_versions.yml", + "quantile_normalised", + "quantile_normalised/E_MTAB_7711_rnaseq", + "quantile_normalised/E_MTAB_7711_rnaseq/E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", + "ratio_standard_variation", + "ratio_standard_variation/std.0.0.parquet", + "ratio_standard_variation/std.0.1.parquet", + "ratio_standard_variation/std.0.10.parquet", + "ratio_standard_variation/std.0.11.parquet", + "ratio_standard_variation/std.0.12.parquet", + "ratio_standard_variation/std.0.13.parquet", + "ratio_standard_variation/std.0.14.parquet", + "ratio_standard_variation/std.0.15.parquet", + "ratio_standard_variation/std.0.16.parquet", + "ratio_standard_variation/std.0.2.parquet", + "ratio_standard_variation/std.0.3.parquet", + "ratio_standard_variation/std.0.4.parquet", + "ratio_standard_variation/std.0.5.parquet", + "ratio_standard_variation/std.0.6.parquet", + "ratio_standard_variation/std.0.7.parquet", + "ratio_standard_variation/std.0.8.parquet", + "ratio_standard_variation/std.0.9.parquet", + "ratio_standard_variation/std.1.1.parquet", + "ratio_standard_variation/std.1.10.parquet", + "ratio_standard_variation/std.1.11.parquet", + "ratio_standard_variation/std.1.12.parquet", + "ratio_standard_variation/std.1.13.parquet", + "ratio_standard_variation/std.1.14.parquet", + "ratio_standard_variation/std.1.15.parquet", + "ratio_standard_variation/std.1.16.parquet", + "ratio_standard_variation/std.1.2.parquet", + "ratio_standard_variation/std.1.3.parquet", + "ratio_standard_variation/std.1.4.parquet", + "ratio_standard_variation/std.1.5.parquet", + "ratio_standard_variation/std.1.6.parquet", + "ratio_standard_variation/std.1.7.parquet", + "ratio_standard_variation/std.1.8.parquet", + "ratio_standard_variation/std.1.9.parquet", + "ratio_standard_variation/std.10.10.parquet", + "ratio_standard_variation/std.10.11.parquet", + "ratio_standard_variation/std.10.12.parquet", + "ratio_standard_variation/std.10.13.parquet", + "ratio_standard_variation/std.10.14.parquet", + "ratio_standard_variation/std.10.15.parquet", + "ratio_standard_variation/std.10.16.parquet", + "ratio_standard_variation/std.10.2.parquet", + "ratio_standard_variation/std.10.3.parquet", + "ratio_standard_variation/std.10.4.parquet", + "ratio_standard_variation/std.10.5.parquet", + "ratio_standard_variation/std.10.6.parquet", + "ratio_standard_variation/std.10.7.parquet", + "ratio_standard_variation/std.10.8.parquet", + "ratio_standard_variation/std.10.9.parquet", + "ratio_standard_variation/std.11.11.parquet", + "ratio_standard_variation/std.11.12.parquet", + "ratio_standard_variation/std.11.13.parquet", + "ratio_standard_variation/std.11.14.parquet", + "ratio_standard_variation/std.11.15.parquet", + "ratio_standard_variation/std.11.16.parquet", + "ratio_standard_variation/std.11.2.parquet", + "ratio_standard_variation/std.11.3.parquet", + "ratio_standard_variation/std.11.4.parquet", + "ratio_standard_variation/std.11.5.parquet", + "ratio_standard_variation/std.11.6.parquet", + "ratio_standard_variation/std.11.7.parquet", + "ratio_standard_variation/std.11.8.parquet", + "ratio_standard_variation/std.11.9.parquet", + "ratio_standard_variation/std.12.12.parquet", + "ratio_standard_variation/std.12.13.parquet", + "ratio_standard_variation/std.12.14.parquet", + "ratio_standard_variation/std.12.15.parquet", + "ratio_standard_variation/std.12.16.parquet", + "ratio_standard_variation/std.12.2.parquet", + "ratio_standard_variation/std.12.3.parquet", + "ratio_standard_variation/std.12.4.parquet", + "ratio_standard_variation/std.12.5.parquet", + "ratio_standard_variation/std.12.6.parquet", + "ratio_standard_variation/std.12.7.parquet", + "ratio_standard_variation/std.12.8.parquet", + "ratio_standard_variation/std.12.9.parquet", + "ratio_standard_variation/std.13.13.parquet", + "ratio_standard_variation/std.13.14.parquet", + "ratio_standard_variation/std.13.15.parquet", + "ratio_standard_variation/std.13.16.parquet", + "ratio_standard_variation/std.13.2.parquet", + "ratio_standard_variation/std.13.3.parquet", + "ratio_standard_variation/std.13.4.parquet", + "ratio_standard_variation/std.13.5.parquet", + "ratio_standard_variation/std.13.6.parquet", + "ratio_standard_variation/std.13.7.parquet", + "ratio_standard_variation/std.13.8.parquet", + "ratio_standard_variation/std.13.9.parquet", + "ratio_standard_variation/std.14.14.parquet", + "ratio_standard_variation/std.14.15.parquet", + "ratio_standard_variation/std.14.16.parquet", + "ratio_standard_variation/std.14.2.parquet", + "ratio_standard_variation/std.14.3.parquet", + "ratio_standard_variation/std.14.4.parquet", + "ratio_standard_variation/std.14.5.parquet", + "ratio_standard_variation/std.14.6.parquet", + "ratio_standard_variation/std.14.7.parquet", + "ratio_standard_variation/std.14.8.parquet", + "ratio_standard_variation/std.14.9.parquet", + "ratio_standard_variation/std.15.15.parquet", + "ratio_standard_variation/std.15.16.parquet", + "ratio_standard_variation/std.15.2.parquet", + "ratio_standard_variation/std.15.3.parquet", + "ratio_standard_variation/std.15.4.parquet", + "ratio_standard_variation/std.15.5.parquet", + "ratio_standard_variation/std.15.6.parquet", + "ratio_standard_variation/std.15.7.parquet", + "ratio_standard_variation/std.15.8.parquet", + "ratio_standard_variation/std.15.9.parquet", + "ratio_standard_variation/std.16.16.parquet", + "ratio_standard_variation/std.16.2.parquet", + "ratio_standard_variation/std.16.3.parquet", + "ratio_standard_variation/std.16.4.parquet", + "ratio_standard_variation/std.16.5.parquet", + "ratio_standard_variation/std.16.6.parquet", + "ratio_standard_variation/std.16.7.parquet", + "ratio_standard_variation/std.16.8.parquet", + "ratio_standard_variation/std.16.9.parquet", + "ratio_standard_variation/std.2.2.parquet", + "ratio_standard_variation/std.2.3.parquet", + "ratio_standard_variation/std.2.4.parquet", + "ratio_standard_variation/std.2.5.parquet", + "ratio_standard_variation/std.2.6.parquet", + "ratio_standard_variation/std.2.7.parquet", + "ratio_standard_variation/std.2.8.parquet", + "ratio_standard_variation/std.2.9.parquet", + "ratio_standard_variation/std.3.3.parquet", + "ratio_standard_variation/std.3.4.parquet", + "ratio_standard_variation/std.3.5.parquet", + "ratio_standard_variation/std.3.6.parquet", + "ratio_standard_variation/std.3.7.parquet", + "ratio_standard_variation/std.3.8.parquet", + "ratio_standard_variation/std.3.9.parquet", + "ratio_standard_variation/std.4.4.parquet", + "ratio_standard_variation/std.4.5.parquet", + "ratio_standard_variation/std.4.6.parquet", + "ratio_standard_variation/std.4.7.parquet", + "ratio_standard_variation/std.4.8.parquet", + "ratio_standard_variation/std.4.9.parquet", + "ratio_standard_variation/std.5.5.parquet", + "ratio_standard_variation/std.5.6.parquet", + "ratio_standard_variation/std.5.7.parquet", + "ratio_standard_variation/std.5.8.parquet", + "ratio_standard_variation/std.5.9.parquet", + "ratio_standard_variation/std.6.6.parquet", + "ratio_standard_variation/std.6.7.parquet", + "ratio_standard_variation/std.6.8.parquet", + "ratio_standard_variation/std.6.9.parquet", + "ratio_standard_variation/std.7.7.parquet", + "ratio_standard_variation/std.7.8.parquet", + "ratio_standard_variation/std.7.9.parquet", + "ratio_standard_variation/std.8.8.parquet", + "ratio_standard_variation/std.8.9.parquet", + "ratio_standard_variation/std.9.9.parquet", + "stability_scoring", + "stability_scoring/genorm", + "stability_scoring/genorm/m_measures.csv", + "stability_scoring/normfinder", + "stability_scoring/normfinder/stability_values.normfinder.csv", "warnings" ], [ - "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", - "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", - "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", - "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1" + "all_counts_filtered.parquet:md5,65d131627063eb9934bbb4f5adc98bc0", + "all_genes_summary.csv:md5,9323e53e94eba5317ec0418041610a9d", + "top_stable_genes_summary.csv:md5,70a73b2f4458113f9b842bf1aeb1c74e", + "top_stable_genes_transposed_counts_filtered.csv:md5,dea022542f54cdec7365b9a921afa841", + "stats_all_genes.csv:md5,b18933f229b7846edfe91886883a8a54", + "rnaseq.stats_all_genes.csv:md5,fc3c4b9b2365f2aeca3f0967edcd7b15", + "cleaned_counts_filtered.parquet:md5,4b40a0560f45426f0c09f347efce253f", + "stats_with_scores.csv:md5,9df06189d08aff0f9065f8a5786ece45", + "cross_join.0.0.parquet:md5,7e76ae2c5bc60ece6e875663a34f6906", + "cross_join.0.1.parquet:md5,f3239e993dd42558bb35ee793761a82e", + "cross_join.0.10.parquet:md5,0aa5b14054b2045c7d060b1f6cbb1da2", + "cross_join.0.11.parquet:md5,b0f3ca05ea3d5a1af36980080a67bed7", + "cross_join.0.12.parquet:md5,c2903251a9062614f79ff8c795354487", + "cross_join.0.13.parquet:md5,4ceb9e71b8327f1e29f5a91161961ab0", + "cross_join.0.14.parquet:md5,b1a0069c67ba0f86f610640d9d4ae161", + "cross_join.0.15.parquet:md5,940975300e1e218df3400cc65c940a0d", + "cross_join.0.16.parquet:md5,904420c55df8af8f299892e1200a5c70", + "cross_join.0.2.parquet:md5,c9eb3049e1003e21f0ce6d133e2c5958", + "cross_join.0.3.parquet:md5,d6dc799ce78643f72a7431d1372604bc", + "cross_join.0.4.parquet:md5,fc0c7a169541ff1ccb9d1f54f6a02eeb", + "cross_join.0.5.parquet:md5,2cb746cb1c2cd758d629b08eb3a36f5b", + "cross_join.0.6.parquet:md5,28c531142803de10b09beece294fff7d", + "cross_join.0.7.parquet:md5,601ba9b3e6e44e496a9c1299b4c5e70b", + "cross_join.0.8.parquet:md5,06688bf36f60cc39ecbceb7a66e8122f", + "cross_join.0.9.parquet:md5,a75bd71e3b31bfe4acad7a766c253f9b", + "cross_join.1.1.parquet:md5,d77719ff32309955c8b17ad1b61bdbee", + "cross_join.1.10.parquet:md5,40dc3f4d056bee644c1504265682878e", + "cross_join.1.11.parquet:md5,aaa7daea414e3e466d086d0379dbd6e0", + "cross_join.1.12.parquet:md5,c99898f1e848fa592c6a5de965903e4a", + "cross_join.1.13.parquet:md5,09233e2c81b092c812c92472eddd9cb4", + "cross_join.1.14.parquet:md5,1510b25ba5fcdbf4780407f0de5cf1f4", + "cross_join.1.15.parquet:md5,7da33e84472fbdceda0156d6f75adc3d", + "cross_join.1.16.parquet:md5,4b68c051e85be6f78c9945d48c336df2", + "cross_join.1.2.parquet:md5,ee2cfeceff695fd8a737698d78381e93", + "cross_join.1.3.parquet:md5,e3fd3f04c6be97978fb259d6112b68a4", + "cross_join.1.4.parquet:md5,592b9887dbabf7a4e1e1fcd7a2fc8a23", + "cross_join.1.5.parquet:md5,16f23da82c8b9919a3843ce316d7c581", + "cross_join.1.6.parquet:md5,707c65cb3acfcda0226e0947cf40c27b", + "cross_join.1.7.parquet:md5,272b98cf7d9a657e08e8d4fc6b52474b", + "cross_join.1.8.parquet:md5,8b3e497055119e27503a42cde68d3f12", + "cross_join.1.9.parquet:md5,d1e292ea4b15e0ad3d6e1b619f669971", + "cross_join.10.10.parquet:md5,6d95a8e4df684aa1186fe88105b0d771", + "cross_join.10.11.parquet:md5,d9cc7eacab721f14a1952e521ac68b80", + "cross_join.10.12.parquet:md5,fd20184d3d9e3b3e06f19f26842458f2", + "cross_join.10.13.parquet:md5,67f21c1f7e52278b78eb4553acf26acf", + "cross_join.10.14.parquet:md5,12a2559266cab49986f53d90fd5cbc5f", + "cross_join.10.15.parquet:md5,d307b4fc1fff13410770fe80e4627fe8", + "cross_join.10.16.parquet:md5,a7bbd0d28bb122ddd250f34b33b089ca", + "cross_join.10.2.parquet:md5,03ee9f3eb5606ada3575eb0f0bdf1921", + "cross_join.10.3.parquet:md5,a1372fc98a3d090b010cf00f66de51a9", + "cross_join.10.4.parquet:md5,b6b3f8276ab1f1d78ed68441c1f8d273", + "cross_join.10.5.parquet:md5,f378b2cf1adab3ed49696e4db1e24fab", + "cross_join.10.6.parquet:md5,0c6b32c8da9ed3632ec82788d452e705", + "cross_join.10.7.parquet:md5,175621b5483923ec4d0cbaa5f50e3084", + "cross_join.10.8.parquet:md5,219766da467e926c8c7daedbf92c8e11", + "cross_join.10.9.parquet:md5,1973f9214a0d5a567f555f17163fba7d", + "cross_join.11.11.parquet:md5,be4d4bcd311a1e0bc8403bbcd21671c2", + "cross_join.11.12.parquet:md5,468cc0dbeec0c4b5d6362aa8b5febf83", + "cross_join.11.13.parquet:md5,8a1798e49d050dea5a45c5122863eb20", + "cross_join.11.14.parquet:md5,ad9cfdb85e7a6c32232ea2f232f210d0", + "cross_join.11.15.parquet:md5,9037285954b111f87a8e28c4767b80a2", + "cross_join.11.16.parquet:md5,b0570f3c710c478448446636372d0b0e", + "cross_join.11.2.parquet:md5,0c2b761b9fd07d412a1ae541797458df", + "cross_join.11.3.parquet:md5,66c6eba5162d49a9c99ed730e2767cc2", + "cross_join.11.4.parquet:md5,36e3781d877256a69fae1b740e2f79b6", + "cross_join.11.5.parquet:md5,7f9e43d253ad11a97dbfb189e75571ec", + "cross_join.11.6.parquet:md5,0a61be9f810fb3f58c2176fe48710598", + "cross_join.11.7.parquet:md5,91b09cb87582c897a75e292448efcb1c", + "cross_join.11.8.parquet:md5,e0e77fd9de40a66f01758fa7c1bbfe8a", + "cross_join.11.9.parquet:md5,631572ba939b5a0404743424eb62d239", + "cross_join.12.12.parquet:md5,cb28628d8731aa57361a349025a7a706", + "cross_join.12.13.parquet:md5,96e1990c873894440ca841c51260346f", + "cross_join.12.14.parquet:md5,282e757d3673308a464428359b419582", + "cross_join.12.15.parquet:md5,eea9c40c7c953c24f7698ddbc001f530", + "cross_join.12.16.parquet:md5,d3b31846618dc835ab71f053eb8b97b5", + "cross_join.12.2.parquet:md5,d967f7f7895bbd44eebab63592e49894", + "cross_join.12.3.parquet:md5,50c90d2a1702eba78d6db85155e84554", + "cross_join.12.4.parquet:md5,88bfaf60385ee591339bc57baf10e4ff", + "cross_join.12.5.parquet:md5,63bf06cb06bdace2db39253b2e85a025", + "cross_join.12.6.parquet:md5,0a161f5b85b964fa239db840f57c8611", + "cross_join.12.7.parquet:md5,e23da7b3ab34a2a4b1f860118538a4b1", + "cross_join.12.8.parquet:md5,79542f9143dad125924a083ec1d7df41", + "cross_join.12.9.parquet:md5,066fc6ceca7e54c1979e077ba6405218", + "cross_join.13.13.parquet:md5,7e37267a571bb95b506ae6672c8a9557", + "cross_join.13.14.parquet:md5,b0cf0bc64d1663ba982d0605d153ece1", + "cross_join.13.15.parquet:md5,8b011d02a96be64aa6171eb4c3707b90", + "cross_join.13.16.parquet:md5,95ab0f3806614abfc26f43cdb4c84bce", + "cross_join.13.2.parquet:md5,0e212df392ed0239ff778961dd40e46e", + "cross_join.13.3.parquet:md5,a3fed95f81dd6cdb0437d60952d5fef6", + "cross_join.13.4.parquet:md5,0f7f006f7d0423e0b7a91e6e650cd197", + "cross_join.13.5.parquet:md5,67af140a74fcf2df0905190bde8f3d39", + "cross_join.13.6.parquet:md5,1c24eae1cf16cd01b05cfecf73ae51ce", + "cross_join.13.7.parquet:md5,42b2a4cdd3e637d1bf278d53c122f0e7", + "cross_join.13.8.parquet:md5,55acaa54412ee87f253c40af0f4720a4", + "cross_join.13.9.parquet:md5,9fd116028ec743df3ef50d948689cf8a", + "cross_join.14.14.parquet:md5,d06bcb7a086822d2238c0ab644adfcf8", + "cross_join.14.15.parquet:md5,7f17ae4eca41a0435d911e6e7ad73382", + "cross_join.14.16.parquet:md5,a4740227f9848ed1def781a0fc07009c", + "cross_join.14.2.parquet:md5,5102c49b7c0c5c1465482530c70aeed1", + "cross_join.14.3.parquet:md5,a181402abb6fdaca79dd74c58a287248", + "cross_join.14.4.parquet:md5,7c2cb03f830058fc18a1606b665f94f4", + "cross_join.14.5.parquet:md5,af066c203e660622b2e6e6dd37a4e06e", + "cross_join.14.6.parquet:md5,8830e4c70e8c624ccffa74fde9a5ac8f", + "cross_join.14.7.parquet:md5,796d8d492d9854f7452a71bfc7dd3a31", + "cross_join.14.8.parquet:md5,b5d49e15174dbe5a386d174755962063", + "cross_join.14.9.parquet:md5,3c76f5e802b616221a5f9ec6f38a4072", + "cross_join.15.15.parquet:md5,8af8def9e60bb4c8fe18e59fa866be1e", + "cross_join.15.16.parquet:md5,3330749f39d30058fec249bc39ce6215", + "cross_join.15.2.parquet:md5,e79a754fcb0f59c262402949c4c0fda6", + "cross_join.15.3.parquet:md5,13faddf90e86419788d5627e00e8e282", + "cross_join.15.4.parquet:md5,be779e8db543cfdf8b10ab378f668651", + "cross_join.15.5.parquet:md5,221cc3fa7a8a27ef721de72fdfb94276", + "cross_join.15.6.parquet:md5,64aad6280f50523202994059995dec64", + "cross_join.15.7.parquet:md5,02fd2392273b5cac4fb81ba1eaeff0ac", + "cross_join.15.8.parquet:md5,4829cc82747059080e382dd1ad44f3b6", + "cross_join.15.9.parquet:md5,ccfec7a98eef5b5445c16857a5d74b3a", + "cross_join.16.16.parquet:md5,b3d1639fa96b20d7eacd910d2016af62", + "cross_join.16.2.parquet:md5,53003afb997e0997d792144db6304557", + "cross_join.16.3.parquet:md5,9640e18450ccf4f5432f72d7a003bcce", + "cross_join.16.4.parquet:md5,03d287f1763a29d3f3bdc74003034146", + "cross_join.16.5.parquet:md5,6e053271fb906b8583623eb96694a66b", + "cross_join.16.6.parquet:md5,8c0d6aa210abde390cccaca7f49664ba", + "cross_join.16.7.parquet:md5,08e199e75a3c80e3bc3debc98d7db7a2", + "cross_join.16.8.parquet:md5,d4c465c45c56806a302233e3c81ac6cd", + "cross_join.16.9.parquet:md5,1e65bffb9e1b899c00535f058e784e33", + "cross_join.2.2.parquet:md5,ef4c3a0f342c4b9692b29a3987477937", + "cross_join.2.3.parquet:md5,b284154698adfa08cd341662b735133c", + "cross_join.2.4.parquet:md5,ab97a2f6d9eefd07478f7d87ac21f1d5", + "cross_join.2.5.parquet:md5,cff079280d60da457d6090d983a78602", + "cross_join.2.6.parquet:md5,6cc58636207f0b85c61729077d50dfe2", + "cross_join.2.7.parquet:md5,e79a84834f60a97377f16d79e2c70b52", + "cross_join.2.8.parquet:md5,4340e9787155977c5d1dca36d1f5b291", + "cross_join.2.9.parquet:md5,f5d2706522c1a8ae9ff2199747d3d8ba", + "cross_join.3.3.parquet:md5,7b18de4ddd5e94fee08c4f2dde442849", + "cross_join.3.4.parquet:md5,7757d071ca5195441917b25e0fc2584b", + "cross_join.3.5.parquet:md5,0e9056434cf760941e14792c3072d8df", + "cross_join.3.6.parquet:md5,0de03058e0dda96292057ac6a6b411d5", + "cross_join.3.7.parquet:md5,f883c241d7d327f592494d6ee2faf7bb", + "cross_join.3.8.parquet:md5,bc7e226977a7cef8cc97b6ea8f7dae18", + "cross_join.3.9.parquet:md5,a2f0b34e3665e8269b0713fe28415c00", + "cross_join.4.4.parquet:md5,c49c2963a6c4fa69cf490f66f814e2a8", + "cross_join.4.5.parquet:md5,a2123fbda2b34fb9f141480bdaff2340", + "cross_join.4.6.parquet:md5,5d6e362d7276975963d09d59133bcc66", + "cross_join.4.7.parquet:md5,ce5adfcd008cc8b1aa2726b548e56b14", + "cross_join.4.8.parquet:md5,d82edd26f55b844b2ca5739e94ff2326", + "cross_join.4.9.parquet:md5,dc6a5cc140e9e05d877190257fb5fc92", + "cross_join.5.5.parquet:md5,f13837a3108e4abdb101661282121d9b", + "cross_join.5.6.parquet:md5,04fe81e433b97db698e44be70b979e60", + "cross_join.5.7.parquet:md5,484dda88b6882f12e3bad7ba9e6c0a56", + "cross_join.5.8.parquet:md5,bafbf426b355f228d72a20b2e0b4f717", + "cross_join.5.9.parquet:md5,ca96c9ab5e9538174a80e2a8a7fd58e5", + "cross_join.6.6.parquet:md5,e35baa2f4667516d663a5525a36a3ecb", + "cross_join.6.7.parquet:md5,905387980011661d51e3a05e47d4d501", + "cross_join.6.8.parquet:md5,0aeb5f8e18ba4d730e342db7191fea02", + "cross_join.6.9.parquet:md5,5c7be098bca498dd835f313047f628ce", + "cross_join.7.7.parquet:md5,32a3730594b57ee826b5bbb25fd3d78a", + "cross_join.7.8.parquet:md5,4abddb54f721c32b7645b5ae2553c485", + "cross_join.7.9.parquet:md5,e5beeb167d286e81b3b350a238273ef1", + "cross_join.8.8.parquet:md5,6a96ab00f0867eda804826145cd8344e", + "cross_join.8.9.parquet:md5,d815351114a073dbc19d2914cd457f2d", + "cross_join.9.9.parquet:md5,accc09150e6b9103bd54c0c9e4ec13a0", + "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", + "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", + "all_counts.parquet:md5,1736df00d0caf241236e64d6da98b925", + "all_genes_summary.csv:md5,9323e53e94eba5317ec0418041610a9d", + "whole_design.csv:md5,3e7748b54a0c25c008d9bd2ddbf1bf00", + "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", + "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", + "common.cpython-313.pyc:md5,5666c1573eb3a2dd9733835d7527bc26", + "genes.cpython-313.pyc:md5,2d45db16037e74010dc3577c3d6187f7", + "samples.cpython-313.pyc:md5,866b8fedb9547d80113abd37bcbba02d", + "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", + "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", + "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", + "graphs.cpython-313.pyc:md5,52b6f67756317cd40915b16ad561f2a4", + "right_sidebar.cpython-313.pyc:md5,9b2921aae3c1c180e49eb608027233fe", + "stores.cpython-313.pyc:md5,e99ea129f976c7c1bf8747a6129c546a", + "tables.cpython-313.pyc:md5,dc4b7170b0f981b1b05be0f183593c30", + "tooltips.cpython-313.pyc:md5,98b60822eb1a4b6ee3d696a364b77378", + "top.cpython-313.pyc:md5,df92052c49b88f9aefe1f26edc510050", + "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", + "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", + "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", + "genes.cpython-313.pyc:md5,3a665f20da183dc279bfd4a5d8f3861d", + "samples.cpython-313.pyc:md5,9332e759e0d216e33c3bd2d55638fce9", + "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", + "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", + "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", + "tables.py:md5,84d17ad7f43458412e550f74a24560e5", + "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", + "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", + "config.cpython-313.pyc:md5,8453d70001d40aef1185d7dc2fcb7a7b", + "data_management.cpython-313.pyc:md5,c2a5f1f6bf9bab0cab754107a06bb855", + "style.cpython-313.pyc:md5,5456916a833a9145562123efb3f04ca5", + "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", + "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", + "style.py:md5,b9f1207f06464e43c05e6e3912f12731", + "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", + "E_MTAB_7711_rnaseq.dataset_stats.csv:md5,370265fa28a847926852fdc1db95afcc", + "E_MTAB_7711_rnaseq.design.csv:md5,3e7748b54a0c25c008d9bd2ddbf1bf00", + "E_MTAB_7711_rnaseq.rnaseq.raw.counts.csv:md5,3c02cf432c29d3751c978439539df388", + "ratios.0.0.parquet:md5,a355c51f1c5d59bc06cf03a151c245b6", + "ratios.0.1.parquet:md5,7af8e11d4236e66ec955bfc91caa6fac", + "ratios.0.10.parquet:md5,6ffffd19518207b53b44e03f961eb2e9", + "ratios.0.11.parquet:md5,5c80fd5580a2552b4913d8b14cf1dc0f", + "ratios.0.12.parquet:md5,127fb52cd8ac2f69db1b67bcde9e4848", + "ratios.0.13.parquet:md5,7730c834668d2e4f100ecaf0a7cd7ab2", + "ratios.0.14.parquet:md5,ea36f4387a0d994c52d4891a1ae176f6", + "ratios.0.15.parquet:md5,db8e85634dce84a7d551aa1aed1471fc", + "ratios.0.16.parquet:md5,d65f4190085c7479d6e0a01aadecba28", + "ratios.0.2.parquet:md5,7e7f759e9d8664bcf3b18976b241bb38", + "ratios.0.3.parquet:md5,d2a7a077b5fcd53f1977e43150104d1c", + "ratios.0.4.parquet:md5,2686077d876375c3f1113d9e8afd48ef", + "ratios.0.5.parquet:md5,e5222bdbf5e767a932301518b09a9acf", + "ratios.0.6.parquet:md5,c69356effc1da780abaaa5a56474ac82", + "ratios.0.7.parquet:md5,42686840fc8841b627a3d92a77b92a95", + "ratios.0.8.parquet:md5,713c07013f4cacb4fa3f3db6291e48fa", + "ratios.0.9.parquet:md5,be56d666d72712799b1504b00449a45a", + "ratios.1.1.parquet:md5,ec0e5a367619e3d22bbc33611ca52b67", + "ratios.1.10.parquet:md5,9341746871f8c6ca773d5a1b9702ec35", + "ratios.1.11.parquet:md5,497642768c01b7a286992f26dfa252ce", + "ratios.1.12.parquet:md5,7ee7421c56211d93adb11868d6372939", + "ratios.1.13.parquet:md5,2fe3986abf0cfea3d5753741dd2fd783", + "ratios.1.14.parquet:md5,6e657f24d5b23ad230cc5b485da23c6c", + "ratios.1.15.parquet:md5,b7bbcf04425751f0723780e296336ddb", + "ratios.1.16.parquet:md5,548e8892eb7d69c875bf987f88b12f2c", + "ratios.1.2.parquet:md5,14a93f127cedff8d76dbe3bcd0baa36e", + "ratios.1.3.parquet:md5,f2fefe175b9624fe0332c6c0b39944df", + "ratios.1.4.parquet:md5,16403a8655ded43018cfe045007c2a73", + "ratios.1.5.parquet:md5,84b4e4d49cf1b1e946886a90186a825f", + "ratios.1.6.parquet:md5,caeddd18b9efdf6127b7cea00522c31c", + "ratios.1.7.parquet:md5,dab3faec99e46dbd3f5ed83c70d1d90c", + "ratios.1.8.parquet:md5,07543c020294f66995f8c7e3efc77728", + "ratios.1.9.parquet:md5,42009140ca8b6977cce3e3bd8800f419", + "ratios.10.10.parquet:md5,e9e146c7ff6779c36da0858f0145a2f7", + "ratios.10.11.parquet:md5,843a8bd51a4421de86120912bfc884c1", + "ratios.10.12.parquet:md5,5a1e858eb8483141deb246dd16922913", + "ratios.10.13.parquet:md5,1b78a23d0c02b51396a074da8978c3db", + "ratios.10.14.parquet:md5,df42a0df447438b8b17d1e0670ce4260", + "ratios.10.15.parquet:md5,046657b57e56c2cbda094d36dfb02163", + "ratios.10.16.parquet:md5,ca647748df2ef7bf22d620c3f95c5bc3", + "ratios.10.2.parquet:md5,925bb322c28181be16e01164ed7e37e0", + "ratios.10.3.parquet:md5,c77f9fbaefbbf6eedf30c268045f7fa5", + "ratios.10.4.parquet:md5,97f8882d2de16b46da6baff729727183", + "ratios.10.5.parquet:md5,b0a2dec08c1577255ba3bd204787488c", + "ratios.10.6.parquet:md5,73fa3845a1aefdf5c7da42ae1e6a54e8", + "ratios.10.7.parquet:md5,66212a87260e1ed357d2816a1452ac47", + "ratios.10.8.parquet:md5,ae400cecd68a636b7b235669b2de4c10", + "ratios.10.9.parquet:md5,bebeeeb1bb03016b4e1ed09ab829cb47", + "ratios.11.11.parquet:md5,36176d02607dac19baaf40b3768e7110", + "ratios.11.12.parquet:md5,fda19923313abbb26790797bda1e5bd0", + "ratios.11.13.parquet:md5,fb0b5e3a9b2b9e6025c7ef466b5f366f", + "ratios.11.14.parquet:md5,a37face0a8388fe416252f897603ffb7", + "ratios.11.15.parquet:md5,80b952f9ade0e7bbe5af33ed2f01a32c", + "ratios.11.16.parquet:md5,db7e2bfe6d7c513ac4b2fc88f2e4b595", + "ratios.11.2.parquet:md5,3467389c29749422573d228ee4489bfb", + "ratios.11.3.parquet:md5,560bb56a6393bc2e0b44f45b649ab189", + "ratios.11.4.parquet:md5,2fbcf5460aaae803a02e9a7148207f64", + "ratios.11.5.parquet:md5,b1785dc5c196ac31866a891edf0f7ab1", + "ratios.11.6.parquet:md5,7bb397eb08c842e729a4dd54cc9e3f47", + "ratios.11.7.parquet:md5,e5721a9e48952759ab11672babb8fc7e", + "ratios.11.8.parquet:md5,7522cdd752e2f1efbd51c997a38d8d85", + "ratios.11.9.parquet:md5,40740c54ac5ead214ca2bafba86c7a4f", + "ratios.12.12.parquet:md5,7b8bfd703c4662fd908290a82ac00f70", + "ratios.12.13.parquet:md5,156ddc68093be2492398d1ef7d997595", + "ratios.12.14.parquet:md5,44ff915c18d65e4cce7ff757e0db3cd0", + "ratios.12.15.parquet:md5,8e8b96e66dd19ab7ebfe2af3257d2e41", + "ratios.12.16.parquet:md5,f820dbe54d70bc24888a6a9c84031447", + "ratios.12.2.parquet:md5,618c02d7c2b9ac3f9ad58606f2c94f04", + "ratios.12.3.parquet:md5,1a7ec23f4e61a404b42153d1e591b98a", + "ratios.12.4.parquet:md5,eac3b290600eaceaddf15cfdb888e9d1", + "ratios.12.5.parquet:md5,68b84e0a27328c887cfab8934484e6b3", + "ratios.12.6.parquet:md5,89ee7fcfa1db720f78f25e45b9e246de", + "ratios.12.7.parquet:md5,df48def02992f838c587ce991468906c", + "ratios.12.8.parquet:md5,8df792a8bed292b93dfdfb96d274aa92", + "ratios.12.9.parquet:md5,a99b6107f6c4f2ec5c7d56c8110f7460", + "ratios.13.13.parquet:md5,5d5f4eee0230969df74a399af7a497cd", + "ratios.13.14.parquet:md5,1c0270fb676760dcb79c2bce06a36142", + "ratios.13.15.parquet:md5,37a17f3a3727e9bfc4431a325caee0fb", + "ratios.13.16.parquet:md5,71c2ed1fb614e8ee912d01110232a40b", + "ratios.13.2.parquet:md5,8653ba639912aab0c954edb4e5e15919", + "ratios.13.3.parquet:md5,88d07a234489857d167f6f872eae5b11", + "ratios.13.4.parquet:md5,ef26cc0a80279b7f0a7bc7327020d70e", + "ratios.13.5.parquet:md5,212f06af08076e7ac12729289128b5f2", + "ratios.13.6.parquet:md5,0b6fc56a7d69628ac24f00379ceb6ab8", + "ratios.13.7.parquet:md5,bca91bdc92a2ab5bfba7cf444b212b51", + "ratios.13.8.parquet:md5,1892d90b46ad394e7499c60dfc9e832f", + "ratios.13.9.parquet:md5,c52a5ac166f333b2a551956067d4d895", + "ratios.14.14.parquet:md5,db0d10a29abf198c2543fa408761cc10", + "ratios.14.15.parquet:md5,9ed98af50494eac98b90a1a8695ca909", + "ratios.14.16.parquet:md5,d53e2b25d03a595bfe934d3e90548a31", + "ratios.14.2.parquet:md5,dcda662a066d65f8ec64eba714d2569b", + "ratios.14.3.parquet:md5,bb88047558e1805edd2b8b0fb07c2d82", + "ratios.14.4.parquet:md5,5caa19f1290bcba7e58060570acf1abf", + "ratios.14.5.parquet:md5,c7ec4aafafcea3089b6a8ff7c53b64d8", + "ratios.14.6.parquet:md5,19b3ecc90f28a467cb3221a2fb6a6b9a", + "ratios.14.7.parquet:md5,097fa4c610288b023e1675162522e812", + "ratios.14.8.parquet:md5,daac142b47596223909740abb0ade1a1", + "ratios.14.9.parquet:md5,1ab7ca2cead2b993268c97a71baaa4bc", + "ratios.15.15.parquet:md5,5b632e0bd1e525e1385b4ba58527321b", + "ratios.15.16.parquet:md5,e529537ab99108e86c33b51adce26783", + "ratios.15.2.parquet:md5,204b4b1be2bf3d031629d76c063d431f", + "ratios.15.3.parquet:md5,7bcc3f85ab24483b6a989aed0f0020c3", + "ratios.15.4.parquet:md5,e90af424ca5bed5bb60a485b7a8ffd60", + "ratios.15.5.parquet:md5,b6fa5ff144f89e68ba2f2af156f27835", + "ratios.15.6.parquet:md5,22061738d4790957a03d4511fd760a8f", + "ratios.15.7.parquet:md5,f302453d1b4901e1c80ab4a07051dd74", + "ratios.15.8.parquet:md5,12674658ce09535aad75ac2dc3ff532c", + "ratios.15.9.parquet:md5,2419458e5ae5f680a4cc4d85f8d62b22", + "ratios.16.16.parquet:md5,61695d3d91a6a1078c7e82adb426aed4", + "ratios.16.2.parquet:md5,5f7ac087a3d090e3fffc65fabbb24c0c", + "ratios.16.3.parquet:md5,131129091bb689a518374aedbd9ff3ca", + "ratios.16.4.parquet:md5,552e4c25759e7d403eaf223d8093194a", + "ratios.16.5.parquet:md5,2b55de4c99e0b272421ffb2281fde1c1", + "ratios.16.6.parquet:md5,080a9d3c5540e5de0d727845ddc6ded6", + "ratios.16.7.parquet:md5,663b1d45e3d60a79149118c7c94c4b45", + "ratios.16.8.parquet:md5,1e09757bff7dfc8419571e5f5a1ff9a0", + "ratios.16.9.parquet:md5,ae735c5c054ab823404d1772463b7b7f", + "ratios.2.2.parquet:md5,63fac44ece88fd443d1483cbeba73072", + "ratios.2.3.parquet:md5,e825c4c94a74875ec35dbc547dbf9628", + "ratios.2.4.parquet:md5,95ae91d43579cfa43a2da4be3b866595", + "ratios.2.5.parquet:md5,511f5f94b767570e5859d19e44590889", + "ratios.2.6.parquet:md5,56c1859fc02eeb0e555e9e92b5237a36", + "ratios.2.7.parquet:md5,5c4f45de615c52db6addfc6cfd65ad47", + "ratios.2.8.parquet:md5,3cd34e8912caf86f8a4a012d38838c17", + "ratios.2.9.parquet:md5,faf7eea2b8baa37132472b1ba49f443d", + "ratios.3.3.parquet:md5,76f440e48b0c25c4995e89fceb6a993a", + "ratios.3.4.parquet:md5,d7ef79ea3d022a02bd3614b69936021e", + "ratios.3.5.parquet:md5,1d5a0d8f2e13424376748bb3d1a7a3bf", + "ratios.3.6.parquet:md5,be8dab0a4a28229ebb716006721c30d0", + "ratios.3.7.parquet:md5,af2ee8da0a77c49f1ed5eb59a6a2f3fc", + "ratios.3.8.parquet:md5,04b227af993ebf74d8b18fcfa0c7b608", + "ratios.3.9.parquet:md5,623903b1402fd1af6ac783b00ef3c159", + "ratios.4.4.parquet:md5,a08007710a0ef6a37e917ca3747bad1d", + "ratios.4.5.parquet:md5,e592677ccf5b184c7622504206ef0d30", + "ratios.4.6.parquet:md5,6e43b1b0cd66ec68cf4dc1832ddea5a6", + "ratios.4.7.parquet:md5,87521100a3feea742124e453f1a70d99", + "ratios.4.8.parquet:md5,4cf05fc4accc86d6397d3dd7f742df38", + "ratios.4.9.parquet:md5,3cc9d27aa578e3f55ba86f4c0b458bd6", + "ratios.5.5.parquet:md5,ea468f80be61cdeea2997747dd480b70", + "ratios.5.6.parquet:md5,02a7a8d8a5cf8bb37da934c9f8c2e5a7", + "ratios.5.7.parquet:md5,52cbf872c6f03e435be1a9d9d4a0cb1f", + "ratios.5.8.parquet:md5,c1939331b44d7744630e1ba78933928a", + "ratios.5.9.parquet:md5,d6f1b39b43472c96aa488c275931eaf2", + "ratios.6.6.parquet:md5,1efa2e29dd6767dc091a9cc05c8a260c", + "ratios.6.7.parquet:md5,e72dddcb5a8c0df89a228fefbfce6056", + "ratios.6.8.parquet:md5,347bcc650582a383d6fb1831729f8bed", + "ratios.6.9.parquet:md5,0e39f2297f79cc66d45b810b52144819", + "ratios.7.7.parquet:md5,ddedf622177fab25bf41c1f0cbc6bc2b", + "ratios.7.8.parquet:md5,585a089e6f34ca5a798d6cbbc7b78944", + "ratios.7.9.parquet:md5,a7a57928a5f9b08aac32132715e4a52d", + "ratios.8.8.parquet:md5,5589622b9f578f5fb944e096c5150d3a", + "ratios.8.9.parquet:md5,afa0680c4ed261ef14b6e2d4985b0b40", + "ratios.9.9.parquet:md5,50aa54172efd0ec34d0742676ddadb64", + "candidate_counts.parquet:md5,84ef96b59bebdabc5824a46ca58c5cd5", + "E_MTAB_7711_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", + "E_MTAB_7711_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", + "E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.csv:md5,3856796c675c7ee3bb6975185f1b869b", + "whole_gene_id_mapping.csv:md5,87c58803a087a768eff2403b40868614", + "whole_gene_metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", + "count_chunk.0.parquet:md5,945ce07af2f6912bc1b04a042ffa4df3", + "count_chunk.1.parquet:md5,f6d15b5960cd58a69ac38c524bfeb44a", + "count_chunk.10.parquet:md5,3f3884f92ca0568ee284ac8d2ee5756a", + "count_chunk.11.parquet:md5,bb694537b13260c63aee27c418fa987a", + "count_chunk.12.parquet:md5,a3f609a812dbe2eeeb1c1fa4deed4bd6", + "count_chunk.13.parquet:md5,d1273e61b59f6edcb292449d4fae3d7d", + "count_chunk.14.parquet:md5,2eb7ecf00cb497883bc81c5cc0ebacc2", + "count_chunk.15.parquet:md5,969d5fb8c8097e36f28b72d29447c33c", + "count_chunk.16.parquet:md5,097c0a41c635a06a81b63deea1c6f098", + "count_chunk.2.parquet:md5,c02192585bbbb94228889f25d1079eb5", + "count_chunk.3.parquet:md5,a935b0bff8e1aba538253fb2d282a815", + "count_chunk.4.parquet:md5,462a8505afe8b705011e56f0532bdfac", + "count_chunk.5.parquet:md5,e499c929f4067ef318ccb55c2168ef34", + "count_chunk.6.parquet:md5,7bc40c9b0a132ddf34c55b5b4762f29a", + "count_chunk.7.parquet:md5,01e560ed2446670049baf47838526663", + "count_chunk.8.parquet:md5,6be163f20e7032633dafa9060b19a364", + "count_chunk.9.parquet:md5,7db8d9e3cd0fbd41fcb2edb2dd3c228d", + "all_counts.parquet:md5,1736df00d0caf241236e64d6da98b925", + "all_counts.parquet:md5,1736df00d0caf241236e64d6da98b925", + "whole_design.csv:md5,3e7748b54a0c25c008d9bd2ddbf1bf00", + "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", + "multiqc_expression_distributions_top_stable_genes.txt:md5,b0a72e2fe2bed3bf3a9f90419462a420", + "multiqc_gene_statistics.txt:md5,12ca1fa8048b26330738fb1dd41b1b4b", + "multiqc_ranked_top_stable_genes_summary.txt:md5,09c98c33edf61c2c71a08d51ec80d2bd", + "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030", + "E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,f7374c86950c01bc6a6b8d8fd415cc8b", + "E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,424ab3200c10539953a74e6f789590a0", + "std.0.0.parquet:md5,f7f1f584fad6e2fb563a256837db16b7", + "std.0.1.parquet:md5,27dfdbac92ef850e34bca6dec964af99", + "std.0.10.parquet:md5,cb4245afac34f688a69331771928601a", + "std.0.11.parquet:md5,2a64bc3da8293d577ce0d768e0e293e9", + "std.0.12.parquet:md5,66963e19755f3921664a008ec60f77e0", + "std.0.13.parquet:md5,da5d199606ea714ab20623a6e798c9c5", + "std.0.14.parquet:md5,4183b3174dcd859ba9a134de40646c94", + "std.0.15.parquet:md5,c4d46549f4fb3ea16b2bbc8316fb1e86", + "std.0.16.parquet:md5,21920021e64e23165a169a34e20923bd", + "std.0.2.parquet:md5,18ae2ca9153343ee7d63bcde0568d360", + "std.0.3.parquet:md5,b21ba0b1790f57e1f617f94ecc6b3dc4", + "std.0.4.parquet:md5,b50352dbca9833e4f4c4fe98eeb59937", + "std.0.5.parquet:md5,7e9ba5aaa8627fc9b5a7af2d6ef7a19e", + "std.0.6.parquet:md5,09ce3da351f69293c9ab3bc6398e5027", + "std.0.7.parquet:md5,44316323b58e59b7b2f9ff58f4d85c94", + "std.0.8.parquet:md5,8882523dff550234de6e3cdec277d5d1", + "std.0.9.parquet:md5,a6992acdc47dd4ccfd6b608d65bd8f19", + "std.1.1.parquet:md5,1b15ccf947fb8fc5a27e308138644a82", + "std.1.10.parquet:md5,11f8f85f8c8d5843131ff37c929fa0a4", + "std.1.11.parquet:md5,16a10598086e9acf0b450d878f60a9af", + "std.1.12.parquet:md5,712d9eba2a240a75ea7418bd8fcca2b1", + "std.1.13.parquet:md5,18949eea624878d245d5566bf2c52eee", + "std.1.14.parquet:md5,933b7117dbbfd550db1a2c249251bb7f", + "std.1.15.parquet:md5,cf9ba2aab349f768231790e9a38ae0a7", + "std.1.16.parquet:md5,e1c982ce291ce93309b7b33543a913ae", + "std.1.2.parquet:md5,71aa5252c1c5570be7d863805da8557b", + "std.1.3.parquet:md5,f41a5c6fd9c20be6e2526271a1d2fea8", + "std.1.4.parquet:md5,15b0ba48a0359b53b91071d78fb619d3", + "std.1.5.parquet:md5,74e83ebd64b4659e50fc2034aedc3337", + "std.1.6.parquet:md5,18a691f273918b31e1ff3c45de7a33c0", + "std.1.7.parquet:md5,cf14eb2142285c6bebafbf255804dbce", + "std.1.8.parquet:md5,4eb995adbfacfc3efb0394f87b1e0fef", + "std.1.9.parquet:md5,2f8b813eb596c0c8499d335136b824cb", + "std.10.10.parquet:md5,68dc78cb4518a1fc3deead1dab9c4e98", + "std.10.11.parquet:md5,e3d0d1d54a452427afee44bb8f8f7bf7", + "std.10.12.parquet:md5,7e45a4df99343383aa4c04261e3f2ff5", + "std.10.13.parquet:md5,7feb69126e290cefe39be2091ff962bb", + "std.10.14.parquet:md5,9766cd3cb29841439cf7139a8bc36ff6", + "std.10.15.parquet:md5,bd22f0aeaaf094778eca461a10672eef", + "std.10.16.parquet:md5,f7ab77bcb1d8baab91521b98f249de2b", + "std.10.2.parquet:md5,ec05ca850903cdca70c8cdf5bca5c77f", + "std.10.3.parquet:md5,11789030e5cd0071a6bd50fe83095354", + "std.10.4.parquet:md5,462f66c742027e3a04019d0655f4c4ca", + "std.10.5.parquet:md5,343c5851906086e7afdf977e4dbc1316", + "std.10.6.parquet:md5,29ba6307c7436c68a5e0750666968dbc", + "std.10.7.parquet:md5,cae20a7090e6fc350bdd10a36ea84444", + "std.10.8.parquet:md5,bca95b87d5bb575346223bcb643f60e5", + "std.10.9.parquet:md5,31e8bc47efc76cd173605372bd1b7eea", + "std.11.11.parquet:md5,979ac87879f83d3e2702bd32bb14c8a8", + "std.11.12.parquet:md5,83cd463128f25b49e76e7e683825f60d", + "std.11.13.parquet:md5,1c643f879df783dca63d3b413b45913b", + "std.11.14.parquet:md5,56f73fa9d507e4a31d1171626541de8b", + "std.11.15.parquet:md5,acdc81f2318990158842af5cd0b02e85", + "std.11.16.parquet:md5,c28dba69d77532178b435397a88e3c77", + "std.11.2.parquet:md5,7727bb854a125e06d5b095b54d99f8e7", + "std.11.3.parquet:md5,c18bc02c96539619815e952051a7d950", + "std.11.4.parquet:md5,547d219f82204d1f0e5b81df24999875", + "std.11.5.parquet:md5,01a68ff717a3d302b21d3817694a0ff4", + "std.11.6.parquet:md5,66379e1c8a5f6a0053bb80b920f2a48e", + "std.11.7.parquet:md5,3549d8f39b52bbe580bbd43b74497271", + "std.11.8.parquet:md5,38c437a312a7c10c9babeb073be3dc0b", + "std.11.9.parquet:md5,c5af0eb1de4fdc955e662347decd6d2b", + "std.12.12.parquet:md5,857da96e1f79bd4732cc40193fff8a13", + "std.12.13.parquet:md5,65d3fdd3a0805839c0d8e5a59d5ec142", + "std.12.14.parquet:md5,47c1675d08fad5ae12904b58a383659c", + "std.12.15.parquet:md5,b416d03b0180bb0e0905acb14ba59516", + "std.12.16.parquet:md5,53d32be9ec57d7b78f5270ffb1b9d983", + "std.12.2.parquet:md5,b2001c980557515107cc16fe31ebe2ba", + "std.12.3.parquet:md5,e072bb955b14bd1d7b9dcceaf944dd64", + "std.12.4.parquet:md5,e2aac77501d07693edf0da6548921d60", + "std.12.5.parquet:md5,44757a40231677ad2b18b33823cdb4ea", + "std.12.6.parquet:md5,47ae297f8e4d33039e85f0da842b034c", + "std.12.7.parquet:md5,9715ee0d5abc89b94467f2c79f457d98", + "std.12.8.parquet:md5,19f9c12a4f8a7ee8d869373f0067e4df", + "std.12.9.parquet:md5,ccc5b0ec564b82b874020ece4ef19c7c", + "std.13.13.parquet:md5,407fc3e78588ea159f850dae379956d1", + "std.13.14.parquet:md5,298a495c0b77165ddaadd054df0fd87f", + "std.13.15.parquet:md5,9d49532e98ad4757b5fd1b52f002b40b", + "std.13.16.parquet:md5,3c4bb049e674cb8a49134b26d5394ba8", + "std.13.2.parquet:md5,4f7725062597614210e64b57f9165ae2", + "std.13.3.parquet:md5,d55749b4e3828300f97e920eb1fac155", + "std.13.4.parquet:md5,5eaeda5db329ce2aded563dd829a92ae", + "std.13.5.parquet:md5,7df214e7ff65db00d3635c003fb572be", + "std.13.6.parquet:md5,fd0769c6433c04f236f90398232eacd6", + "std.13.7.parquet:md5,9d89e6449c132d48db5211ab40fed485", + "std.13.8.parquet:md5,37326ce5d31ded417bb7b875d1b48ff1", + "std.13.9.parquet:md5,6605f549c4c564ca5aa63c05c5808bd1", + "std.14.14.parquet:md5,734bc2fe5a30df286332ddc1e8e612cc", + "std.14.15.parquet:md5,8eb2bd8a653a5ffdf89c1c10a8d64caa", + "std.14.16.parquet:md5,d8ed4f59886c8a2adf4b5fef14c3999b", + "std.14.2.parquet:md5,8e7246d123978e8c14a4b0c8ae089ee7", + "std.14.3.parquet:md5,08d35d497abf204ac33eac16ac6d8e53", + "std.14.4.parquet:md5,81cec87e8958e2627a496e70491f016e", + "std.14.5.parquet:md5,c6148a101cd3a1628bcc2ad8ae2aefc4", + "std.14.6.parquet:md5,8b76227ebeecce0a59a9a18ab3bf5eba", + "std.14.7.parquet:md5,06e1f034cfbd83980277512f26ada3c0", + "std.14.8.parquet:md5,55015f5158f5ab44644b29126f85941d", + "std.14.9.parquet:md5,d6d7403e73fa34982e7f9e03b72f44d1", + "std.15.15.parquet:md5,afb7f4e05042ffec0cae4712299f28ad", + "std.15.16.parquet:md5,b0e232a8c1c1f01b66bc4d40122d2493", + "std.15.2.parquet:md5,071700c13605ad8de1bd517aa7317dfd", + "std.15.3.parquet:md5,dd4d91da12f98439b3d94c308ceab6e0", + "std.15.4.parquet:md5,db4630238515c901f5dc65fedb5f2b92", + "std.15.5.parquet:md5,d53438dc3b40229c24822ec9eb530dcb", + "std.15.6.parquet:md5,dbd6a825933d30c466e201705e3e3cdd", + "std.15.7.parquet:md5,66323071c468bec87555a42e8275c18e", + "std.15.8.parquet:md5,fa41e9a05d7ba4d71c737347fdf2882c", + "std.15.9.parquet:md5,70c2456934546b5ea2036f22f075c0ee", + "std.16.16.parquet:md5,f7b86c99d639c7db86c36b1f1beae643", + "std.16.2.parquet:md5,10f7ee1af80dddad2938599d504ae013", + "std.16.3.parquet:md5,82160c8f78c30beb3550ca43186e888d", + "std.16.4.parquet:md5,280dcf56b02c2580a1ead8273a7e12a9", + "std.16.5.parquet:md5,3a426b7559b1a4b791483887e900567c", + "std.16.6.parquet:md5,1cc73ac6c367a1f2f932d027b9ecb051", + "std.16.7.parquet:md5,fa15b7b1c83df83e7afeaeba0ad566ed", + "std.16.8.parquet:md5,3d9b605eaac2dff80650581d6b160ed0", + "std.16.9.parquet:md5,b0864e293b295b844563a57d62c624aa", + "std.2.2.parquet:md5,e99427127de921e5cc2e7875890e4977", + "std.2.3.parquet:md5,5b4737511f9f9a5cacfb56f68c5eec23", + "std.2.4.parquet:md5,d54785b00e6d39195add2957bc2d6f9f", + "std.2.5.parquet:md5,2ac6ae4554b8950c45f834a9c108bab6", + "std.2.6.parquet:md5,382a27f122f74dc9a65687ead05a6a34", + "std.2.7.parquet:md5,41f174a7c8f7c7f65c562e1e21d655df", + "std.2.8.parquet:md5,ebcabf1007aab0bd81d2d96b210e4aac", + "std.2.9.parquet:md5,65366a1207657e5afb74f27e15dc1c7b", + "std.3.3.parquet:md5,506a62ca65ecc5804f15e6965eed6977", + "std.3.4.parquet:md5,0a9f00e1521538a27b9ecfece22ff849", + "std.3.5.parquet:md5,ed56a7c68aa98476923b09460117d81d", + "std.3.6.parquet:md5,deb6a7883efa1f57601f7656f22babb8", + "std.3.7.parquet:md5,66532d7113804d5213e7074ebe64af0e", + "std.3.8.parquet:md5,06cec858ba2e43e97a035617b51d32a1", + "std.3.9.parquet:md5,2d780e82e9e71eba70619c08f701e972", + "std.4.4.parquet:md5,7418812c4aa5b85dbe9e3796e47e2055", + "std.4.5.parquet:md5,5623218acb89fc86c7bae605c92adeef", + "std.4.6.parquet:md5,1f75e593b4e2d1bbf177c97843bad2a6", + "std.4.7.parquet:md5,c31d0118d869829a098ddb3018c3266d", + "std.4.8.parquet:md5,871ed7aee924e426e36702bf7f118e3c", + "std.4.9.parquet:md5,756eb1acfea0045a596330a85811aec2", + "std.5.5.parquet:md5,52edf8d36ef43fec76b3d9f34218403e", + "std.5.6.parquet:md5,11a81eaa8e0151fcdfa1c7429c6d32ea", + "std.5.7.parquet:md5,828dd6a6b2266ab77e4aaf3ea05a1e37", + "std.5.8.parquet:md5,6889338863e72fc94d0c75a89b2d6aa9", + "std.5.9.parquet:md5,e61e86cad95eded4ed262daf053b47a5", + "std.6.6.parquet:md5,00f6598ec67405bca4b2c87614beefec", + "std.6.7.parquet:md5,29ea4a97b554ce27d778f8606484401d", + "std.6.8.parquet:md5,16c084c6e8c8a601c150800fa7bec5d5", + "std.6.9.parquet:md5,c649e7433cbd7cde59081f2f0f744b4c", + "std.7.7.parquet:md5,20ae4657f1a34d4b5a56f6d0b87c3e56", + "std.7.8.parquet:md5,65b782704f2e6124f159677489cefcae", + "std.7.9.parquet:md5,783798bcb749ce77c05cac8d4a535706", + "std.8.8.parquet:md5,a4e42852f03df97c1dc885a3762545a8", + "std.8.9.parquet:md5,ded56086d0126f3f426eba97bb349ee4", + "std.9.9.parquet:md5,5c852bc2035d149e55ddd0c7bf2c689c", + "m_measures.csv:md5,72860d9c3f88e1a170cb84459e38ecbd", + "stability_values.normfinder.csv:md5,a15d1dc962e43a0065c2954d939cb521" ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T11:05:48.72465951" + "timestamp": "2025-11-08T17:02:15.756268007" }, - "-profile test_accessions_only": { + "-profile test_local_and_downloaded": { + "content": [ + null, + [ + "pipeline_info" + ], + [ + + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-08T16:58:43.646395859" + }, + "-profile test_ignore_errors": { + "content": [ + null, + [ + "pipeline_info" + ], + [ + + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-08T16:58:50.112382172" + }, + "-profile test_download_only": { "content": [ null, [ @@ -43,12 +2088,18 @@ "expression_atlas/accessions/accessions.txt", "expression_atlas/accessions/selected_experiments.metadata.tsv", "expression_atlas/accessions/species_experiments.metadata.tsv", + "expression_atlas/datasets", + "expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", + "expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", "geo", "geo/accessions", "geo/accessions/accessions.tsv", "geo/accessions/geo_all_datasets.metadata.tsv", "geo/accessions/geo_rejected_datasets.metadata.tsv", "geo/accessions/geo_selected_datasets.metadata.tsv", + "geo/datasets", + "geo/datasets/GSE55951.design.csv", + "geo/datasets/GSE55951.microarray.normalised.counts.csv", "multiqc", "multiqc/multiqc_data", "multiqc/multiqc_data/llms-full.txt", @@ -92,16 +2143,20 @@ "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", "accessions.tsv:md5,095fb7f3a666b4382d4ba7296053451b", - "geo_all_datasets.metadata.tsv:md5,47367b8ff7c7ab98c575185c3b51284f", - "geo_rejected_datasets.metadata.tsv:md5,341a71045752afba5a83cedb0c0f23e8", - "geo_selected_datasets.metadata.tsv:md5,47367b8ff7c7ab98c575185c3b51284f", + "geo_all_datasets.metadata.tsv:md5,f5385de48a2e614681d3de4820a28c1e", + "geo_rejected_datasets.metadata.tsv:md5,cf907a70f381df40e044186db1a320a2", + "geo_selected_datasets.metadata.tsv:md5,f5385de48a2e614681d3de4820a28c1e", + "GSE55951.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d", + "GSE55951.microarray.normalised.counts.csv:md5,18fd2d728ad2ec5cb78f994f73375144", "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", "multiqc_eatlas_all_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", "multiqc_eatlas_selected_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", - "multiqc_geo_all_experiments_metadata.txt:md5,6b524d6e36600f1b6d1faadde102e10a", - "multiqc_geo_rejected_experiments_metadata.txt:md5,3b196d575f0051031ebd78a42d24e47a", - "multiqc_geo_selected_experiments_metadata.txt:md5,6b524d6e36600f1b6d1faadde102e10a", + "multiqc_geo_all_experiments_metadata.txt:md5,5e1395382d94f4a8f39c17a89509c5f8", + "multiqc_geo_rejected_experiments_metadata.txt:md5,ea6234d7e4c463fc249aebdf91788df8", + "multiqc_geo_selected_experiments_metadata.txt:md5,5e1395382d94f4a8f39c17a89509c5f8", "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030" ] ], @@ -109,12 +2164,85 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T11:06:24.97488603" + "timestamp": "2025-11-08T16:54:39.147528939" }, - "-profile test_download_only": { + "-profile test_full": { "content": [ null, [ + "aggregate_results", + "aggregate_results/all_counts_filtered.parquet", + "aggregate_results/all_genes_summary.csv", + "aggregate_results/top_stable_genes_summary.csv", + "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", + "base_statistics", + "base_statistics/all", + "base_statistics/all/stats_all_genes.csv", + "base_statistics/rnaseq", + "base_statistics/rnaseq/rnaseq.stats_all_genes.csv", + "clean_count_data", + "clean_count_data/cleaned_counts_filtered.parquet", + "compute_stability_scores", + "compute_stability_scores/stats_with_scores.csv", + "dash_app", + "dash_app/app.py", + "dash_app/assets", + "dash_app/assets/style.css", + "dash_app/data", + "dash_app/data/all_counts.parquet", + "dash_app/data/all_genes_summary.csv", + "dash_app/data/whole_design.csv", + "dash_app/file_system_backend", + "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", + "dash_app/spec-file.txt", + "dash_app/src", + "dash_app/src/callbacks", + "dash_app/src/callbacks/__pycache__", + "dash_app/src/callbacks/__pycache__/common.cpython-313.pyc", + "dash_app/src/callbacks/__pycache__/genes.cpython-313.pyc", + "dash_app/src/callbacks/__pycache__/samples.cpython-313.pyc", + "dash_app/src/callbacks/common.py", + "dash_app/src/callbacks/genes.py", + "dash_app/src/callbacks/samples.py", + "dash_app/src/components", + "dash_app/src/components/__pycache__", + "dash_app/src/components/__pycache__/graphs.cpython-313.pyc", + "dash_app/src/components/__pycache__/right_sidebar.cpython-313.pyc", + "dash_app/src/components/__pycache__/stores.cpython-313.pyc", + "dash_app/src/components/__pycache__/tables.cpython-313.pyc", + "dash_app/src/components/__pycache__/tooltips.cpython-313.pyc", + "dash_app/src/components/__pycache__/top.cpython-313.pyc", + "dash_app/src/components/graphs.py", + "dash_app/src/components/icons.py", + "dash_app/src/components/right_sidebar.py", + "dash_app/src/components/settings", + "dash_app/src/components/settings/__pycache__", + "dash_app/src/components/settings/__pycache__/genes.cpython-313.pyc", + "dash_app/src/components/settings/__pycache__/samples.cpython-313.pyc", + "dash_app/src/components/settings/genes.py", + "dash_app/src/components/settings/samples.py", + "dash_app/src/components/stores.py", + "dash_app/src/components/tables.py", + "dash_app/src/components/tooltips.py", + "dash_app/src/components/top.py", + "dash_app/src/utils", + "dash_app/src/utils/__pycache__", + "dash_app/src/utils/__pycache__/config.cpython-313.pyc", + "dash_app/src/utils/__pycache__/data_management.cpython-313.pyc", + "dash_app/src/utils/__pycache__/style.cpython-313.pyc", + "dash_app/src/utils/config.py", + "dash_app/src/utils/data_management.py", + "dash_app/src/utils/style.py", + "dash_app/versions.yml", + "dataset_statistics", + "dataset_statistics/E_GEOD_61690_rnaseq.dataset_stats.csv", + "dataset_statistics/E_GEOD_77826_rnaseq.dataset_stats.csv", + "dataset_statistics/E_MTAB_4251_rnaseq.dataset_stats.csv", + "dataset_statistics/E_MTAB_4301_rnaseq.dataset_stats.csv", + "dataset_statistics/E_MTAB_5038_rnaseq.dataset_stats.csv", + "dataset_statistics/E_MTAB_5215_rnaseq.dataset_stats.csv", + "dataset_statistics/E_MTAB_552_rnaseq.dataset_stats.csv", + "dataset_statistics/E_MTAB_7711_rnaseq.dataset_stats.csv", "errors", "expression_atlas", "expression_atlas/accessions", @@ -122,17 +2250,60 @@ "expression_atlas/accessions/selected_experiments.metadata.tsv", "expression_atlas/accessions/species_experiments.metadata.tsv", "expression_atlas/datasets", - "expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", - "expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", + "expression_atlas/datasets/E_GEOD_61690_rnaseq.design.csv", + "expression_atlas/datasets/E_GEOD_61690_rnaseq.rnaseq.raw.counts.csv", + "expression_atlas/datasets/E_GEOD_77826_rnaseq.design.csv", + "expression_atlas/datasets/E_GEOD_77826_rnaseq.rnaseq.raw.counts.csv", + "expression_atlas/datasets/E_MTAB_4251_rnaseq.design.csv", + "expression_atlas/datasets/E_MTAB_4251_rnaseq.rnaseq.raw.counts.csv", + "expression_atlas/datasets/E_MTAB_4301_rnaseq.design.csv", + "expression_atlas/datasets/E_MTAB_4301_rnaseq.rnaseq.raw.counts.csv", + "expression_atlas/datasets/E_MTAB_5038_rnaseq.design.csv", + "expression_atlas/datasets/E_MTAB_5038_rnaseq.rnaseq.raw.counts.csv", + "expression_atlas/datasets/E_MTAB_5215_rnaseq.design.csv", + "expression_atlas/datasets/E_MTAB_5215_rnaseq.rnaseq.raw.counts.csv", + "expression_atlas/datasets/E_MTAB_552_rnaseq.design.csv", + "expression_atlas/datasets/E_MTAB_552_rnaseq.rnaseq.raw.counts.csv", + "expression_atlas/datasets/E_MTAB_7711_rnaseq.design.csv", + "expression_atlas/datasets/E_MTAB_7711_rnaseq.rnaseq.raw.counts.csv", "geo", - "geo/accessions", - "geo/accessions/accessions.tsv", - "geo/accessions/geo_all_datasets.metadata.tsv", - "geo/accessions/geo_rejected_datasets.metadata.tsv", - "geo/accessions/geo_selected_datasets.metadata.tsv", - "geo/datasets", - "geo/datasets/GSE55951.design.csv", - "geo/datasets/GSE55951.microarray.normalised.counts.csv", + "geo/excluded_geo_accessions.txt", + "get_candidate_genes", + "get_candidate_genes/candidate_counts.parquet", + "idmapping", + "idmapping/datasets", + "idmapping/datasets/E_GEOD_61690_rnaseq.rnaseq.raw.counts.mapping.csv", + "idmapping/datasets/E_GEOD_61690_rnaseq.rnaseq.raw.counts.metadata.csv", + "idmapping/datasets/E_GEOD_61690_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/datasets/E_GEOD_77826_rnaseq.rnaseq.raw.counts.mapping.csv", + "idmapping/datasets/E_GEOD_77826_rnaseq.rnaseq.raw.counts.metadata.csv", + "idmapping/datasets/E_GEOD_77826_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/datasets/E_MTAB_4251_rnaseq.rnaseq.raw.counts.mapping.csv", + "idmapping/datasets/E_MTAB_4251_rnaseq.rnaseq.raw.counts.metadata.csv", + "idmapping/datasets/E_MTAB_4251_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/datasets/E_MTAB_4301_rnaseq.rnaseq.raw.counts.mapping.csv", + "idmapping/datasets/E_MTAB_4301_rnaseq.rnaseq.raw.counts.metadata.csv", + "idmapping/datasets/E_MTAB_4301_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/datasets/E_MTAB_5038_rnaseq.rnaseq.raw.counts.mapping.csv", + "idmapping/datasets/E_MTAB_5038_rnaseq.rnaseq.raw.counts.metadata.csv", + "idmapping/datasets/E_MTAB_5038_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/datasets/E_MTAB_5215_rnaseq.rnaseq.raw.counts.mapping.csv", + "idmapping/datasets/E_MTAB_5215_rnaseq.rnaseq.raw.counts.metadata.csv", + "idmapping/datasets/E_MTAB_5215_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/datasets/E_MTAB_552_rnaseq.rnaseq.raw.counts.mapping.csv", + "idmapping/datasets/E_MTAB_552_rnaseq.rnaseq.raw.counts.metadata.csv", + "idmapping/datasets/E_MTAB_552_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/datasets/E_MTAB_7711_rnaseq.rnaseq.raw.counts.mapping.csv", + "idmapping/datasets/E_MTAB_7711_rnaseq.rnaseq.raw.counts.metadata.csv", + "idmapping/datasets/E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/whole_gene_id_mapping.csv", + "idmapping/whole_gene_metadata.csv", + "merged_datasets", + "merged_datasets/all", + "merged_datasets/all/all_counts.parquet", + "merged_datasets/rnaseq", + "merged_datasets/rnaseq/all_counts.parquet", + "merged_datasets/whole_design.csv", "multiqc", "multiqc/multiqc_data", "multiqc/multiqc_data/llms-full.txt", @@ -142,62 +2313,216 @@ "multiqc/multiqc_data/multiqc_data.json", "multiqc/multiqc_data/multiqc_eatlas_all_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_rejected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_selected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", + "multiqc/multiqc_data/multiqc_gene_statistics.txt", + "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", "multiqc/multiqc_plots", "multiqc/multiqc_plots/pdf", "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_rejected_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_selected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", + "multiqc/multiqc_plots/pdf/gene_statistics.pdf", + "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", "multiqc/multiqc_plots/png", "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_rejected_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_selected_experiments_metadata.png", + "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", + "multiqc/multiqc_plots/png/gene_statistics.png", + "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", "multiqc/multiqc_plots/svg", "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_rejected_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_selected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", + "multiqc/multiqc_plots/svg/gene_statistics.svg", + "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", + "normalised", + "normalised/E_GEOD_61690_rnaseq", + "normalised/E_GEOD_61690_rnaseq/normalisation_deseq2", + "normalised/E_GEOD_61690_rnaseq/normalisation_deseq2/E_GEOD_61690_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normalised/E_GEOD_77826_rnaseq", + "normalised/E_GEOD_77826_rnaseq/normalisation_deseq2", + "normalised/E_GEOD_77826_rnaseq/normalisation_deseq2/E_GEOD_77826_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normalised/E_MTAB_4251_rnaseq", + "normalised/E_MTAB_4251_rnaseq/normalisation_deseq2", + "normalised/E_MTAB_4251_rnaseq/normalisation_deseq2/E_MTAB_4251_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normalised/E_MTAB_4301_rnaseq", + "normalised/E_MTAB_4301_rnaseq/normalisation_deseq2", + "normalised/E_MTAB_4301_rnaseq/normalisation_deseq2/E_MTAB_4301_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normalised/E_MTAB_5038_rnaseq", + "normalised/E_MTAB_5038_rnaseq/normalisation_deseq2", + "normalised/E_MTAB_5038_rnaseq/normalisation_deseq2/E_MTAB_5038_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normalised/E_MTAB_5215_rnaseq", + "normalised/E_MTAB_5215_rnaseq/normalisation_deseq2", + "normalised/E_MTAB_5215_rnaseq/normalisation_deseq2/E_MTAB_5215_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normalised/E_MTAB_552_rnaseq", + "normalised/E_MTAB_552_rnaseq/normalisation_deseq2", + "normalised/E_MTAB_552_rnaseq/normalisation_deseq2/E_MTAB_552_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normalised/E_MTAB_7711_rnaseq", + "normalised/E_MTAB_7711_rnaseq/normalisation_deseq2", + "normalised/E_MTAB_7711_rnaseq/normalisation_deseq2/E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", "pipeline_info", "pipeline_info/software_mqc_versions.yml", + "quantile_normalised", + "quantile_normalised/E_GEOD_61690_rnaseq", + "quantile_normalised/E_GEOD_61690_rnaseq/E_GEOD_61690_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", + "quantile_normalised/E_GEOD_77826_rnaseq", + "quantile_normalised/E_GEOD_77826_rnaseq/E_GEOD_77826_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", + "quantile_normalised/E_MTAB_4251_rnaseq", + "quantile_normalised/E_MTAB_4251_rnaseq/E_MTAB_4251_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", + "quantile_normalised/E_MTAB_4301_rnaseq", + "quantile_normalised/E_MTAB_4301_rnaseq/E_MTAB_4301_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", + "quantile_normalised/E_MTAB_5038_rnaseq", + "quantile_normalised/E_MTAB_5038_rnaseq/E_MTAB_5038_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", + "quantile_normalised/E_MTAB_5215_rnaseq", + "quantile_normalised/E_MTAB_5215_rnaseq/E_MTAB_5215_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", + "quantile_normalised/E_MTAB_552_rnaseq", + "quantile_normalised/E_MTAB_552_rnaseq/E_MTAB_552_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", + "quantile_normalised/E_MTAB_7711_rnaseq", + "quantile_normalised/E_MTAB_7711_rnaseq/E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", + "stability_scoring", + "stability_scoring/normfinder", + "stability_scoring/normfinder/stability_values.normfinder.csv", "warnings" ], [ - "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", - "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", - "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", - "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", - "accessions.tsv:md5,095fb7f3a666b4382d4ba7296053451b", - "geo_all_datasets.metadata.tsv:md5,f7369624c52232498b5bc2454fa0821a", - "geo_rejected_datasets.metadata.tsv:md5,341a71045752afba5a83cedb0c0f23e8", - "geo_selected_datasets.metadata.tsv:md5,f7369624c52232498b5bc2454fa0821a", - "GSE55951.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d", - "GSE55951.microarray.normalised.counts.csv:md5,18fd2d728ad2ec5cb78f994f73375144", + "all_counts_filtered.parquet:md5,e4aad0044b83171b71b8a3926eccfd1a", + "all_genes_summary.csv:md5,39a8d7ef0b72232c352cd1b63b9d83ed", + "top_stable_genes_summary.csv:md5,daa39aaa66ab7c4ad0240c6d97b27226", + "top_stable_genes_transposed_counts_filtered.csv:md5,684b2eab39b01aa1f08317c437cfe0fb", + "stats_all_genes.csv:md5,979b60306cb4a375383bf7f251d420e2", + "rnaseq.stats_all_genes.csv:md5,43a6eabdd569890e442f5e95432b6fa5", + "cleaned_counts_filtered.parquet:md5,48880f086280859779d81905bacc0fc1", + "stats_with_scores.csv:md5,df4018b2d1bbfd19e29e81cad8ff6b81", + "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", + "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", + "all_counts.parquet:md5,1f19d098af2c813d0ce391b9eda53218", + "all_genes_summary.csv:md5,39a8d7ef0b72232c352cd1b63b9d83ed", + "whole_design.csv:md5,112168cfd2cc4aad6154fd0aefa7e8fa", + "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", + "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", + "common.cpython-313.pyc:md5,b09a65f4db6d64f2be3394c0cd4f5d53", + "genes.cpython-313.pyc:md5,24d7b73d83310718cd468fe966c1d393", + "samples.cpython-313.pyc:md5,ec0b78afc64755487ef0263a7b8db643", + "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", + "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", + "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", + "graphs.cpython-313.pyc:md5,ea20facda8832bc5ec08dd7058a92c5a", + "right_sidebar.cpython-313.pyc:md5,9fd04280bc6928eae4628baf0f8f931f", + "stores.cpython-313.pyc:md5,f583ebec33388147afb10fcc3bab42a1", + "tables.cpython-313.pyc:md5,e619c4a46cd6f57e3df0fffcd75fde30", + "tooltips.cpython-313.pyc:md5,ee71d4ad971477121f15309e14115166", + "top.cpython-313.pyc:md5,32bdc9e369ee7176e9e2a89dba3b4cac", + "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", + "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", + "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", + "genes.cpython-313.pyc:md5,0a9c01319b67e0b702d0d363a751b6b6", + "samples.cpython-313.pyc:md5,886f59d65dde1182be41a785727a85a4", + "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", + "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", + "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", + "tables.py:md5,84d17ad7f43458412e550f74a24560e5", + "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", + "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", + "config.cpython-313.pyc:md5,052a0caba837d667d76fbf55cf8a2224", + "data_management.cpython-313.pyc:md5,62a8bafd3de9d0e9fd8991315667d341", + "style.cpython-313.pyc:md5,67486186d18ce48b2e2be38bb2c4f2e8", + "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", + "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", + "style.py:md5,b9f1207f06464e43c05e6e3912f12731", + "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", + "E_GEOD_61690_rnaseq.dataset_stats.csv:md5,a32803af6640f8b67def869a8e13f3a2", + "E_GEOD_77826_rnaseq.dataset_stats.csv:md5,d7f8d0ccc6a639a8b264d2a6cba84240", + "E_MTAB_4251_rnaseq.dataset_stats.csv:md5,004f8331e0780519ffaede673bee01e8", + "E_MTAB_4301_rnaseq.dataset_stats.csv:md5,abc0d94f32b6b372e1094fe3160e08a1", + "E_MTAB_5038_rnaseq.dataset_stats.csv:md5,7f278757f7aac91d38d6898e10516e1d", + "E_MTAB_5215_rnaseq.dataset_stats.csv:md5,f7c428db2c5918a2c83fe17624bb5db7", + "E_MTAB_552_rnaseq.dataset_stats.csv:md5,c15c5c7f9a9d9f3e3616c3c72f0cc285", + "E_MTAB_7711_rnaseq.dataset_stats.csv:md5,430d11fb6abbdf111dcd1d42e95e2548", + "accessions.txt:md5,dad63ac7a1715277fa44567bc40b5872", + "selected_experiments.metadata.tsv:md5,15baa430176fcadf7bf03034fa9e95c6", + "species_experiments.metadata.tsv:md5,15baa430176fcadf7bf03034fa9e95c6", + "E_GEOD_61690_rnaseq.design.csv:md5,ba807caf74e0b55f5c3fe23810a89560", + "E_GEOD_61690_rnaseq.rnaseq.raw.counts.csv:md5,2e3f1a125b3d41d622e2d24447620eb3", + "E_GEOD_77826_rnaseq.design.csv:md5,5aa61df754aa9c6c107b247c642d2e53", + "E_GEOD_77826_rnaseq.rnaseq.raw.counts.csv:md5,85cea79c602a9924d5a4d6b597ef5530", + "E_MTAB_4251_rnaseq.design.csv:md5,4f3ef7b76ca6ed1ec3157295bc4a8d84", + "E_MTAB_4251_rnaseq.rnaseq.raw.counts.csv:md5,5cf27be0e00b93d5d431754ba8058687", + "E_MTAB_4301_rnaseq.design.csv:md5,165eeef7d612c01fd62baae1a3f296ae", + "E_MTAB_4301_rnaseq.rnaseq.raw.counts.csv:md5,1ab49feea238e7b1419937b5037952b5", + "E_MTAB_5038_rnaseq.design.csv:md5,352ed3163d7deef2be35d899418d5ad4", + "E_MTAB_5038_rnaseq.rnaseq.raw.counts.csv:md5,b4acb3d7c39cdb2bd6cef6c9314c5b2a", + "E_MTAB_5215_rnaseq.design.csv:md5,2741dcd5b45bacce865db632f626a273", + "E_MTAB_5215_rnaseq.rnaseq.raw.counts.csv:md5,273704bdf762c342271b33958a84d1e7", + "E_MTAB_552_rnaseq.design.csv:md5,b81490696f638e90c1cf14236bb0c08c", + "E_MTAB_552_rnaseq.rnaseq.raw.counts.csv:md5,830f50b60b17b62f9ca2f6a163a2879f", + "E_MTAB_7711_rnaseq.design.csv:md5,3e7748b54a0c25c008d9bd2ddbf1bf00", + "E_MTAB_7711_rnaseq.rnaseq.raw.counts.csv:md5,3c02cf432c29d3751c978439539df388", + "excluded_geo_accessions.txt:md5,3b29ba0fe90a301ef71639760fc8e5a9", + "candidate_counts.parquet:md5,b3793a0dd3a06f2d7e16023456aac9ca", + "E_GEOD_61690_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", + "E_GEOD_61690_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", + "E_GEOD_61690_rnaseq.rnaseq.raw.counts.renamed.csv:md5,ccb6efdfb49bc4057618a2a10ec880b7", + "E_GEOD_77826_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", + "E_GEOD_77826_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", + "E_GEOD_77826_rnaseq.rnaseq.raw.counts.renamed.csv:md5,9f933a357deca364f73ca96aa6fa8c5a", + "E_MTAB_4251_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", + "E_MTAB_4251_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", + "E_MTAB_4251_rnaseq.rnaseq.raw.counts.renamed.csv:md5,74aa87fe4102ae828b1bfb0dc7e2bb3a", + "E_MTAB_4301_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", + "E_MTAB_4301_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", + "E_MTAB_4301_rnaseq.rnaseq.raw.counts.renamed.csv:md5,d7a9d4f33e612a803d0032cf1a8b6677", + "E_MTAB_5038_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", + "E_MTAB_5038_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", + "E_MTAB_5038_rnaseq.rnaseq.raw.counts.renamed.csv:md5,07fb1b79dff4a159c9814225dd947d30", + "E_MTAB_5215_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", + "E_MTAB_5215_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", + "E_MTAB_5215_rnaseq.rnaseq.raw.counts.renamed.csv:md5,f3a308689af31ba086fc5f341f0589a4", + "E_MTAB_552_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", + "E_MTAB_552_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", + "E_MTAB_552_rnaseq.rnaseq.raw.counts.renamed.csv:md5,449f7c179fc675784f15f58735196644", + "E_MTAB_7711_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", + "E_MTAB_7711_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", + "E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.csv:md5,3856796c675c7ee3bb6975185f1b869b", + "whole_gene_id_mapping.csv:md5,87c58803a087a768eff2403b40868614", + "whole_gene_metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", + "all_counts.parquet:md5,1f19d098af2c813d0ce391b9eda53218", + "all_counts.parquet:md5,e2ca2b6898a28f583f41994cff8e9e34", + "whole_design.csv:md5,112168cfd2cc4aad6154fd0aefa7e8fa", "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", - "multiqc_eatlas_all_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", - "multiqc_eatlas_selected_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", - "multiqc_geo_all_experiments_metadata.txt:md5,daf7c9d1abf16831d9e1a24482393635", - "multiqc_geo_rejected_experiments_metadata.txt:md5,3b196d575f0051031ebd78a42d24e47a", - "multiqc_geo_selected_experiments_metadata.txt:md5,daf7c9d1abf16831d9e1a24482393635", - "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030" + "multiqc_eatlas_all_experiments_metadata.txt:md5,a2b6cf46faf4b8c7dcdae06e17b35432", + "multiqc_eatlas_selected_experiments_metadata.txt:md5,a2b6cf46faf4b8c7dcdae06e17b35432", + "multiqc_expression_distributions_top_stable_genes.txt:md5,a8b7a3a0d3ecdbbb3874b1a8e32c5425", + "multiqc_gene_statistics.txt:md5,c07cdee51db5d4f42f4987564098b4de", + "multiqc_ranked_top_stable_genes_summary.txt:md5,b73b4b702dea4bf30fdf5f0c62f4582c", + "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030", + "E_GEOD_61690_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,4bc05ef9eff93062591fc37ae93b830d", + "E_GEOD_77826_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,0e4f0b0a0276f3a835d26d4b518296f2", + "E_MTAB_4251_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,c9d0a14d809fe994abb1280ee7c00612", + "E_MTAB_4301_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,cdd0db06fdcfb4261853a63b5527c4a1", + "E_MTAB_5038_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,7f72ebf195c0dfb26d13c09157ea1914", + "E_MTAB_5215_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,e427282767b3833f899b1a03bc40edc2", + "E_MTAB_552_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,26e7b283f3c901cc417c1fe0e6b9d555", + "E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,f7374c86950c01bc6a6b8d8fd415cc8b", + "E_GEOD_61690_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,44769b8573e9bd8b09c46bb5e5e5404c", + "E_GEOD_77826_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,ad69a1121e3359058763506c736430ab", + "E_MTAB_4251_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,dc9bf18fc110383081768702d05e711a", + "E_MTAB_4301_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,5511120c972162c05586be3a918ba35e", + "E_MTAB_5038_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,5735835a7817e75b1789f48fed702c44", + "E_MTAB_5215_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,0a3961314238ec57c7144a9781466ac3", + "E_MTAB_552_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,8b801cd4f42277a757d1360be54c9940", + "E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,51c0e455009e214c856a2ef6418b01d2", + "stability_values.normfinder.csv:md5,614ee8b42bd3bc4fe1a4663cc2b7463d" ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T11:07:13.377517982" + "timestamp": "2025-11-08T17:21:19.722455668" }, "-profile test_one_rnaseq_one_microarray": { "content": [ @@ -345,60 +2670,60 @@ "warnings" ], [ - "all_counts_filtered.parquet:md5,9dd07574791da408a3c00064774fd2a3", - "all_genes_summary.csv:md5,720e3ec50de0227cbfbce9cb0fd542f8", - "top_stable_genes_summary.csv:md5,0f86d98eb45898a80923232774e8ee81", - "top_stable_genes_transposed_counts_filtered.csv:md5,de8bf611ffbb33b1f600a13be0904fad", - "stats_all_genes.csv:md5,0561def8497c2c5d2291653105a4296e", - "microarray.stats_all_genes.csv:md5,449a0e1a08a6e1ec4ced368a8ab1538f", - "rnaseq.stats_all_genes.csv:md5,c8eb12ea25bc4368cf55753ed40b4271", - "cleaned_counts_filtered.parquet:md5,d1bb960ef0ca8d34430ef34a52b3ca96", - "stats_with_scores.csv:md5,bfc234a1d8c8cc0d53b102c5675e9988", + "all_counts_filtered.parquet:md5,f866985e43c480776dfcb05c8c06e49b", + "all_genes_summary.csv:md5,69ba69d9a58db2d10d4f17aecc41697b", + "top_stable_genes_summary.csv:md5,aed44b3253bcf143a636941ceefc3f9e", + "top_stable_genes_transposed_counts_filtered.csv:md5,a1f20aa526443cd3f54daafa6402b55d", + "stats_all_genes.csv:md5,0b52f1317e69e96d80d3690096bbce92", + "microarray.stats_all_genes.csv:md5,391c29159d970a72e03060c45437061d", + "rnaseq.stats_all_genes.csv:md5,d7ff8d9fa2d05ada58739875d88e06ce", + "cleaned_counts_filtered.parquet:md5,be312e13ec2ab8b57c663f3ecdde87f3", + "stats_with_scores.csv:md5,5c548d370d73f8cfb43fcd13a876e993", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_counts.parquet:md5,c3e514cb129fffb55c0152b9116b81c5", - "all_genes_summary.csv:md5,720e3ec50de0227cbfbce9cb0fd542f8", + "all_counts.parquet:md5,ddf26512efc2ea97257f6f634619491f", + "all_genes_summary.csv:md5,69ba69d9a58db2d10d4f17aecc41697b", "whole_design.csv:md5,91d69f908aab581087541383a9cc1025", "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", - "common.cpython-313.pyc:md5,0f0ff495fcb7243cc1188e601c374450", - "genes.cpython-313.pyc:md5,a8f7cb8770d654e7841291b9ef099e9c", - "samples.cpython-313.pyc:md5,0a8def370e4fe1e790ad572a84fc14c5", + "common.cpython-313.pyc:md5,cc7cb7ed16434b4a43ab5782241627de", + "genes.cpython-313.pyc:md5,8a92ae29ed9974eb29248c4e21e0715c", + "samples.cpython-313.pyc:md5,fb346c2f612de7500a1f5b76afee7e8d", "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", - "graphs.cpython-313.pyc:md5,6346dada33930b855e36e19eb96c6d29", - "right_sidebar.cpython-313.pyc:md5,1b209dbfa96e1f627952991f4301bf79", - "stores.cpython-313.pyc:md5,6cade39e1ae1f346befb8220f24c08b2", - "tables.cpython-313.pyc:md5,0fe34dad12b08318b334021f939f68d1", - "tooltips.cpython-313.pyc:md5,38d97cad354496df1cbbd32c11decc35", - "top.cpython-313.pyc:md5,3e31c5e39d2acbd49847378a23e33b1b", + "graphs.cpython-313.pyc:md5,eea82c4a14212014260922068ae5ed04", + "right_sidebar.cpython-313.pyc:md5,94c760ce32040f3b5ea37b5d3e2c3a19", + "stores.cpython-313.pyc:md5,832b069bf682ce8907c19b81edb2c10f", + "tables.cpython-313.pyc:md5,d24f656b9aa718bf746aded25a487a84", + "tooltips.cpython-313.pyc:md5,73d1bc477c66be7d8f2b5230a8d00572", + "top.cpython-313.pyc:md5,37a20595a428164c20090d64314ae0fb", "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", - "genes.cpython-313.pyc:md5,3119d2a9456b328722d503da5b07653a", - "samples.cpython-313.pyc:md5,7a2e47b0fac7db2b763035aa9d00f830", + "genes.cpython-313.pyc:md5,856ff23441233f9bf5eed8e62f75a66d", + "samples.cpython-313.pyc:md5,8a74deeb75fa6768968e8fd7d2ebff37", "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", "tables.py:md5,84d17ad7f43458412e550f74a24560e5", "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.cpython-313.pyc:md5,62544cb677a32b64691d14ec763c9451", - "data_management.cpython-313.pyc:md5,240a7e00e5c6186e54225cafed7354d7", - "style.cpython-313.pyc:md5,1ac3d137e3a4775de24f91352a6a0b0f", + "config.cpython-313.pyc:md5,b83f5941ca31e47a79675883fb0c7c07", + "data_management.cpython-313.pyc:md5,9c035c628e12438445367c1e93677105", + "style.cpython-313.pyc:md5,95e8e3da5520837f0e8e897d676c6e6c", "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", "style.py:md5,b9f1207f06464e43c05e6e3912f12731", "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "E_GEOD_21945_A_AFFY_2.dataset_stats.csv:md5,c9fd75d794c11b7f97ee111b743e040d", - "E_GEOD_52806_rnaseq.dataset_stats.csv:md5,a28d66897810174044e9ca30f8e4675e", + "E_GEOD_21945_A_AFFY_2.dataset_stats.csv:md5,5c80252d4ffa083f1c898580e9521d79", + "E_GEOD_52806_rnaseq.dataset_stats.csv:md5,444997cea92417085759f839d946da98", "E_GEOD_21945_A_AFFY_2.design.csv:md5,3c2cc68528e555a885176d845f04d422", "E_GEOD_21945_A_AFFY_2.microarray.normalised.counts.csv:md5,647f1f8b46457e201af3903be3326654", "E_GEOD_52806_rnaseq.design.csv:md5,a0f1f0f86a2655f526d0cc099d5b6adc", "E_GEOD_52806_rnaseq.rnaseq.raw.counts.csv:md5,da027fc750b0f00dca199122a67feac7", "excluded_geo_accessions.txt:md5,3d247b02a5dfe0064be4c09ceb270c27", - "candidate_counts.parquet:md5,4ef144ae7c7c09e6d47117405eb47b37", + "candidate_counts.parquet:md5,bba3c2a48acf01c14f31109564197130", "E_GEOD_21945_A_AFFY_2.microarray.normalised.counts.mapping.csv:md5,e00b6804a4ba7ff25c708a9c6a4770f8", "E_GEOD_21945_A_AFFY_2.microarray.normalised.counts.metadata.csv:md5,7efa12130b8d5bcdb79d630d07fec5d3", "E_GEOD_21945_A_AFFY_2.microarray.normalised.counts.renamed.csv:md5,91576c77a0cff0116ef8aad73281b729", @@ -407,25 +2732,41 @@ "E_GEOD_52806_rnaseq.rnaseq.raw.counts.renamed.csv:md5,b8774e7b2c57ac8763dfd244c2c9b0f9", "whole_gene_id_mapping.csv:md5,b7d2972efa44bdf1903702e260189b68", "whole_gene_metadata.csv:md5,a08ab44a98542a8529d2a4b5a47e4408", - "all_counts.parquet:md5,c3e514cb129fffb55c0152b9116b81c5", - "all_counts.parquet:md5,f23c8b1ca3930141ea7016d516703e2c", - "all_counts.parquet:md5,f7a2c9c30f41a91fc4b71cb521cdb2c8", + "all_counts.parquet:md5,ddf26512efc2ea97257f6f634619491f", + "all_counts.parquet:md5,7bea3b16ef009981cb1d6bdf5ce3c7e6", + "all_counts.parquet:md5,a0b9095a1806043a58cdcab4a0145829", "whole_design.csv:md5,91d69f908aab581087541383a9cc1025", "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", - "multiqc_expression_distributions_top_stable_genes.txt:md5,6da36ac9ecb493270d9e7b4b04281cd4", - "multiqc_gene_statistics.txt:md5,6e3b76f372c326055c6f302d8fc6594d", - "multiqc_ranked_top_stable_genes_summary.txt:md5,4a1ea438cfcc3f6a8f2ffcbabf87dec2", + "multiqc_expression_distributions_top_stable_genes.txt:md5,6335dcc35e436e1e0745d384a2aa9ced", + "multiqc_gene_statistics.txt:md5,959a3cd171c7d0d6798a54dcdc651134", + "multiqc_ranked_top_stable_genes_summary.txt:md5,520eef70d6c205719e6b1e173174bcfd", "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030", "E_GEOD_52806_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,b3b7bd2d373cd9f2e84c015c4d3c9d67", - "E_GEOD_21945_A_AFFY_2.microarray.normalised.counts.renamed.quant_norm.parquet:md5,2a27fa11e3e5143313f40d16b3815adf", - "E_GEOD_52806_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,bca82a0a962312e90a02096438b5eed7", - "stability_values.normfinder.csv:md5,9d6fb3c72f8dcedfc1c4295f9b2b7a6f" + "E_GEOD_21945_A_AFFY_2.microarray.normalised.counts.renamed.quant_norm.parquet:md5,3d9a59c5990674e632770aaf9557c733", + "E_GEOD_52806_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,932550ef062957fecc7d8e5da27c935b", + "stability_values.normfinder.csv:md5,86892f38ee9d3dc5e42710a4a873ddad" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-08T16:56:59.861500569" + }, + "-profile test_dataset_custom_mapping": { + "content": [ + null, + [ + "pipeline_info" + ], + [ + ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T11:10:17.827869101" + "timestamp": "2025-11-08T17:05:31.497015342" } } \ No newline at end of file diff --git a/tests/modules/local/aggregate_results/main.nf.test.snap b/tests/modules/local/aggregate_results/main.nf.test.snap deleted file mode 100644 index ba15af1f..00000000 --- a/tests/modules/local/aggregate_results/main.nf.test.snap +++ /dev/null @@ -1,100 +0,0 @@ -{ - "Without microarray": { - "content": [ - { - "0": [ - "all_genes_summary.csv:md5,3b49b24a0cb36b9e35b37410917de0b1" - ], - "1": [ - "top_stable_genes_summary.csv:md5,ba17539a8a7b462f0e455dd4f81c5e62" - ], - "2": [ - "all_counts_filtered.parquet:md5,286e7595b3a7b961e39976585d3c106e" - ], - "3": [ - "top_stable_genes_transposed_counts_filtered.csv:md5,0380b330c64ce9efe9644f84e7a5cddb" - ], - "4": [ - [ - "AGGREGATE_RESULTS", - "python", - "3.12.8" - ] - ], - "5": [ - [ - "AGGREGATE_RESULTS", - "polars", - "1.17.1" - ] - ], - "all_counts_filtered": [ - "all_counts_filtered.parquet:md5,286e7595b3a7b961e39976585d3c106e" - ], - "all_genes_summary": [ - "all_genes_summary.csv:md5,3b49b24a0cb36b9e35b37410917de0b1" - ], - "top_stable_genes_summary": [ - "top_stable_genes_summary.csv:md5,ba17539a8a7b462f0e455dd4f81c5e62" - ], - "top_stable_genes_transposed_counts_filtered": [ - "top_stable_genes_transposed_counts_filtered.csv:md5,0380b330c64ce9efe9644f84e7a5cddb" - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-08T16:26:52.3050561" - }, - "With microarray": { - "content": [ - { - "0": [ - "all_genes_summary.csv:md5,da32872124a121e27f3dc1c784c8601b" - ], - "1": [ - "top_stable_genes_summary.csv:md5,da32872124a121e27f3dc1c784c8601b" - ], - "2": [ - "all_counts_filtered.parquet:md5,286e7595b3a7b961e39976585d3c106e" - ], - "3": [ - "top_stable_genes_transposed_counts_filtered.csv:md5,0380b330c64ce9efe9644f84e7a5cddb" - ], - "4": [ - [ - "AGGREGATE_RESULTS", - "python", - "3.12.8" - ] - ], - "5": [ - [ - "AGGREGATE_RESULTS", - "polars", - "1.17.1" - ] - ], - "all_counts_filtered": [ - "all_counts_filtered.parquet:md5,286e7595b3a7b961e39976585d3c106e" - ], - "all_genes_summary": [ - "all_genes_summary.csv:md5,da32872124a121e27f3dc1c784c8601b" - ], - "top_stable_genes_summary": [ - "top_stable_genes_summary.csv:md5,da32872124a121e27f3dc1c784c8601b" - ], - "top_stable_genes_transposed_counts_filtered": [ - "top_stable_genes_transposed_counts_filtered.csv:md5,0380b330c64ce9efe9644f84e7a5cddb" - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-08T16:26:56.797009735" - } -} \ No newline at end of file diff --git a/tests/modules/local/clean_count_data/main.nf.test b/tests/modules/local/clean_count_data/main.nf.test index 12967ff4..dc131785 100644 --- a/tests/modules/local/clean_count_data/main.nf.test +++ b/tests/modules/local/clean_count_data/main.nf.test @@ -37,7 +37,8 @@ nextflow_process { input[0] = [ [dataset: 'test'], file( '$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet', checkIfExists: true), - file( '$projectDir/tests/test_data/dataset_statistics/input/output/test.dataset_stats.csv', checkIfExists: true) + file( '$projectDir/tests/test_data/dataset_statistics/output/test.dataset_stats.csv', checkIfExists: true) + ] input[1] = 0.1 """ diff --git a/tests/modules/local/idmapping/gprofiler/main.nf.test b/tests/modules/local/idmapping/gprofiler/main.nf.test index 51639da7..06a58471 100644 --- a/tests/modules/local/idmapping/gprofiler/main.nf.test +++ b/tests/modules/local/idmapping/gprofiler/main.nf.test @@ -180,4 +180,33 @@ nextflow_process { } + test("Custom mapping - TSV") { + + tag "custom_mapping_tsv" + + when { + process { + """ + input[0] = Channel.of( + [ + [ dataset: "test" ], + file("$projectDir/tests/test_data/idmapping/tsv/counts.ensembl_ids.tsv", checkIfExists: true) + ] + ) + input[1] = "Beta vulgaris" + input[2] = Channel.fromPath( "$projectDir/tests/test_data/idmapping/tsv/mapping.tsv", checkIfExists: true) + input[3] = Channel.fromPath( "$projectDir/tests/test_data/idmapping/tsv/metadata.tsv", checkIfExists: true) + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + } diff --git a/tests/modules/local/idmapping/gprofiler/main.nf.test.snap b/tests/modules/local/idmapping/gprofiler/main.nf.test.snap index b8aafef2..9b78bb9d 100644 --- a/tests/modules/local/idmapping/gprofiler/main.nf.test.snap +++ b/tests/modules/local/idmapping/gprofiler/main.nf.test.snap @@ -1,4 +1,64 @@ { + "Custom mapping - TSV": { + "content": [ + { + "0": [ + [ + { + "dataset": "test" + }, + "counts.ensembl_ids.renamed.csv:md5,b2f45fd17fcb2688ea08b94527f70c1f" + ] + ], + "1": [ + "counts.ensembl_ids.metadata.csv:md5,af7e8d352d9c4591d53d95f660457beb" + ], + "2": [ + "counts.ensembl_ids.mapping.csv:md5,6ff8d8f71b9df7a1b08ff0bfda8da755" + ], + "3": [ + + ], + "4": [ + [ + "GPROFILER_IDMAPPING", + "python", + "3.13.5" + ] + ], + "5": [ + [ + "GPROFILER_IDMAPPING", + "pandas", + "2.3.1" + ] + ], + "6": [ + [ + "GPROFILER_IDMAPPING", + "requests", + "2.32.4" + ] + ], + "counts": [ + [ + { + "dataset": "test" + }, + "counts.ensembl_ids.renamed.csv:md5,b2f45fd17fcb2688ea08b94527f70c1f" + ] + ], + "metadata": [ + "counts.ensembl_ids.metadata.csv:md5,af7e8d352d9c4591d53d95f660457beb" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-08T19:45:48.962861272" + }, "Custom mapping": { "content": [ { diff --git a/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test.snap b/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test.snap index 1eb87d3a..87e35d9e 100644 --- a/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test.snap +++ b/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test.snap @@ -5,6 +5,7 @@ "0": [ [ { + "accession": "E-GEOD-61690", "dataset": "E_GEOD_61690_rnaseq", "design": "E_GEOD_61690_rnaseq.design.csv:md5,ba807caf74e0b55f5c3fe23810a89560" }, @@ -12,6 +13,7 @@ ], [ { + "accession": "E-MTAB-552", "dataset": "E_MTAB_552_rnaseq", "design": "E_MTAB_552_rnaseq.design.csv:md5,b81490696f638e90c1cf14236bb0c08c" }, @@ -19,6 +21,7 @@ ], [ { + "accession": "E-MTAB-8187", "dataset": "E_MTAB_8187_rnaseq", "design": "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf" }, @@ -38,6 +41,7 @@ "downloaded_datasets": [ [ { + "accession": "E-GEOD-61690", "dataset": "E_GEOD_61690_rnaseq", "design": "E_GEOD_61690_rnaseq.design.csv:md5,ba807caf74e0b55f5c3fe23810a89560" }, @@ -45,6 +49,7 @@ ], [ { + "accession": "E-MTAB-552", "dataset": "E_MTAB_552_rnaseq", "design": "E_MTAB_552_rnaseq.design.csv:md5,b81490696f638e90c1cf14236bb0c08c" }, @@ -52,6 +57,7 @@ ], [ { + "accession": "E-MTAB-8187", "dataset": "E_MTAB_8187_rnaseq", "design": "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf" }, @@ -64,7 +70,7 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-10-16T08:31:07.81004853" + "timestamp": "2025-11-08T16:41:54.19757792" }, "Accessions only": { "content": [ @@ -113,6 +119,7 @@ "0": [ [ { + "accession": "E-GEOD-77826", "dataset": "E_GEOD_77826_rnaseq", "design": "E_GEOD_77826_rnaseq.design.csv:md5,5aa61df754aa9c6c107b247c642d2e53" }, @@ -120,6 +127,7 @@ ], [ { + "accession": "E-MTAB-4251", "dataset": "E_MTAB_4251_rnaseq", "design": "E_MTAB_4251_rnaseq.design.csv:md5,4f3ef7b76ca6ed1ec3157295bc4a8d84" }, @@ -127,6 +135,7 @@ ], [ { + "accession": "E-MTAB-4301", "dataset": "E_MTAB_4301_rnaseq", "design": "E_MTAB_4301_rnaseq.design.csv:md5,165eeef7d612c01fd62baae1a3f296ae" }, @@ -134,6 +143,7 @@ ], [ { + "accession": "E-MTAB-5038", "dataset": "E_MTAB_5038_rnaseq", "design": "E_MTAB_5038_rnaseq.design.csv:md5,352ed3163d7deef2be35d899418d5ad4" }, @@ -141,6 +151,7 @@ ], [ { + "accession": "E-MTAB-5215", "dataset": "E_MTAB_5215_rnaseq", "design": "E_MTAB_5215_rnaseq.design.csv:md5,2741dcd5b45bacce865db632f626a273" }, @@ -148,6 +159,7 @@ ], [ { + "accession": "E-MTAB-7711", "dataset": "E_MTAB_7711_rnaseq", "design": "E_MTAB_7711_rnaseq.design.csv:md5,3e7748b54a0c25c008d9bd2ddbf1bf00" }, @@ -181,6 +193,7 @@ "downloaded_datasets": [ [ { + "accession": "E-GEOD-77826", "dataset": "E_GEOD_77826_rnaseq", "design": "E_GEOD_77826_rnaseq.design.csv:md5,5aa61df754aa9c6c107b247c642d2e53" }, @@ -188,6 +201,7 @@ ], [ { + "accession": "E-MTAB-4251", "dataset": "E_MTAB_4251_rnaseq", "design": "E_MTAB_4251_rnaseq.design.csv:md5,4f3ef7b76ca6ed1ec3157295bc4a8d84" }, @@ -195,6 +209,7 @@ ], [ { + "accession": "E-MTAB-4301", "dataset": "E_MTAB_4301_rnaseq", "design": "E_MTAB_4301_rnaseq.design.csv:md5,165eeef7d612c01fd62baae1a3f296ae" }, @@ -202,6 +217,7 @@ ], [ { + "accession": "E-MTAB-5038", "dataset": "E_MTAB_5038_rnaseq", "design": "E_MTAB_5038_rnaseq.design.csv:md5,352ed3163d7deef2be35d899418d5ad4" }, @@ -209,6 +225,7 @@ ], [ { + "accession": "E-MTAB-5215", "dataset": "E_MTAB_5215_rnaseq", "design": "E_MTAB_5215_rnaseq.design.csv:md5,2741dcd5b45bacce865db632f626a273" }, @@ -216,6 +233,7 @@ ], [ { + "accession": "E-MTAB-7711", "dataset": "E_MTAB_7711_rnaseq", "design": "E_MTAB_7711_rnaseq.design.csv:md5,3e7748b54a0c25c008d9bd2ddbf1bf00" }, @@ -228,7 +246,7 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-10-16T06:05:13.057537003" + "timestamp": "2025-11-08T16:41:16.46799497" }, "No accesssion + no keywords + multiple dataset species": { "content": [ @@ -236,6 +254,7 @@ "0": [ [ { + "accession": "E-MTAB-8187", "dataset": "E_MTAB_8187_rnaseq", "design": "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf" }, @@ -251,6 +270,7 @@ "downloaded_datasets": [ [ { + "accession": "E-MTAB-8187", "dataset": "E_MTAB_8187_rnaseq", "design": "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf" }, @@ -263,6 +283,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-10-16T08:30:50.794065861" + "timestamp": "2025-11-08T16:41:39.903564263" } } \ No newline at end of file diff --git a/tests/test_data/idmapping/base/counts.ensembl_ids.csv b/tests/test_data/idmapping/base/counts.ensembl_ids.csv index 0a9dbca4..a093ec4b 100644 --- a/tests/test_data/idmapping/base/counts.ensembl_ids.csv +++ b/tests/test_data/idmapping/base/counts.ensembl_ids.csv @@ -1,4 +1,4 @@ -ERR029909,ERR029910,ERR029911,ERR029912,ERR029913,ERR029914,ERR029915,ERR029916,ERR029917,ERR029918,ERR029920,ERR029921,ERR029922,ERR029923,ERR029924 +gend_id,ERR029909,ERR029910,ERR029911,ERR029912,ERR029913,ERR029914,ERR029915,ERR029916,ERR029917,ERR029918,ERR029920,ERR029921,ERR029922,ERR029923,ERR029924 ENSRNA049434199,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ENSRNA049434246,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ENSRNA049434252,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 diff --git a/tests/test_data/idmapping/tsv/counts.ensembl_ids.tsv b/tests/test_data/idmapping/tsv/counts.ensembl_ids.tsv new file mode 100644 index 00000000..b1e1511d --- /dev/null +++ b/tests/test_data/idmapping/tsv/counts.ensembl_ids.tsv @@ -0,0 +1,4 @@ +gene_id ERR029909 ERR029910 ERR029911 ERR029912 ERR029913 ERR029914 ERR029915 ERR029916 ERR029917 ERR029918 ERR029920 ERR029921 ERR029922 ERR029923 ERR029924 +ENSRNA049434199 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +ENSRNA049434246 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +ENSRNA049434252 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/tests/test_data/idmapping/tsv/mapping.tsv b/tests/test_data/idmapping/tsv/mapping.tsv new file mode 100644 index 00000000..fa258254 --- /dev/null +++ b/tests/test_data/idmapping/tsv/mapping.tsv @@ -0,0 +1,4 @@ +original_gene_id ensembl_gene_id +ENSRNA049434199 SNSRNA049434199 +ENSRNA049434246 SNSRNA049434246 +ENSRNA049434252 SNSRNA049434252 diff --git a/tests/test_data/idmapping/tsv/metadata.tsv b/tests/test_data/idmapping/tsv/metadata.tsv new file mode 100644 index 00000000..d2406e51 --- /dev/null +++ b/tests/test_data/idmapping/tsv/metadata.tsv @@ -0,0 +1,4 @@ +ensembl_gene_id name description +SNSRNA049434199 geneA descriptionA +SNSRNA049434246 geneB descriptionB +SNSRNA049434252 geneC descriptionC diff --git a/tests/test_data/input_datasets/input_big.yaml b/tests/test_data/input_datasets/input_big.yaml new file mode 100644 index 00000000..a8a4764b --- /dev/null +++ b/tests/test_data/input_datasets/input_big.yaml @@ -0,0 +1,4 @@ +- counts: https://raw.githubusercontent.com/nf-core/test-datasets/differentialabundance/modules_testdata/SRP254919.salmon.merged.gene_counts.top1000cov.assay.tsv + design: tests/test_data/input_datasets/rnaseq_big.design.csv + platform: rnaseq + normalised: false diff --git a/tests/test_data/input_datasets/mapping.csv b/tests/test_data/input_datasets/mapping.csv new file mode 100644 index 00000000..5eac321f --- /dev/null +++ b/tests/test_data/input_datasets/mapping.csv @@ -0,0 +1,10 @@ +original_gene_id,ensembl_gene_id +ENSRNA049453121,SNSRNA049434199 +ENSRNA049453138,SNSRNA049434246 +ENSRNA049454388,SNSRNA049434252 +ENSRNA049454416,SNSRNA049434253 +ENSRNA049454647,SNSRNA049434254 +ENSRNA049454661,SNSRNA049434255 +ENSRNA049454747,SNSRNA049434256 +ENSRNA049454887,SNSRNA049434257 +ENSRNA049454931,SNSRNA049434258 diff --git a/tests/test_data/input_datasets/metadata.csv b/tests/test_data/input_datasets/metadata.csv new file mode 100644 index 00000000..a7cd3a84 --- /dev/null +++ b/tests/test_data/input_datasets/metadata.csv @@ -0,0 +1,10 @@ +ensembl_gene_id,name,description +ENSRNA049453121,geneA,descriptionA +ENSRNA049453138,geneB,descriptionB +ENSRNA049454388,geneC,descriptionC +ENSRNA049454416,geneD,descriptionD +ENSRNA049454647,geneE,descriptionE +ENSRNA049454661,geneF,descriptionF +ENSRNA049454747,geneG,descriptionG +ENSRNA049454887,geneH,descriptionH +ENSRNA049454931,geneI,descriptionI diff --git a/tests/test_data/input_datasets/microarray.normalised.csv b/tests/test_data/input_datasets/microarray.normalised.csv index 60869917..1f93b0ca 100644 --- a/tests/test_data/input_datasets/microarray.normalised.csv +++ b/tests/test_data/input_datasets/microarray.normalised.csv @@ -1,4 +1,4 @@ -,GSM1528575,GSM1528576,GSM1528579,GSM1528583,GSM1528584,GSM1528585,GSM1528580,GSM1528586,GSM1528582,GSM1528578,GSM1528581,GSM1528577 +ensembl_gene_id,GSM1528575,GSM1528576,GSM1528579,GSM1528583,GSM1528584,GSM1528585,GSM1528580,GSM1528586,GSM1528582,GSM1528578,GSM1528581,GSM1528577 ENSRNA049453121,20925.1255070264,136184.261516502,144325.370645564,89427.0987612997,164143.182734208,34178.6378088171,28842.7323281157,76973.395782103,41906.9367255656,44756.5602263121,252562.049703724,6953.65643340122 ENSRNA049453138,196173.051628372,16607.8367703051,344972.83715281,22602.4535330758,13678.598561184,104546.421532852,15451.4637472048,71664.8857281649,160643.257448002,91459.0578537683,88396.7173963033,281623.08555275 ENSRNA049454388,91547.4240932405,11625.4857392136,84483.143792525,80582.6604222701,218857.576978944,58304.7350856292,42234.0009090266,88475.1675656357,87306.1181782617,17513.436610296,90922.3378933406,76490.2207674135 diff --git a/tests/test_data/input_datasets/rnaseq.raw.csv b/tests/test_data/input_datasets/rnaseq.raw.csv index a9a6bdb4..4d558cc2 100644 --- a/tests/test_data/input_datasets/rnaseq.raw.csv +++ b/tests/test_data/input_datasets/rnaseq.raw.csv @@ -1,4 +1,4 @@ -,ESM1528575,ESM1528576,ESM1528579,ESM1528583,ESM1528584,ESM1528585,ESM1528580,ESM1528586,ESM1528582,ESM1528578,ESM1528581,ESM1528577 +ensembl_gene_id,ESM1528575,ESM1528576,ESM1528579,ESM1528583,ESM1528584,ESM1528585,ESM1528580,ESM1528586,ESM1528582,ESM1528578,ESM1528581,ESM1528577 ENSRNA049453121,1,82,8,82,4,68,88,73,46,57,25,22 ENSRNA049453138,68,93,41,84,36,18,28,92,84,85,92,32 ENSRNA049454388,38,10,0,23,11,17,95,57,25,82,10,70 diff --git a/tests/test_data/input_datasets/rnaseq_big.design.csv b/tests/test_data/input_datasets/rnaseq_big.design.csv new file mode 100644 index 00000000..e8de12df --- /dev/null +++ b/tests/test_data/input_datasets/rnaseq_big.design.csv @@ -0,0 +1,7 @@ +sample,condition +SRX8042381,control +SRX8042382,control +SRX8042383,control +SRX8042384,treatment +SRX8042385,treatment +SRX8042386,treatment From 91b91552e4479909e7f4da9caddc3125d387848c Mon Sep 17 00:00:00 2001 From: Olivier Date: Sun, 9 Nov 2025 09:23:11 +0100 Subject: [PATCH 146/258] allow tsv files in normalisation (deseq2 and edger) --- bin/normalise_with_deseq2.R | 18 +++++++++-- bin/normalise_with_edger.R | 18 +++++++++-- .../local/normalisation/deseq2/main.nf.test | 23 ++++++++++++++ .../normalisation/deseq2/main.nf.test.snap | 30 +++++++++++++++---- .../local/normalisation/edger/main.nf.test | 21 +++++++++++++ .../normalisation/edger/main.nf.test.snap | 18 +++++++++++ tests/test_data/normalisation/base/counts.tsv | 13 ++++++++ tests/test_data/normalisation/base/design.tsv | 10 +++++++ 8 files changed, 139 insertions(+), 12 deletions(-) create mode 100644 tests/test_data/normalisation/base/counts.tsv create mode 100644 tests/test_data/normalisation/base/design.tsv diff --git a/bin/normalise_with_deseq2.R b/bin/normalise_with_deseq2.R index c1ff5721..0120bb1b 100755 --- a/bin/normalise_with_deseq2.R +++ b/bin/normalise_with_deseq2.R @@ -31,6 +31,18 @@ get_args <- function() { return(args) } +parse_dataframe <- function(file_path, ...) { + if (grepl("\\.csv$", file_path)) { + data <- read.csv(file_path, ...) + } else if (grepl("\\.tsv$", file_path)) { + data <- read.table(file_path, sep = "\t", header = TRUE, ...) + } else { + write("UNSUPPORTED FILE FORMAT", file = FAILURE_REASON_FILE) + quit(save = "no", status = 0) + } + return(data) +} + check_samples <- function(count_matrix, design_data) { # check if the column names of count_matrix match the sample names if (!all( colnames(count_matrix) == design_data$sample )) { @@ -95,7 +107,7 @@ get_cpm_counts <- function(normalised_counts, filtered_count_matrix) { get_normalised_cpm_counts <- function(count_file, design_file) { - count_data <- read.csv(count_file, row.names = 1) + count_data <- parse_dataframe(count_file, row.names = 1) # data should all be integers but sometimes they are integers converted to floats (1234 -> 1234.0) # DESeq2 does not accept that so we must convert them into integers @@ -107,7 +119,7 @@ get_normalised_cpm_counts <- function(count_file, design_file) { count_matrix <- remove_all_zero_columns(count_matrix) # getting design data - design_data <- read.csv(design_file) + design_data <- parse_dataframe(design_file) # removing extra samples in design table design_data <- design_data[design_data$sample %in% colnames(count_matrix), ] @@ -153,7 +165,7 @@ get_normalised_cpm_counts <- function(count_file, design_file) { } export_data <- function(cpm_counts, filename) { - filename <- sub("\\.csv$", ".cpm.csv", filename) + filename <- sub("\\.(csv|tsv)$", ".cpm.csv", filename) message(paste('Exporting normalised counts per million to:', filename)) write.table(cpm_counts, filename, sep = ',', row.names = TRUE, col.names = NA, quote = FALSE) } diff --git a/bin/normalise_with_edger.R b/bin/normalise_with_edger.R index b90044ca..89b4a35c 100755 --- a/bin/normalise_with_edger.R +++ b/bin/normalise_with_edger.R @@ -29,6 +29,18 @@ get_args <- function() { return(args) } +parse_dataframe <- function(file_path, ...) { + if (grepl("\\.csv$", file_path)) { + data <- read.csv(file_path, ...) + } else if (grepl("\\.tsv$", file_path)) { + data <- read.table(file_path, sep = "\t", header = TRUE, ...) + } else { + write("UNSUPPORTED FILE FORMAT", file = FAILURE_REASON_FILE) + quit(save = "no", status = 0) + } + return(data) +} + remove_all_zero_columns <- function(df) { # remove columns which contain only zeros df <- df[, colSums(df) != 0] @@ -84,7 +96,7 @@ get_normalised_cpm_counts <- function(count_file, design_file) { message(paste('Normalizing counts in:', count_file)) - count_data <- read.csv(args$count_file, row.names = 1) + count_data <- parse_dataframe(count_file, row.names = 1) count_matrix <- as.matrix(count_data) # in some rare datasets, columns can contain only zeros @@ -92,7 +104,7 @@ get_normalised_cpm_counts <- function(count_file, design_file) { count_matrix <- remove_all_zero_columns(count_matrix) # getting design data - design_data <- read.csv(design_file) + design_data <- parse_dataframe(design_file) # removing extra samples in design table design_data <- design_data[design_data$sample %in% colnames(count_matrix), ] @@ -128,7 +140,7 @@ get_normalised_cpm_counts <- function(count_file, design_file) { } export_data <- function(cpm_counts, filename) { - filename <- sub("\\.csv$", ".cpm.csv", filename) + filename <- sub("\\.(csv|tsv)$", ".cpm.csv", filename) message(paste('Exporting normalised counts per million to:', filename)) write.table(cpm_counts, filename, sep = ',', row.names = TRUE, col.names = NA, quote = FALSE) } diff --git a/tests/modules/local/normalisation/deseq2/main.nf.test b/tests/modules/local/normalisation/deseq2/main.nf.test index a95c2c68..958faf86 100644 --- a/tests/modules/local/normalisation/deseq2/main.nf.test +++ b/tests/modules/local/normalisation/deseq2/main.nf.test @@ -98,4 +98,27 @@ nextflow_process { } + test("TSV files") { + + when { + + process { + """ + input[0] = [ + [ accession: "accession", design: file('$projectDir/tests/test_data/normalisation/base/design.tsv') ], + file('$projectDir/tests/test_data/normalisation/base/counts.tsv') + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out.cpm).match() } + ) + } + + } + } diff --git a/tests/modules/local/normalisation/deseq2/main.nf.test.snap b/tests/modules/local/normalisation/deseq2/main.nf.test.snap index f6513c82..1958e538 100644 --- a/tests/modules/local/normalisation/deseq2/main.nf.test.snap +++ b/tests/modules/local/normalisation/deseq2/main.nf.test.snap @@ -13,9 +13,9 @@ ], "meta": { "nf-test": "0.9.2", - "nextflow": "25.04.4" + "nextflow": "25.04.8" }, - "timestamp": "2025-07-06T07:33:14.191843839" + "timestamp": "2025-11-09T09:18:21.978762401" }, "One group": { "content": [ @@ -31,9 +31,27 @@ ], "meta": { "nf-test": "0.9.2", - "nextflow": "24.10.3" + "nextflow": "25.04.8" }, - "timestamp": "2025-01-29T14:20:12.392982447" + "timestamp": "2025-11-09T09:18:50.127029306" + }, + "TSV files": { + "content": [ + [ + [ + { + "accession": "accession", + "design": "design.tsv:md5,7e1fd70fcb7cb6d2835748989b8c0401" + }, + "counts.cpm.csv:md5,df001c189c61c11dfab04d1bb47f7511" + ] + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-09T09:19:17.176614603" }, "Rows with many zeros": { "content": [ @@ -49,8 +67,8 @@ ], "meta": { "nf-test": "0.9.2", - "nextflow": "24.10.3" + "nextflow": "25.04.8" }, - "timestamp": "2025-01-29T14:20:00.462942337" + "timestamp": "2025-11-09T09:18:36.066964301" } } \ No newline at end of file diff --git a/tests/modules/local/normalisation/edger/main.nf.test b/tests/modules/local/normalisation/edger/main.nf.test index d6380c74..011b3183 100644 --- a/tests/modules/local/normalisation/edger/main.nf.test +++ b/tests/modules/local/normalisation/edger/main.nf.test @@ -88,4 +88,25 @@ nextflow_process { } + test("TSV files") { + + when { + + process { + """ + meta = [accession: "accession", design: file('$projectDir/tests/test_data/normalisation/base/design.tsv')] + input[0] = [meta, file('$projectDir/tests/test_data/normalisation/base/counts.tsv')] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out.cpm).match() } + ) + } + + } + } diff --git a/tests/modules/local/normalisation/edger/main.nf.test.snap b/tests/modules/local/normalisation/edger/main.nf.test.snap index e4bc5c2e..9b7542ce 100644 --- a/tests/modules/local/normalisation/edger/main.nf.test.snap +++ b/tests/modules/local/normalisation/edger/main.nf.test.snap @@ -35,6 +35,24 @@ }, "timestamp": "2025-01-29T14:20:58.681438521" }, + "TSV files": { + "content": [ + [ + [ + { + "accession": "accession", + "design": "design.tsv:md5,7e1fd70fcb7cb6d2835748989b8c0401" + }, + "counts.cpm.csv:md5,537bffb095e79d9667c955accd81e3a2" + ] + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-09T09:22:40.458649972" + }, "Rows with many zeros": { "content": [ [ diff --git a/tests/test_data/normalisation/base/counts.tsv b/tests/test_data/normalisation/base/counts.tsv new file mode 100644 index 00000000..17db2d66 --- /dev/null +++ b/tests/test_data/normalisation/base/counts.tsv @@ -0,0 +1,13 @@ + E_MTAB_5038_rnaseq_SRR1586392 E_MTAB_5038_rnaseq_SRR1586393 E_MTAB_5038_rnaseq_SRR1586394 E_MTAB_5038_rnaseq_SRR1586395 E_MTAB_5038_rnaseq_SRR1586396 E_MTAB_5038_rnaseq_SRR1586397 E_MTAB_5038_rnaseq_SRR1586400 E_MTAB_5038_rnaseq_SRR1586401 E_MTAB_5038_rnaseq_SRR1586402 +ENSRNA549434199 14 25 27 47 39 34 38 19 64 +ENSRNA549434200 91 37 78 84 6 51 18 2 57 +ENSRNA549434201 98 48 69 7 73 48 57 92 36 +ENSRNA549434202 52 15 41 19 8 100 85 83 97 +ENSRNA549434203 86 71 53 16 66 23 12 42 33 +ENSRNA549434204 62 2 25 89 74 32 45 56 26 +ENSRNA549434205 98 42 79 76 74 85 3 91 56 +ENSRNA549434206 42 49 4 88 82 34 27 83 98 +ENSRNA549434207 82 93 85 14 38 8 98 97 30 +ENSRNA549434208 72 36 4 60 25 7 14 76 47 +ENSRNA549434209 65 12 99 82 72 52 24 79 31 +ENSRNA549434210 0 0 0 0 0 0 0 0 0 diff --git a/tests/test_data/normalisation/base/design.tsv b/tests/test_data/normalisation/base/design.tsv new file mode 100644 index 00000000..fca7e731 --- /dev/null +++ b/tests/test_data/normalisation/base/design.tsv @@ -0,0 +1,10 @@ +batch condition sample +E_MTAB_5038_rnaseq g1 E_MTAB_5038_rnaseq_SRR1586392 +E_MTAB_5038_rnaseq g1 E_MTAB_5038_rnaseq_SRR1586393 +E_MTAB_5038_rnaseq g1 E_MTAB_5038_rnaseq_SRR1586394 +E_MTAB_5038_rnaseq g2 E_MTAB_5038_rnaseq_SRR1586395 +E_MTAB_5038_rnaseq g2 E_MTAB_5038_rnaseq_SRR1586396 +E_MTAB_5038_rnaseq g2 E_MTAB_5038_rnaseq_SRR1586397 +E_MTAB_5038_rnaseq g3 E_MTAB_5038_rnaseq_SRR1586400 +E_MTAB_5038_rnaseq g3 E_MTAB_5038_rnaseq_SRR1586401 +E_MTAB_5038_rnaseq g3 E_MTAB_5038_rnaseq_SRR1586402 From 64bcdf8238489632dfcccc36428f3a6c05a227af Mon Sep 17 00:00:00 2001 From: Olivier Date: Sun, 9 Nov 2025 13:22:04 +0100 Subject: [PATCH 147/258] update documentation --- README.md | 68 +---- bin/gprofiler_utils.py | 2 +- conf/modules.config | 2 - conf/modules/id_mapping.config | 2 +- conf/modules/merging.config | 45 --- conf/modules/scoring.config | 17 -- docs/output.md | 154 +++++++--- docs/usage.md | 268 +++++++++++++----- .../tool/nf_core_stableexpression.xml | 6 +- nextflow.config | 2 +- nextflow_schema.json | 8 +- tests/default.nf.test | 2 +- workflows/stableexpression.nf | 2 +- 13 files changed, 341 insertions(+), 237 deletions(-) delete mode 100644 conf/modules/merging.config delete mode 100644 conf/modules/scoring.config diff --git a/README.md b/README.md index 45ba48e3..4624c389 100644 --- a/README.md +++ b/README.md @@ -21,73 +21,31 @@ ## Introduction -**nf-core/stableexpression** is a bioinformatics pipeline that aims at finding the most stable genes among a single or multiple public / local count datasets. It takes as input a species name (mandatory), keywords for expression atlas search (optional) and / or a CSV input file listing local raw / normalised count datasets (optional). **A typical usage is to find the most suitable qPCR housekeeping genes for a specific species (and optionally specific conditions)**. +**nf-core/stableexpression** is a bioinformatics pipeline that aims at finding the most stable genes among a single or multiple public / local count datasets. It takes as main inputs a species name (mandatory), keywords for expression atlas search (optional) and / or a CSV input file listing local raw / normalised count datasets (optional). **A typical usage is to find the most suitable qPCR housekeeping genes for a specific species (and optionally specific conditions)**.

    -## Pipeline summary - -1. Get Expression Atlas accessions corresponding to the provided species (and optionally keywords) ([Expression Atlas](https://www.ebi.ac.uk/gxa/home); optional) -2. Download Expression Atlas data ([Expression Atlas](https://www.ebi.ac.uk/gxa/home); optional) -3. Normalize raw data (using [DESeq2](https://bioconductor.org/packages/release/bioc/html/DESeq2.html) or [EdgeR](https://bioconductor.org/packages/release/bioc/html/edgeR.html)) -4. Map gene IDS to Ensembl IDS for standardisation among datasets ([g:Profiler](https://biit.cs.ut.ee/gprofiler/gost)) -5. Compute pairwise gene variation -6. Compute gene variation statistics and get the most stable genes -7. Present QC for raw reads ([`MultiQC`](http://multiqc.info/)) - -## Usage +## Basic usage > [!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. -First, prepare a samplesheet listing the different count datasets: - -`datasets.csv`: - -```csv -counts,design,normalised -path/to/normalised.counts.csv,path/to/normalised.design.csv,true -path/to/raw.counts.csv,path/to/raw.design.csv,false -``` - -Make sure to format your datasets properly: - -`counts.csv`: +To search the most stable genes in a species considering all public datasets, simply run: -```csv -,sample_A,sample_B,sample_C -gene_1,1,2,3 -gene_2,1,2,3 -... -``` +```bash +nextflow run nf-core/stableexpression \ + -r dev \ + -profile \ + --species \ + --outdir + ``` -`design.csv`: + For more specific scenarios, __like fetching only specific conditions or using your own expression datasets__, please refer to the [usage documentation](https://nf-co.re/stableexpression/usage). -```csv -sample,condition -sample_A,condition_1 -sample_B,condition_2 -... -``` - -Now you can run the pipeline as follows: - -> ```bash -> nextflow run nf-core/stableexpression \ -> -profile docker \ -> --species \ -> --eatlas_accessions \ -> --keywords \ -> --datasets ./datasets.csv \ -> --outdir ./results -> ``` - -> [!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/stableexpression/usage) and the [parameter documentation](https://nf-co.re/stableexpression/parameters). +> [!NOTE] +> See [here](https://nf-co.re/stableexpression/usage#profiles) for more information about profiles. ## Pipeline output diff --git a/bin/gprofiler_utils.py b/bin/gprofiler_utils.py index 7d72340a..769abbf1 100755 --- a/bin/gprofiler_utils.py +++ b/bin/gprofiler_utils.py @@ -39,7 +39,7 @@ "g:Profiler servers (main and beta) seem to be down... Please retry later... " "If you have gene ID mappings and / or gene metadata for these datasets, you can provide them " "directly using the `--gene_id_mapping` and `--gene_metadata` parameters respectively, " - "and by skipping the g:Profiler ID mapping step with `--skip_gprofiler`." + "and by skipping the g:Profiler ID mapping step with `--skip_id_mapping`." ) diff --git a/conf/modules.config b/conf/modules.config index 3626e0af..6248bbb3 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -23,6 +23,4 @@ includeConfig 'modules/expression_atlas.config' includeConfig 'modules/geo.config' includeConfig 'modules/id_mapping.config' includeConfig 'modules/normalisation.config' -includeConfig 'modules/merging.config' -includeConfig 'modules/scoring.config' includeConfig 'modules/qc.config' diff --git a/conf/modules/id_mapping.config b/conf/modules/id_mapping.config index f06c486b..4317fb79 100644 --- a/conf/modules/id_mapping.config +++ b/conf/modules/id_mapping.config @@ -2,7 +2,7 @@ process { withName: GPROFILER_IDMAPPING { publishDir = [ - path: { "${params.outdir}/idmapping/datasets/" }, + path: { "${params.outdir}/idmapping/" }, mode: params.publish_dir_mode ] } diff --git a/conf/modules/merging.config b/conf/modules/merging.config deleted file mode 100644 index 831c633a..00000000 --- a/conf/modules/merging.config +++ /dev/null @@ -1,45 +0,0 @@ -process { - - withName: MERGE_RNASEQ_COUNTS { - publishDir = [ - path: { "${params.outdir}/merged_datasets/rnaseq/" }, - mode: params.publish_dir_mode - ] - } - - withName: MERGE_MICROARRAY_COUNTS { - publishDir = [ - path: { "${params.outdir}/merged_datasets/microarray/" }, - mode: params.publish_dir_mode - ] - } - - withName: MERGE_ALL_COUNTS { - publishDir = [ - path: { "${params.outdir}/merged_datasets/all/" }, - mode: params.publish_dir_mode - ] - } - - withName: COMPUTE_BASE_STATISTICS_FOR_RNASEQ { - publishDir = [ - path: { "${params.outdir}/base_statistics/rnaseq/" }, - mode: params.publish_dir_mode - ] - } - - withName: COMPUTE_BASE_STATISTICS_FOR_MICROARRAY { - publishDir = [ - path: { "${params.outdir}/base_statistics/microarray/" }, - mode: params.publish_dir_mode - ] - } - - withName: COMPUTE_BASE_STATISTICS { - publishDir = [ - path: { "${params.outdir}/base_statistics/all/" }, - mode: params.publish_dir_mode - ] - } - -} diff --git a/conf/modules/scoring.config b/conf/modules/scoring.config deleted file mode 100644 index c5bfe09e..00000000 --- a/conf/modules/scoring.config +++ /dev/null @@ -1,17 +0,0 @@ -process { - - withName: NORMFINDER { - publishDir = [ - path: { "${params.outdir}/stability_scoring/normfinder/" }, - mode: params.publish_dir_mode - ] - } - - withName: COMPUTE_M_MEASURE { - publishDir = [ - path: { "${params.outdir}/stability_scoring/genorm/" }, - mode: params.publish_dir_mode - ] - } - -} diff --git a/docs/output.md b/docs/output.md index d96305b5..82b0f57a 100644 --- a/docs/output.md +++ b/docs/output.md @@ -1,42 +1,52 @@ # nf-core/stableexpression: Output +## Pipeline reports (TLDR) + +The main output of the pipeline is the MultiQC report, which summarises results at the end of the pipeline. This report is located at `/multiqc/multiqc_report.html` and can be opened in your favorite browser. + +For advanced users who seek to explore more deeply the distributions of normalised counts gene per gene or sample per sample, a Dash Plotly app is readily prepared at the end of each pipeline run. See [here](#dash-plotly-app) for explanation on how to run the app. + ## 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. +This document describes the output produced by 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. - - ## Pipeline overview The pipeline is built using [Nextflow](https://www.nextflow.io/) and processes data using the following steps: -- [FastQC](#fastqc) - Raw read QC -- [MultiQC](#multiqc) - Aggregate report describing results and QC from the whole pipeline -- [Pipeline information](#pipeline-information) - Report metrics generated during the workflow execution -- [Expression Atlas](#expression-atlas): get Expression Atlas accessions and download data -- [Normalisation](#normalisation): normalise raw data (with DESeq2 or EdgeR) -- [gProfiler](#gprofiler-idmapping): map gene IDS to Ensembl IDS -- [Gene Statistics](#gene-statistics): merge all counts, compute gene variation statistics and get the most stable genes +1. Get accessions + - Get [Expression Atlas](https://www.ebi.ac.uk/gxa/home) dataset accessions corresponding to the provided species (and optionally keywords) (run by default; optional) + - Get NBCI [GEO](https://www.ncbi.nlm.nih.gov/gds) __microarray__ dataset accessions corresponding to the provided species (and optionally keywords) (run by default; optional) +2. Download data + - Download [Expression Atlas](https://www.ebi.ac.uk/gxa/home) data (run by default; optional) + - Download NBCI [GEO](https://www.ncbi.nlm.nih.gov/gds) data (run by default; optional) +3. ID Mapping + - Map gene IDS to Ensembl IDS for standardisation among datasets using [g:Profiler](https://biit.cs.ut.ee/gprofiler/gost) (run by default; optional) +4. Data normalisation + - Normalize RNAseq raw data using [DESeq2](https://bioconductor.org/packages/release/bioc/html/DESeq2.html) or [EdgeR](https://bioconductor.org/packages/release/bioc/html/edgeR.html) + - Perform quantile normalisation on each dataset separately using [scikit-learn](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.quantile_transform.html) +5. Data cleaning + - Get statistics for each sample in each dataset + - Remove samples that diverge too much from the expected normalised profile +6. Merge all data +7. Compute base statistics for each gene, platform-wide and for each platform (RNAseq and microarray) +8. Compute stability scoring + - Get list of candidate genes based on base statistics + - Run optimised, scalable version of [Normfinder](https://www.moma.dk/software/normfinder) + - Run optimised, scalable version of [Genorm](https://genomebiology.biomedcentral.com/articles/10.1186/gb-2002-3-7-research0034) (NOT run by default; optional) + - Compute stability scores for each candidate gene +9. Aggregate results +10. Prepare [Dash Plotly](https://dash.plotly.com/) app for further investigation of gene / sample counts +11. Make [`MultiQC`](http://multiqc.info/) report ## Output files -### FastQC - -
    -Output files - -- `fastqc/` - - `*_fastqc.html`: FastQC report containing quality metrics. - - `*_fastqc.zip`: Zip archive containing the FastQC report, tab-delimited data file and plot images. - -
    - -[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 +This report is located at `multiqc/multiqc_report.html` and can be opened in a browser. +
    Output files @@ -51,26 +61,57 @@ MultiQC](http://multiqc.info) is a visualization tool that generates a single HT 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 . -### Gene Variation +### Dash Plotly app + +`dash_app/`: folder containing the Dash Plotly app + +To launch the app, you must first create and activate the appropriate conda environment: + +```bash +conda env create -n nf-core-stableexpression-dash -f /dash_app/spec-file.txt +conda activate nf-core-stableexpression-dash +``` + +then: +``` +cd dash_app +python app.py +``` +and open your browser at `http://localhost:8080` + +> [!NOTE] +> The app will try to use the port `8080` by default. If it is already in use, it will try `8081`, `8082` and so on. Check the logs to see which port it is using. + + +### Expression Atlas
    Output files -- `gene_variation/` - - A list of the most stable genes in `stats_most_stable_genes.csv`. - - Descriptive statistics for all genes in `stats_all_genes.csv` - - All normalised counts (for each gene and each sample) in `count_summary.csv`. +- `expression_atlas/accessions/`: accessions found when querying Expression Atlas +- `expression_atlas/datasets/`: count datasets (normalized: `*.normalised.csv` / raw: `*.raw.csv`) and experimental designs (`*.design.csv`) downloaded from Expression Atlas.
    -### Expression Atlas +### GEO
    Output files -- `expressionatlas/` - - List of accessions found when querying Expression Atlas: `accessions.txt`. - - List of count datasets (normalized: `*.normalised.csv` / raw: `*.raw.csv`) and experimental designs (`*.design.csv`) downloaded from Expression Atlas. +- `geo/accessions/`: accessions found when querying GEO +- `geo/datasets/`: count datasets (normalized: `*.normalised.csv` / raw: `*.raw.csv`) and experimental designs (`*.design.csv`) downloaded from GEO. + +
    + +### IDMapping (g:Profiler) + +
    +Output files + +- `idmapping/` + - Count datasets whose gene IDs have been mapped to Ensembl IDs: `*.renamed.csv`. + - Table associating original gene IDs and Ensembl IDs: `*.mapping.csv`. + - Ensembl gene metadata (name and description): `*.metadata.csv`.
    @@ -79,25 +120,58 @@ Results generated by MultiQC collate pipeline QC from supported tools e.g. FastQ
    Output files -List of newly normalised datasets in `normalisation/` +- `normalised/`: Newly normalised datasets + - `normalised/deseq2/` for DESeq2 + - `normalised/edger/` for EdgeR +- `quantile_normalised` : Quantile normalised datasets + + +### Gene base statistics + +
    +Output files -- `normalisation/deseq2/` for DESeq2 -- `normalisation/edger/` for EdgeR +- `merged_datasets/`: Merged count datasets (sample-wide) + - `merged_datasets/all/` : all datasets together + - `merged_datasets/rnaseq/` : only RNA-seq datasets + - `merged_datasets/microarray/` : only microarray datasets
    -### GProfiler IDMapping +### Merged counts + +The file containing all normalised counts is bundled as a Parquet file with the Dash Plotly app.
    Output files -- `idmapping/` - - Count datasets whose gene IDs have been mapped to Ensembl IDs: `*_renamed.csv`. - - Table associating original gene IDs and Ensembl IDs: `*_mapping.csv`. - - Ensembl gene metadata (name and description): `*_metadata.csv`. +- `dash_app/data/all_counts.parquet`: Merged count datasets (sample-wide)
    +### Summary of gene statistics and scores + +The gene stat summary is also bundled with the Dash Plotly app. + +
    +Output files + +- `dash_app/data/all_genes_summary.csv`: file containing all gene statistics, scores and ranked by stability score + +
    + +### Overall experimental design + +
    +Output files + +- `dash_app/data/whole_design.csv`: file containing all experimental design information + +
    + + + + ### Pipeline information
    diff --git a/docs/usage.md b/docs/usage.md index 4d64ff52..c14fa1ce 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -2,128 +2,238 @@ ## :warning: Please read this documentation on the nf-core website: [https://nf-co.re/stableexpression/usage](https://nf-co.re/stableexpression/usage) +> [!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). + > _Documentation of pipeline parameters is generated automatically from the pipeline schema and can no longer be found in markdown files._ -## Introduction -You can run this pipeline in multiple ways. +## 1. Basic run + +This pipeline fetches Expression Atlas and GEO accessions for the provided species and downloads the corresponding data. + +```bash +nextflow run nf-core/stableexpression \ + -r dev \ + -profile \ + --species \ + --outdir +``` + +> [!NOTE] +> See [here](#profiles) for more information about profiles. -1. Expression Atlas **automatic mode**: without keywords +## 2. Specific public datasets -This pipeline fetches Expression Atlas accessions for the provided species and downloads the corresponding data. +You can provide keywords to restrict downloaded datasets to specific conditions. ```bash nextflow run nf-core/stableexpression \ - -profile \ - --species \ - --fetch_eatlas_accessions \ + -r dev \ + -profile \ + --species \ + --keywords --outdir ``` -1. Expression Atlas **automatic mode**: with keywords +> [!NOTE] +> - Multiple keywords must be separated by commas. +> - Note that the keywords are additive: you will get datasets that fit with __either of the keywords__. +> - A dataset will be downloaded if a keyword is found in its summary or in the same of a sample. -The pipeline fetches Expression Atlas accessions for the provided species / keywords and downloads the corresponding data. You do not need to specify the `--fetch_eatlas_accessions` parameter when you specify keywords. + +## 3. Provide your own accessions + +You may already have an idea of specific Expression Atlas / GEO accessions you want to use in the analysis. +In this case, you can provide them directly to the pipeline. ```bash nextflow run nf-core/stableexpression \ - -profile \ - --species \ - --keywords + -r dev \ + -profile \ + --species \ + --skip_fetch_eatlas_accessions \ + --skip_fetch_geo_accessions \ + [--eatlas_accessions ] \ + [--eatlas_accessions_file ] \ + [--geo_accessions ] \ + [--geo_accessions_file ] \ --outdir ``` -3. Expression Atlas **manual mode** +> [!WARNING] +> If you want to download only the datasets corresponding to the accessions supplied, ou must set the `--skip_fetch_eatlas_accessions` and `--skip_fetch_geo_accessions`. + +> [!NOTE] +> If you provide accessions through `--eatlas_accessions_file` or `--geo_accessions_file`, there must be one accession per line. The extension of the file does not matter. -The pipeline downloads the count datasets and experimental designs for the provided accessions. +In case you do not know which accessions you want but you would like to control precisely which datasets are included in you analysis, you may run first: ```bash nextflow run nf-core/stableexpression \ - -profile \ - --species \ - --eatlas_accessions \ + -r dev \ + -profile \ + --species \ + --accessions_only \ --outdir ``` -4. Using local count datasets +Fetched accessions with their respective metadata will be available in `/expression_atlas/accessions/` and `/geo/accessions/` + +## 4. Use your own expression datasets + +You can of course provide your own counts datasets / experimental designs. + +> [!NOTE] +> - To ensure all RNAseq datasets are processed the same way, it is better to provide them raw. +> - In case you want to provide normalise counts, please provide CPMs (counts per million) in order to stay aligned with the way raw datasets are processed in the pipeline. -Conversely, you can provide your own counts datasets / experiment designs. +> [!WARNING] +> Microarray data must be already normalised. To be compliant with Expression Atlas, you should use the `RMA` methods. First, prepare a samplesheet listing the different count datasets you want to use. Each row represents a specific dataset and must contain: -- counts: the path to the count dataset (a CSV file) -- design: the path to the experimental design associated to this dataset (a CSV file) -- normalised: a boolean (true / false) representing whether the counts are already normalised or not +| Column | Description | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `counts` | Path to the count dataset (a CSV / TSV file) | +| `design` | Path to the experimental design associated to this dataset (a CSV / TSV file) | +| `platform` | Platform used to generate the counts (`rnaseq` or `microarray`) +| `normalised` | Boolean (`true` / `false`) representing whether the counts are already normalised or not. It should look as follows: -`datasets.csv`: - -```csv +```csv title=datasets.csv counts,design,platform,normalised path/to/normalised.counts.csv,path/to/normalised.design.csv,rnaseq,true -path/to/raw.counts.csv,path/to/raw.design.csv,microarray,false -... +path/to/raw.counts.csv,path/to/raw.design.csv,rnaseq,false +path/to/microarray.counts.csv,path/to/microarray.design.csv,microarray,true ``` -(the `platform` field can be either `rnaseq` or `microarray`). +It can also be a YAML file: + +```yaml title=datasets.yaml +- counts: path/to/normalised.counts.csv + design: path/to/normalised.design.csv + platform: rnaseq + normalised: true +- counts: path/to/raw.counts.csv + design: path/to/raw.design.csv + platform: rnaseq + normalised: false +- counts: path/to/microarray.counts.csv + design: path/to/microarray.design.csv + platform: microarray + normalised: true +``` -While the counts and design CSV files should have the following structure: -`counts.csv`: +The counts should have the following structure: -```csv -,sample_A,sample_B,sample_C +```csv title=counts.csv +gene_id,sample_A,sample_B,sample_C gene_1,1,2,3 gene_2,1,2,3 -... ``` -> [!NOTE] -> -> - To ensure all RNAseq datasets are processed the same way, it is better to provide them raw. -> In case you want to provide normalise counts, please provide CPMs (counts per million) in order to stay aligned with the way raw datasets are processed in the pipeline. -> - Microarray data must be already normalised. To be compliant with Expression Atlas, you can use the `RMA` or `LOESS` methods. - -> [!WARNING] -> Remember to write a comma before the first sample name. This serves to indicate that the actual first column (gene IDs) is the index -`design.csv`: +While the design should look like: -```csv +```csv title=design.csv sample,condition sample_A,condition_1 sample_B,condition_2 -... -... +sample_C,condition_1 ``` + +> [!WARNING] +> - In the count file, the first header column (corresponding to gene IDs) should not be empty. However, its name can be anything. +> - The count file should not have any column other than the first one (gene IDs) and the sample columns. Extra columns will be ignored. + +> [!TIP] +> Both counts and design files can also be supplied as TSV files. + + Now run the pipeline with: ```bash nextflow run nf-core/stableexpression \ - -profile \ - --species \ - --datasets \ + -r dev \ + -profile \ + --species \ + --datasets \ + --skip_fetch_eatlas_accessions \ + --skip_fetch_geo_accessions \ --outdir ``` -## Running the pipeline +> [!TIP] +> The `--skip_fetch_eatlas_accessions` and `--skip_fetch_geo_accessions` parameters are supplied here to show how to analyse __only your own dataset__. You may remove these parameters if you want to mix you dataset(s) with public ones. -You can run the pipeline using a mix of the different pathways. +> [!IMPORTANT] +> By default, the pipeline tries to map gene IDs to Ensembl gene IDs. __All genes that cannot be mapped are discarded from the analysis__. This ensures that all genes are named the same between datasets and allows comparing multiple datasets with each other. If you are confident that your genes have the same name between your different datasets or if you think that your gene IDs won't be mapped properly, you can disable this mapping by adding the `--skip_id_mapping` parameter. In such case, it is recommended to supply your own gene id mapping file and gene metadata file with the `--gene_id_mapping` and `--gene_metadata` parameters. See [next section](#5-custom-gene-id-mapping-and-metadata) for further details. -Example usage: +> [!TIP] +> You can check if your gene IDs can be mapped using the [g:Profiler server](https://biit.cs.ut.ee/gprofiler/convert). -> ```bash -> nextflow run nf-core/stableexpression \ -> -profile docker \ -> --species "Arabidopsis thaliana" \ -> --eatlas_accessions "E-MTAB-552,E-GEOD-61690" \ -> --keywords "stress,flowering" \ -> --datasets ./datasets.csv \ -> --outdir ./results -> ``` +### 5. Custom gene ID mapping and metadata -This will launch the pipeline with the `docker` configuration profile. See below for more information about profiles. +You can supply your own gene id mapping file and optionally gene metadata with: + +```bash +nextflow run nf-core/stableexpression \ + -r dev \ + -profile \ + --species \ + --datasets \ + --gene_id_mapping \ + --gene_metadata \ + --skip_fetch_eatlas_accessions \ + --skip_fetch_geo_accessions \ + --outdir +``` + +Structure of the gene id mapping file: + +| Column | Description | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `original_gene_id` | Gene ID used in the provided count dataset(s) | +| `ensembl_gene_id` | Mapped gene ID | + +It should look as follows: + +```csv title=gene_id_mapping.csv +original_gene_id,ensembl_gene_id +gene_A,ENSG1234567890 +geneB,OTHERmappedgeneID +``` + +> [!NOTE] +> The gene IDs in the `ensembl_gene_id` column do not have to be real Ensembl gene IDs. + + +Structure of the gene metadata file: + +| Column | Description | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ensembl_gene_id` | Mapped gene ID | +| `name` | Gene common name | +| `description` | Gene description | + +It should look as follows: + +```csv title=gene_metadata.csv +ensembl_gene_id,name,description +ENSG1234567890,Gene A,Description of gene A +OTHERmappedgeneID,My OTHER Gene,Another description +``` + +### 6. More advanced scenarios + +For advanced scenarios and if you want the entire list of avalable parameters, you can see the [parameter documentation](https://nf-co.re/stableexpression/parameters). + + +## Pipeline output Note that the pipeline will create the following files in your working directory: @@ -134,6 +244,10 @@ work # Directory containing the nextflow working files # Other nextflow hidden files, eg. history of pipeline runs and old logs. ``` +For a detailed description of the output files, please consult the [nf-core stableexpression output directory structure](https://nf-co.re/stableexpression/output). + +## Parameters + If you wish to repeatedly use the same parameters for multiple runs, rather than specifying each flag in the command, you can specify these in a params file. Pipeline settings can be provided in a `yaml` or `json` file via `-params-file `. @@ -144,7 +258,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/stableexpression -profile docker -params-file params.yaml +nextflow run -r dev nf-core/stableexpression -profile docker -params-file params.yaml ``` with: @@ -158,6 +272,7 @@ outdir: './results/' You can also generate such `YAML`/`JSON` files via [nf-core/launch](https://nf-co.re/launch). + ### 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: @@ -184,14 +299,33 @@ To further assist in reproducibility, you can use share and reuse [parameter fil > [!NOTE] > These options are part of Nextflow and use a _single_ hyphen (pipeline parameters use a double-hyphen) -### `-profile` +### [`-profile`](#profiles) Use this parameter to choose a configuration profile. Profiles can give configuration presets for different compute environments. Several generic profiles are bundled with the pipeline which instruct the pipeline to use software packaged using different methods (Docker, Singularity, Podman, Shifter, Charliecloud, Apptainer, Conda) - see below. > [!IMPORTANT] -> We highly recommend the use of Docker or Singularity containers for full pipeline reproducibility, however when this is not possible, Conda is also supported. +> We highly recommend the use of Apptainer (Singularity) or Docker containers for full pipeline reproducibility, however when this is not possible, Conda is also supported. + +> [!TIP] + +> When running the pipeline of multi-user server or on a cluster, the best practice is to use Apptainer (formerly Singularity). You can install Apptainer by following these [instructions](https://apptainer.org/docs/admin/main/installation.html#). +> In case you encounter the following error when running Apptainer: +> ``` +> ERROR : Could not write info to setgroups: Permission denied +> ERROR : Error while waiting event for user namespace mappings: no event received +> ``` +> you may need to install the `apptainer-suid` package instead of `apptainer`: +> ``` +> # Debian / Ubuntu +> sudo apt install apptainer-suid +> # RHEL / CentOS +> sudo yum install apptainer-suid +> # Fedora +> sudo dnf install apptainer-suid +>``` + The pipeline also dynamically loads configurations from [https://github.com/nf-core/configs](https://github.com/nf-core/configs) when it runs, making multiple config profiles for various institutional clusters available at run time. For more information and to check if your system is supported, please see the [nf-core/configs documentation](https://github.com/nf-core/configs#documentation). @@ -203,6 +337,8 @@ If `-profile` is not specified, the pipeline will run locally and expect all sof - `test` - A profile with a complete configuration for automated testing - Includes links to test data so needs no other parameters +- `apptainer` + - A generic configuration profile to be used with [Apptainer](https://apptainer.org/) - `docker` - A generic configuration profile to be used with [Docker](https://docker.com/) - `singularity` @@ -213,12 +349,12 @@ If `-profile` is not specified, the pipeline will run locally and expect all sof - A generic configuration profile to be used with [Shifter](https://nersc.gitlab.io/development/shifter/how-to-use/) - `charliecloud` - A generic configuration profile to be used with [Charliecloud](https://charliecloud.io/) -- `apptainer` - - A generic configuration profile to be used with [Apptainer](https://apptainer.org/) - `wave` - A generic configuration profile to enable [Wave](https://seqera.io/wave/) containers. Use together with one of the above (requires Nextflow ` 24.03.0-edge` or later). - `conda` - A generic configuration profile to be used with [Conda](https://conda.io/docs/). Please only use Conda as a last resort i.e. when it's not possible to run the pipeline with Docker, Singularity, Podman, Shifter, Charliecloud, or Apptainer. +- `micromamba` + - A faster, more lightweight alternative to Conda. As for Conda, use Micromamba as a last resort. ### `-resume` diff --git a/galaxy/tool_shed/tool/nf_core_stableexpression.xml b/galaxy/tool_shed/tool/nf_core_stableexpression.xml index 04f8276a..bd502632 100644 --- a/galaxy/tool_shed/tool/nf_core_stableexpression.xml +++ b/galaxy/tool_shed/tool/nf_core_stableexpression.xml @@ -96,8 +96,8 @@ VERSION="1.0dev"; echo "$VERSION" #if $geo_dataset_options.exclude_geo_accessions_file --exclude_geo_accessions_file "$geo_dataset_options.exclude_geo_accessions_file" #end if - #if $idmapping_options.skip_gprofiler - --skip_gprofiler $idmapping_options.skip_gprofiler + #if $idmapping_options.skip_id_mapping + --skip_id_mapping $idmapping_options.skip_id_mapping #end if #if $idmapping_options.gene_id_mapping_file --gene_id_mapping_file "$idmapping_options.gene_id_mapping_file" @@ -179,7 +179,7 @@ VERSION="1.0dev"; echo "$VERSION"
    - +
    diff --git a/nextflow.config b/nextflow.config index 494a057d..eb0a1ff2 100644 --- a/nextflow.config +++ b/nextflow.config @@ -38,7 +38,7 @@ params { // ID mapping gene_metadata = null gene_id_mapping_file = null - skip_gprofiler = false + skip_id_mapping = false // statistics normalisation_method = 'deseq2' diff --git a/nextflow_schema.json b/nextflow_schema.json index 9ca94074..d9be418b 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -170,11 +170,11 @@ "fa_icon": "fas fa-map", "description": "Options for mapping gene IDs.", "properties": { - "skip_gprofiler": { + "skip_id_mapping": { "type": "boolean", "description": "Skip g:Profiler ID mapping step", "fa_icon": "fas fa-ban", - "help": "If you don't want to map gene IDs with g:Profiler, you can skip this step by providing `--skip_gprofiler`. It can be in particular useful if the g:Profiler is down and if you already have a custom mapping file." + "help": "If you don't want to map gene IDs with g:Profiler, you can skip this step by providing `--skip_id_mapping`. It can be in particular useful if the g:Profiler is down and if you already have a custom mapping file." }, "gene_id_mapping_file": { "type": "string", @@ -182,7 +182,7 @@ "exists": true, "schema": "assets/schema_gene_id_mapping.json", "mimetype": "text/csv", - "pattern": "^\\S+\\.(csv|dat)$", + "pattern": "^\\S+\\.(csv|tsv|dat)$", "description": "Custom gene id mapping file", "help_text": "Path to comma-separated file containing custom gene id mappings. Each row represents a mapping from the original gene ID in your count datasets to the ensembl ID in g:Profiler. The mapping file should be a comma-separated file with 2 columns (original_gene_id and ensembl_gene_id) and a header row.", "fa_icon": "fas fa-file" @@ -193,7 +193,7 @@ "exists": true, "schema": "assets/schema_gene_metadata.json", "mimetype": "text/csv", - "pattern": "^\\S+\\.(csv|dat)$", + "pattern": "^\\S+\\.(csv|tsv|dat)$", "description": "Custom gene metadata file", "help_text": "Path to comma-separated file containing custom gene metadata information. Each row represents a gene and links its ensembl gene ID to its name and description. The metadata file should be a comma-separated file with 3 columns (ensembl_gene_id, name and description) and a header row.", "fa_icon": "fas fa-file" diff --git a/tests/default.nf.test b/tests/default.nf.test index 6c410219..8429ade2 100644 --- a/tests/default.nf.test +++ b/tests/default.nf.test @@ -227,7 +227,7 @@ nextflow_pipeline { params { species = 'solanum tuberosum' datasets = "${projectDir}/tests/test_data/input_datasets/input.csv" - skip_gprofiler = true + skip_id_mapping = true gene_id_mapping = "${projectDir}/tests/test_data/input_datasets/mapping.csv" gene_metadata = "${projectDir}/tests/test_data/input_datasets/metadata.csv" outdir = "$outputDir" diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 48d123f0..5aae4437 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -75,7 +75,7 @@ workflow STABLEEXPRESSION { ch_gene_id_mapping = params.gene_id_mapping_file ? Channel.fromPath( params.gene_id_mapping_file, checkIfExists: true ) : Channel.value( [] ) ch_gene_metadata = params.gene_metadata ? Channel.fromPath( params.gene_metadata, checkIfExists: true ) : Channel.value( [] ) - if ( !params.skip_gprofiler ) { + if ( !params.skip_id_mapping ) { // tries to map gene IDs to Ensembl IDs whenever possible ID_MAPPING( From 433dfeac1dc66175303117b8de79972230691eaa Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 10 Nov 2025 07:49:32 +0100 Subject: [PATCH 148/258] change species parameter to allow more complex species names; add species tag in get accessions modules; display a warn message when no dataset is found --- modules/local/expressionatlas/getaccessions/main.nf | 2 ++ modules/local/geo/getaccessions/main.nf | 2 ++ nextflow_schema.json | 4 ++-- workflows/stableexpression.nf | 5 ++++- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/modules/local/expressionatlas/getaccessions/main.nf b/modules/local/expressionatlas/getaccessions/main.nf index ee553a8a..804c44c9 100644 --- a/modules/local/expressionatlas/getaccessions/main.nf +++ b/modules/local/expressionatlas/getaccessions/main.nf @@ -2,6 +2,8 @@ process EXPRESSIONATLAS_GETACCESSIONS { label 'process_medium' + tag "${species}" + conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/f2/f2219a174683388670dc0817da45717014aca444323027480f84aaaf12bfb460/data': diff --git a/modules/local/geo/getaccessions/main.nf b/modules/local/geo/getaccessions/main.nf index 50e9e33c..040c6f2b 100644 --- a/modules/local/geo/getaccessions/main.nf +++ b/modules/local/geo/getaccessions/main.nf @@ -2,6 +2,8 @@ process GEO_GETACCESSIONS { label 'process_high_cpus' + tag "${species}" + conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/ca/caae35ec5dc72367102a616a47b6f1a7b3de9ff272422f2c08895b8bb5f0566c/data': diff --git a/nextflow_schema.json b/nextflow_schema.json index d9be418b..2005be49 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -16,8 +16,8 @@ "type": "string", "description": "Scientifc species name (genus and species)", "fa_icon": "fas fa-hippo", - "pattern": "^([a-zA-Z]+)[_ ]([a-zA-Z]+)$", - "help_text": "Genus and species may be separated by ` ` or `_`. Example: `--species 'Arabidopsis thaliana'` or `--species 'homo_sapiens'`. Character case is not important." + "pattern": "^([a-zA-Z]+)[_ ]([a-zA-Z]+)[_ a-zA-Z]*$", + "help_text": "At least genus and species name should be supplied. Words should be separated by ` ` or `_`. Note that character case is ignored. Examples: `--species 'Arabidopsis thaliana'`, `--species 'homo_sapiens' or `--species MARMOTA_MARMOTA_MARMOTA`." }, "outdir": { "type": "string", diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 5aae4437..57bb96ee 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -41,7 +41,7 @@ workflow STABLEEXPRESSION { ch_all_genes_statistics = Channel.empty() ch_top_stable_genes_transposed_counts = Channel.empty() - def species = params.species.split(' ').join('_') + def species = params.species.split(' ').join('_').toLowerCase() // ----------------------------------------------------------------- // FETCH AND DOWNLOAD EXPRESSION ATLAS DATASETS IF NEEDED @@ -66,6 +66,9 @@ workflow STABLEEXPRESSION { ch_counts = storeDatasetSize( ch_counts, "nb_genes", "nb_samples" ) + // display a warning if no datasets are found + ch_counts.count().map { n -> if( n == 0 ) { log.warn "No datasets found" } } + if ( !params.accessions_only && !params.download_only ) { // ----------------------------------------------------------------- From c9b9dd46c3b14676fff0b05792174a6f023b3611 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 10 Nov 2025 07:57:07 +0100 Subject: [PATCH 149/258] update README.md --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4624c389..5790973f 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,10 @@ [![Nextflow](https://img.shields.io/badge/version-%E2%89%A525.04.0-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-3.4.1-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/3.4.1) -[![run with conda](http://img.shields.io/badge/run%20with-conda-3EB049?labelColor=000000&logo=anaconda)](https://docs.conda.io/en/latest/) +[![run with apptainer](https://custom-icon-badges.demolab.com/badge/run%20with-apptainer-4545?logo=apptainer&color=teal&labelColor=000000)](https://apptainer.org/) [![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/) +[![run with conda](http://img.shields.io/badge/run%20with-conda-3EB049?labelColor=000000&logo=anaconda)](https://docs.conda.io/en/latest/) +[![run with singularity](https://img.shields.io/badge/run%20with-singularity-1d355c.svg?logo=singularity&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/stableexpression) [![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23stableexpression-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/stableexpression)[![Follow on Bluesky](https://img.shields.io/badge/bluesky-%40nf__core-1185fe?labelColor=000000&logo=bluesky)](https://bsky.app/profile/nf-co.re)[![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) @@ -42,7 +43,8 @@ nextflow run nf-core/stableexpression \ --outdir ``` - For more specific scenarios, __like fetching only specific conditions or using your own expression datasets__, please refer to the [usage documentation](https://nf-co.re/stableexpression/usage). +> [!IMPORTANT] + > For more specific scenarios, __like fetching only specific conditions or using your own expression dataset(s)__, please refer to the [usage documentation](https://nf-co.re/stableexpression/usage). > [!NOTE] > See [here](https://nf-co.re/stableexpression/usage#profiles) for more information about profiles. @@ -53,6 +55,12 @@ To see the results of an example test run with a full size dataset refer to the For more details about the output files and reports, please refer to the [output documentation](https://nf-co.re/stableexpression/output). +## Support us + +If you like nf-core/stableexpression, please make sure you give it a star on GitHub. + +[![stars - stableexpression](https://img.shields.io/github/stars/nf-core/stableexpression?style=social)](https://github.com/nf-core/stableexpression) + ## Credits nf-core/stableexpression was originally written by Olivier Coen. From 7ddbf89dce355e285ffb38f8c0ca521b659e2a32 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 10 Nov 2025 09:52:31 +0100 Subject: [PATCH 150/258] fix .github/workflows/release-announcements.yml with latest code --- .github/workflows/release-announcements.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release-announcements.yml b/.github/workflows/release-announcements.yml index 8509c7bb..431d3d44 100644 --- a/.github/workflows/release-announcements.yml +++ b/.github/workflows/release-announcements.yml @@ -17,8 +17,7 @@ jobs: - 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 - + 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@master with: access-token: ${{ secrets.MASTODON_ACCESS_TOKEN }} @@ -27,9 +26,7 @@ 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_topics.outputs.description }} - + ${{ steps.get_description.outputs.description }} Please see the changelog: ${{ github.event.release.html_url }} ${{ steps.get_topics.outputs.topics }} #nfcore #openscience #nextflow #bioinformatics From 7bfb57cda5cfb7b8271cbabf66de5a446868eb4e Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 10 Nov 2025 12:36:12 +0100 Subject: [PATCH 151/258] update get_geo_dataset_accessions.py to improve output and allow rnaseq data --- bin/get_geo_dataset_accessions.py | 361 ++++++++++++------------------ 1 file changed, 144 insertions(+), 217 deletions(-) diff --git a/bin/get_geo_dataset_accessions.py b/bin/get_geo_dataset_accessions.py index d361eca2..ddc65dec 100755 --- a/bin/get_geo_dataset_accessions.py +++ b/bin/get_geo_dataset_accessions.py @@ -5,13 +5,12 @@ import argparse import logging import tarfile +from functools import partial from multiprocessing import Pool from pathlib import Path from urllib.request import urlretrieve import pandas as pd - -# from random import sample import requests import xmltodict from Bio import Entrez @@ -33,7 +32,7 @@ # mandatory for running the script in an apptainer container # Entrez.Parser.Parser.directory("/tmp/biopython") -ACCESSION_OUTFILE_NAME = "accessions.tsv" +ACCESSION_OUTFILE_NAME = "accessions.txt" SPECIES_DATASETS_OUTFILE_NAME = "geo_all_datasets.metadata.tsv" REJECTED_DATASETS_OUTFILE_NAME = "geo_rejected_datasets.metadata.tsv" # WRONG_SPECS_DATASETS_METADATA_OUTFILE_NAME = "geo_wrong_platform_moltype_datasets.metadata.tsv" @@ -53,11 +52,11 @@ NB_PROBE_IDS_TO_SAMPLE = 10 ALLOWED_LIBRARY_SOURCES = ["transcriptomic", "RNA"] +ALLOWED_MOLECULE_TYPES = ["RNA", "SRA"] -# TODO: see how to integrate RNA-seq experiments as well GEO_EXPERIMENT_TYPE_TO_PLATFORM = { "Expression profiling by array": "microarray", - # "Expression profiling by high throughput sequencing": "rnaseq" + "Expression profiling by high throughput sequencing": "rnaseq", } MINIML_TMPDIR = "geo_miniml" @@ -218,7 +217,13 @@ def fetch_geo_datasets_for_species(species: str) -> list[dict]: Args: species (str): Scientific name of the species (e.g. "Homo sapiens"). """ - query = f'"{species}"[Organism] AND "gse"[Entry Type] AND "expression profiling by array"[DataSet Type]' + dataset_types = [ + f'"{experiment_type}"[DataSet Type]' + for experiment_type in GEO_EXPERIMENT_TYPE_TO_PLATFORM + ] + formatted_dataset_type = "(" + " OR ".join(dataset_types) + ")" + + query = f'"{species}"[Organism] AND "gse"[Entry Type] AND {formatted_dataset_type}' logger.info(f"Fetching GEO datasets with query: {query}") # getting list of all datasets IDs for this species @@ -299,13 +304,24 @@ def parse_dataset_metadata(file: Path, accession: str) -> dict | None: # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -def fetch_geo_platform_data(platform_accessions: list[str]) -> dict: +def fetch_geo_platform_metadata(datasets: list[dict]) -> dict: """ Fetch data for a GEO platform Args: platform_accession (str): accession of the platform """ + # unique list of platform accessions + platform_accessions = list( + set( + [ + platform_accession + for dataset in datasets + for platform_accession in dataset["platform_accessions"] + ] + ) + ) + # formating query formatted_platform_accessions = [ f'"{platform_accession}"[GEO Accession]' for platform_accession in platform_accessions @@ -318,150 +334,52 @@ def fetch_geo_platform_data(platform_accessions: list[str]) -> dict: ids = record.get("IdList", []) if not ids: logger.warning(f"No GEO platform found for accessions {platform_accessions}.") - return [] + return {} # fetching summary info - results = send_request_to_entrez_esummary(ids) - return results - - -def augment_with_platform_metadata( - datasets: list[dict], -) -> tuple[list[dict], list[dict]]: - # unique list of platform accessions - platform_accessions = list( - set( - [ - platform_accession - for dataset in datasets - for platform_accession in dataset["platform_accessions"] - ] - ) - ) # one single request to NCBI for all platform accessions - # we extract the platform accessions to allow better parsing afterwards - acc_to_metadata = { + platform_metadatas = send_request_to_entrez_esummary(ids) + # return dict associating dataset accessions with platform metadata + return { platform_metadata["Accession"]: platform_metadata - for platform_metadata in fetch_geo_platform_data(platform_accessions) + for platform_metadata in platform_metadatas } - # adding the platform metadata to the corresponding metadata - issues = [] - augmented_metadata_list = [] - for dataset in datasets: - accession = dataset["accession"] - platform_accessions = dataset["platform_accessions"] - dataset["platform_metadata"] = [] - - if not platform_accessions: - issues.append({"accession": accession, "reason": "NO PLATFORM ACCESSIONS"}) - continue - - for platform_accession in platform_accessions: - # filtering out cases where the platform metadata is not available - if platform_accession not in acc_to_metadata: - continue - # augmenting metadata with platform metadata - dataset["platform_metadata"].append(acc_to_metadata[platform_accession]) - - # getting list of platform taxon - platforms_taxons = [ - platform_metadata.get("taxon") - for platform_metadata in dataset["platform_metadata"] - if platform_metadata.get("taxon") is not None - ] - - # checking if there is one single platform taxon - # otherwise, checking the dataset - if not platforms_taxons: - logger.warning(f"No taxon found for dataset {accession}") - issues.append({"accession": accession, "reason": "NO PLATFORM TAXON"}) - continue - elif len(platforms_taxons) > 1: - logger.warning( - f"Multiple taxons for dataset {accession}: {platforms_taxons}" - ) - issues.append( - { - "accession": accession, - "reason": f"MULTIPLE PLATFORM TAXONS: {platforms_taxons}", - } - ) - continue - - dataset["platform_taxon"] = platforms_taxons[0] - augmented_metadata_list.append(dataset) - - return augmented_metadata_list, issues - - -""" -def download_platform_datatable(ftp_link: str, platform_accession: str) -> Path | None: - filename = f"soft/{platform_accession}_family.soft.gz" - ftp_url = ftp_link + filename - output_file = Path(PLATFORM_SOFT_TMPDIR) / f"{platform_accession}.gz" - download_file_at_url(ftp_url, output_file) - return output_file -""" - -""" -def get_platform_probe_id_samples(platform_accession: str) -> list[str]: - response = send_request_to_ncbi_api(platform_accession) - if response is None: - return [] - - header_found = False - probe_ids = [] - counter = 0 - for line in response.iter_lines(decode_unicode=True): - if counter >= NB_PROBE_IDS_TO_PARSE: - break - if line: - # removing HTML patterns - line = re.sub("<[^<]+?>", "", line).strip() - line = re.sub(r"", "", line) - # first things first: try to get the header - if not header_found: - if line.startswith("ID"): - header_found = True - continue - else: - # once the header was gotten, all successive lines are the data - probe_id = line.split("\t")[0] - probe_ids.append(probe_id) - counter += 1 - - # return a random sample of probe IDs - nb_samples = min(len(probe_ids), NB_PROBE_IDS_TO_SAMPLE) - return sample(probe_ids, nb_samples) - -def probe_ids_can_be_converted( - dataset_metadata: dict, species: str -) -> tuple[dict, bool]: - platform_dict_list = dataset_metadata["platform_metadata"] - all_probe_ids = [] +def check_platform_metadata( + dataset: dict, accession_to_platform_metadata: dict, species: str +) -> dict: + accession = dataset["accession"] + platform_accessions = dataset["platform_accessions"] - for platform_dict in platform_dict_list: - # looping until we find data for our species - if format_species(platform_dict["taxon"]) != format_species(species): - continue - # getting a sample of the first probe ids - sampled_probe_ids = get_platform_probe_id_samples(platform_dict["Accession"]) + if not platform_accessions: + return {accession: "NO PLATFORM ACCESSIONS"} - # if we could not get any probe ids for a platform, we won't use this dataset - if not sampled_probe_ids: - return dataset_metadata, False + platforms_metadata = [ + accession_to_platform_metadata[platform_accession] + for platform_accession in dataset["platform_accessions"] + ] - all_probe_ids += sampled_probe_ids + # getting list of platform taxon + platforms_taxons = [] + for metadata in platforms_metadata: + if metadata.get("taxon") is not None: + platforms_taxons += metadata.get("taxon").split("; ") + platforms_taxons = list(set(platforms_taxons)) + + # checking if there is one single platform taxon + # otherwise, checking the dataset + if not platforms_taxons: + return {accession: "NO PLATFORM TAXON"} + + # checking that at least one platform has the correct taxon + if not any( + format_species(species) == format_species(taxon) for taxon in platforms_taxons + ): + return {accession: f"TAXON MISMATCH: {platforms_taxons}"} - # try to convert ids - mapping_dict, _ = convert_ids(all_probe_ids, species) + return {} - # if at least one ID could be converted - can_be_converted = True if mapping_dict else False - return dataset_metadata, can_be_converted -""" # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # FORMATTING @@ -469,7 +387,7 @@ def probe_ids_can_be_converted( def format_species(species: str) -> str: - return "_".join(species.lower().split(" ")[:2]) + return "_".join(species.lower().split(" ")) def format_platform_name(platform_name: str) -> str: @@ -617,45 +535,53 @@ def exclude_unwanted_accessions( return datasets_to_keep -def check_species_issues(parsed_species_list: list, species: str) -> dict: +def check_species_issues(parsed_species_list: list, species: str) -> str | None: # trying to find our species in the list of species parsed for parsed_species in parsed_species_list: if format_species(parsed_species) == format_species(species): - return {} - return {"parsed_species": parsed_species_list} + return None + return f"PARSED SPECIES: {parsed_species_list}" -def check_molecule_type_issues(molecules_types: list) -> dict: +def check_molecule_type_issues(molecules_types: list) -> str | None: # we want only GEO series that contain only RNA molecules # for other series, they should be superseries contained other series that are being parsed too # so anyway, this would lead in duplicates - if all(["rna" in molecule_type.lower() for molecule_type in molecules_types]): - return {} - return {"molecule_types": molecules_types} + if any( + [ + molecule_type.upper() in ALLOWED_MOLECULE_TYPES + for molecule_type in molecules_types + ] + ): + return None + return f"MOLECULE TYPES: {molecules_types}" -def check_experiment_type_issues(experiment_types: list | str, platform: str) -> dict: +def check_experiment_type_issues( + experiment_types: list | str, platform: str +) -> str | None: experiment_types = ( experiment_types if isinstance(experiment_types, list) else [experiment_types] ) for experiment_type in experiment_types: # if at least one experiment type is ok, we keep this dataset if GEO_EXPERIMENT_TYPE_TO_PLATFORM.get(experiment_type) == platform: - return {} - return {"experiment_types": experiment_types} + return None + return f"EXPERIMENT TYPES: {experiment_types}" -def check_source_issues(library_sources: list) -> dict: +def check_source_issues(library_sources: list) -> str | None: # if we have no data about library sources, we just cannot infer if not library_sources: - return {} - if len(library_sources) == 1 and library_sources[0] in ALLOWED_LIBRARY_SOURCES: - return {} - # TODO: see how to process series with multiple library sources - return {"library_sources": library_sources} + return None + if any( + library_source in ALLOWED_LIBRARY_SOURCES for library_source in library_sources + ): + return None + return f"LIBRARY SOURCES: {library_sources}" -def search_keywords(dataset: dict, keywords: list[str]) -> tuple[list, dict]: +def search_keywords(dataset: dict, keywords: list[str]) -> tuple[list, str | None]: accession = dataset["accession"] all_searchable_fields = ( [dataset["summary"], dataset["title"]] @@ -668,9 +594,9 @@ def search_keywords(dataset: dict, keywords: list[str]) -> tuple[list, dict]: if found_keywords: dataset["found_keywords"] = list(set(found_keywords)) logger.info(f"Found keywords: {found_keywords} in accession {accession}") - return found_keywords, {} + return found_keywords, None else: - return [], {"accession": accession, "keywords_found": False} + return [], "NO KEYWORDS_FOUND" def check_dataset( @@ -682,29 +608,33 @@ def check_dataset( library_sources = dataset["sample_library_sources"] molecules_types = dataset["sample_molecule_types"] + issues = [] + # checking species - issue_dict = check_species_issues(parsed_species_list, species) + if issue := check_species_issues(parsed_species_list, species): + issues.append(issue) # checking platform if platform is not None: - platform_issue_dict = check_experiment_type_issues(experiment_types, platform) - issue_dict |= platform_issue_dict + if issue := check_experiment_type_issues(experiment_types, platform): + issues.append(issue) # checking that library sources fit - transcriptomic_issue_dict = check_source_issues(library_sources) - issue_dict |= transcriptomic_issue_dict + if issue := check_source_issues(library_sources): + issues.append(issue) # checking that all molecule types are RNA - moltype_issue_dict = check_molecule_type_issues(molecules_types) - issue_dict |= moltype_issue_dict + if issue := check_molecule_type_issues(molecules_types): + issues.append(issue) found_keywords = [] if keywords: - found_keywords, keyword_issue_dict = search_keywords(dataset, keywords) - issue_dict |= keyword_issue_dict + found_keywords, keyword_issue = search_keywords(dataset, keywords) + if keyword_issue: + issues.append(keyword_issue) - if issue_dict: - rejection_dict = {"accession": accession, "reasons": issue_dict} + if issues: + rejection_dict = {accession: issues} else: rejection_dict = {} @@ -769,7 +699,7 @@ def main(): ) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # PARSING METADATA + # PARSING DATASET METADATA # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ logger.info(f"Parsing metadata for {len(datasets)} datasets") @@ -790,67 +720,61 @@ def main(): # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ logger.info(f"Validating {len(augmented_datasets)} datasets") - selected_datasets = [] - rejected_datasets = [] + checked_datasets = [] + rejection_dict = {} for dataset in tqdm(augmented_datasets): - found_keywords, rejection_dict = check_dataset( + found_keywords, issue_dict = check_dataset( dataset, args.species, args.platform, args.keywords ) - if rejection_dict: - rejected_datasets.append(rejection_dict) + if issue_dict: + rejection_dict |= issue_dict else: if found_keywords: dataset["found_keywords"] = found_keywords - selected_datasets.append(dataset) + checked_datasets.append(dataset) + + logger.info(f"Validated {len(checked_datasets)} datasets") # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # GETTING METADATA OF SEQUENCING PLATFORMS # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - logger.info(f"Getting platform metadata for {len(selected_datasets)} datasets") - selected_datasets_chunks = chunk_list( - selected_datasets, PLATFORM_METADATA_CHUNKSIZE - ) + logger.info("Getting platform metadata") + # making chunks to group requests to NCBI GEO + checked_datasets_chunks = chunk_list(checked_datasets, PLATFORM_METADATA_CHUNKSIZE) # resetting selecting datasets - selected_datasets = [] - for selected_datasets_chunk in tqdm(selected_datasets_chunks): - augmented_datasets, issues = augment_with_platform_metadata( + accession_to_platform_metadata = {} + for selected_datasets_chunk in tqdm(checked_datasets_chunks): + accession_to_platform_metadata |= fetch_geo_platform_metadata( selected_datasets_chunk ) - selected_datasets += augmented_datasets - rejected_datasets += issues - """ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # FILTERING OUT DATASETS FOR WHICH ID MAPPING DOES NOT WORK + # CHECKING PLATFORM METADATA # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # this cannot be done in parallel because it requires HTTP requests - logger.info( - f"Checking gene ID mapping issues for {len(platform_augmented_dataset_metadata_list)} datasets" + logger.info(f"Checking platform metadata for {len(checked_datasets)} datasets") + func = partial( + check_platform_metadata, + accession_to_platform_metadata=accession_to_platform_metadata, + species=args.species, ) - func = partial(probe_ids_can_be_converted, species=args.species) - final_metadata_list = [] + selected_datasets = [] + # resetting selecting datasets + for dataset in tqdm(checked_datasets): + accession = dataset["accession"] + issue_dict = func(dataset) + if issue_dict: + if accession in rejection_dict: # should not happen but in case + rejection_dict[accession] += issue_dict[accession] + else: + rejection_dict |= issue_dict + else: + selected_datasets.append(dataset) - with ( - Pool(processes=args.nb_cpus) as p, - tqdm(total=len(platform_augmented_dataset_metadata_list)) as pbar, - ): - for metadata, can_be_converted in p.imap_unordered( - func, platform_augmented_dataset_metadata_list - ): - pbar.update() - pbar.refresh() - if can_be_converted: - final_metadata_list.append(metadata) - - export_filtered_out_datasets_if_any( - platform_augmented_dataset_metadata_list, - final_metadata_list, - GENE_ID_MAPPING_ISSUES_DATASETS_METADATA_OUTFILE_NAME, - "gene id mapping", - ) - """ + if rejection_dict: + logger.warning(f"{len(rejection_dict)} datasets rejected") + logger.warning(f"Reasons for rejection: {rejection_dict}") # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # EXPORTING ACCESSIONS @@ -858,11 +782,9 @@ def main(): logger.info(f"Kept {len(selected_datasets)} datasets") # getting accessions of selected experiments - selected_accessions = [ - {"accession": dataset["accession"], "platform_taxon": dataset["platform_taxon"]} - for dataset in selected_datasets - ] - export_dataset_metadatas(selected_accessions, ACCESSION_OUTFILE_NAME) + selected_accessions = [dataset["accession"] for dataset in selected_datasets] + with open(ACCESSION_OUTFILE_NAME, "w") as fout: + fout.write("\n".join(selected_accessions)) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # EXPORTING DATASETS @@ -870,6 +792,11 @@ def main(): export_dataset_metadatas(augmented_datasets, SPECIES_DATASETS_OUTFILE_NAME) export_dataset_metadatas(selected_datasets, SELECTED_DATASETS_OUTFILE_NAME) + + rejected_datasets = [ + {"accession": accession, "reason": reason} + for accession, reason in rejection_dict.items() + ] export_dataset_metadatas( rejected_datasets, REJECTED_DATASETS_OUTFILE_NAME, clean_columns=False ) From efbaad31650714c495fd1d3832690aa4dafadc04 Mon Sep 17 00:00:00 2001 From: Olivier Date: Tue, 11 Nov 2025 14:16:14 +0100 Subject: [PATCH 152/258] clean code; remove superseries from geo accessions --- bin/download_geo_data.R | 5 +- bin/get_geo_dataset_accessions.py | 246 +++++++++--------- modules/local/expressionatlas/getdata/main.nf | 5 +- modules/local/geo/getaccessions/main.nf | 2 +- modules/local/geo/getdata/main.nf | 7 +- subworkflows/local/geo_fetchdata/main.nf | 12 +- .../main.nf | 27 +- workflows/stableexpression.nf | 8 +- 8 files changed, 170 insertions(+), 142 deletions(-) diff --git a/bin/download_geo_data.R b/bin/download_geo_data.R index b38b3cd2..01fb660c 100755 --- a/bin/download_geo_data.R +++ b/bin/download_geo_data.R @@ -162,7 +162,7 @@ clean_count_data <- function(df) { } -process_data <- function(atlas_data, accession, species) { +process_data <- function(geo_data, accession, species) { eset <- geo_data[[ 1 ]] #print(exprs(eset)) @@ -187,11 +187,10 @@ process_data <- function(atlas_data, accession, species) { data <- geo_data [[ file ]] - #print(fData(data)) # get count data for samples corresponding to the species of interest count_df <- data.frame(exprs(data)) %>% select(all_of(species_samples)) - print(count_df) + # checking that data are from RMA pipeline and followed proper normalisation # raises error otherwise check_microarray_normalisation(count_df) diff --git a/bin/get_geo_dataset_accessions.py b/bin/get_geo_dataset_accessions.py index ddc65dec..b9e39a83 100755 --- a/bin/get_geo_dataset_accessions.py +++ b/bin/get_geo_dataset_accessions.py @@ -51,12 +51,17 @@ NB_PROBE_IDS_TO_PARSE = 1000 NB_PROBE_IDS_TO_SAMPLE = 10 +SUPERSERIES_SUMMARY = "This SuperSeries is composed of the SubSeries listed below." + ALLOWED_LIBRARY_SOURCES = ["transcriptomic", "RNA"] -ALLOWED_MOLECULE_TYPES = ["RNA", "SRA"] +ALLOWED_MOLECULE_TYPES = [ + "RNA", + # "SRA" +] GEO_EXPERIMENT_TYPE_TO_PLATFORM = { "Expression profiling by array": "microarray", - "Expression profiling by high throughput sequencing": "rnaseq", + # "Expression profiling by high throughput sequencing": "rnaseq", } MINIML_TMPDIR = "geo_miniml" @@ -258,7 +263,30 @@ def fetch_geo_datasets_for_species(species: str) -> list[dict]: results = send_request_to_entrez_esummary(ids) # keeping only series datasets (just a double check here) - return [r for r in results if "GSE" in r["Accession"]] + # and removing superseries (they are just containers of series that are also contained here) + return [ + r + for r in results + if "GSE" in r["Accession"] and r["summary"] != SUPERSERIES_SUMMARY + ] + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# FORMATTING +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +def format_species(species: str) -> str: + return "_".join(species.lower().split(" ")) + + +def format_platform_name(platform_name: str) -> str: + return platform_name.replace("_", "").replace("-", "").lower() + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# GET METADATA +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def download_dataset_metadata(ftp_link: str, accession: str) -> Path | None: @@ -299,106 +327,6 @@ def parse_dataset_metadata(file: Path, accession: str) -> dict | None: return xmltodict.parse(xml_content)["MINiML"] -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# GEO PLATFORMS -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - -def fetch_geo_platform_metadata(datasets: list[dict]) -> dict: - """ - Fetch data for a GEO platform - - Args: - platform_accession (str): accession of the platform - """ - # unique list of platform accessions - platform_accessions = list( - set( - [ - platform_accession - for dataset in datasets - for platform_accession in dataset["platform_accessions"] - ] - ) - ) - # formating query - formatted_platform_accessions = [ - f'"{platform_accession}"[GEO Accession]' - for platform_accession in platform_accessions - ] - platform_accessions_str = " OR ".join(formatted_platform_accessions) - query = f'({platform_accessions_str}) AND "gpl"[Entry Type] ' - - record = send_request_to_entrez_esearch(query=query) - - ids = record.get("IdList", []) - if not ids: - logger.warning(f"No GEO platform found for accessions {platform_accessions}.") - return {} - - # fetching summary info - # one single request to NCBI for all platform accessions - platform_metadatas = send_request_to_entrez_esummary(ids) - # return dict associating dataset accessions with platform metadata - return { - platform_metadata["Accession"]: platform_metadata - for platform_metadata in platform_metadatas - } - - -def check_platform_metadata( - dataset: dict, accession_to_platform_metadata: dict, species: str -) -> dict: - accession = dataset["accession"] - platform_accessions = dataset["platform_accessions"] - - if not platform_accessions: - return {accession: "NO PLATFORM ACCESSIONS"} - - platforms_metadata = [ - accession_to_platform_metadata[platform_accession] - for platform_accession in dataset["platform_accessions"] - ] - - # getting list of platform taxon - platforms_taxons = [] - for metadata in platforms_metadata: - if metadata.get("taxon") is not None: - platforms_taxons += metadata.get("taxon").split("; ") - platforms_taxons = list(set(platforms_taxons)) - - # checking if there is one single platform taxon - # otherwise, checking the dataset - if not platforms_taxons: - return {accession: "NO PLATFORM TAXON"} - - # checking that at least one platform has the correct taxon - if not any( - format_species(species) == format_species(taxon) for taxon in platforms_taxons - ): - return {accession: f"TAXON MISMATCH: {platforms_taxons}"} - - return {} - - -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# FORMATTING -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - -def format_species(species: str) -> str: - return "_".join(species.lower().split(" ")) - - -def format_platform_name(platform_name: str) -> str: - return platform_name.replace("_", "").replace("-", "").lower() - - -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# METADATA -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - def parse_characteristics( characteristics: str | dict | list, stored_characteristics: list ): @@ -436,6 +364,11 @@ def parse_interesting_metadata( "GPL" + gpl_id for gpl_id in dataset_metadata["GPL"].split(";") ] + experiment_types = dataset_metadata["gdsType"] + experiment_types = ( + experiment_types if isinstance(experiment_types, list) else [experiment_types] + ) + # if additional metadata have sample information if "Sample" in additional_metadata: # change to list if it's a single dictionary @@ -479,7 +412,7 @@ def parse_interesting_metadata( "summary": dataset_metadata["summary"], "title": dataset_metadata["title"], "overall_design": additional_metadata["Series"]["Overall-Design"], - "experiment_types": dataset_metadata["gdsType"], + "experiment_types": experiment_types, "sample_characteristics": list(set(sample_characteristics)), "sample_library_strategies": list(set(sample_library_strategies)), "sample_library_sources": list(set(sample_library_sources)), @@ -489,7 +422,7 @@ def parse_interesting_metadata( } -def parse_metadata(dataset_metadata: dict) -> dict | None: +def fetch_dataset_metadata(dataset_metadata: dict) -> dict | None: """ Parses metadata from a dataset metadata dictionary. @@ -557,12 +490,7 @@ def check_molecule_type_issues(molecules_types: list) -> str | None: return f"MOLECULE TYPES: {molecules_types}" -def check_experiment_type_issues( - experiment_types: list | str, platform: str -) -> str | None: - experiment_types = ( - experiment_types if isinstance(experiment_types, list) else [experiment_types] - ) +def check_experiment_type_issues(experiment_types: list, platform: str) -> str | None: for experiment_type in experiment_types: # if at least one experiment type is ok, we keep this dataset if GEO_EXPERIMENT_TYPE_TO_PLATFORM.get(experiment_type) == platform: @@ -641,6 +569,92 @@ def check_dataset( return found_keywords, rejection_dict +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# GEO PLATFORMS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +def fetch_geo_platform_metadata(datasets: list[dict]) -> dict: + """ + Fetch data for a GEO platform + + Args: + platform_accession (str): accession of the platform + """ + # unique list of platform accessions + platform_accessions = list( + set( + [ + platform_accession + for dataset in datasets + for platform_accession in dataset["platform_accessions"] + ] + ) + ) + # formating query + formatted_platform_accessions = [ + f'"{platform_accession}"[GEO Accession]' + for platform_accession in platform_accessions + ] + platform_accessions_str = " OR ".join(formatted_platform_accessions) + query = f'({platform_accessions_str}) AND "gpl"[Entry Type] ' + + record = send_request_to_entrez_esearch(query=query) + + ids = record.get("IdList", []) + if not ids: + logger.warning(f"No GEO platform found for accessions {platform_accessions}.") + return {} + + # fetching summary info + # one single request to NCBI for all platform accessions + platform_metadatas = send_request_to_entrez_esummary(ids) + # return dict associating dataset accessions with platform metadata + return { + platform_metadata["Accession"]: platform_metadata + for platform_metadata in platform_metadatas + } + + +def check_dataset_platforms( + dataset: dict, accession_to_platform_metadata: dict, species: str +) -> dict: + accession = dataset["accession"] + platform_accessions = dataset["platform_accessions"] + + if not platform_accessions: + return {accession: "NO PLATFORM ACCESSIONS"} + + platforms_metadata = [ + accession_to_platform_metadata[platform_accession] + for platform_accession in dataset["platform_accessions"] + ] + + # getting list of platform taxon + platforms_taxons = [] + for metadata in platforms_metadata: + if metadata.get("taxon") is not None: + platforms_taxons += metadata.get("taxon").split("; ") + platforms_taxons = list(set(platforms_taxons)) + + if not platforms_taxons: + return {accession: "NO PLATFORM TAXON"} + + # checking if at least one of the platform accession is the good one + # sample will be further filtered during download (download_geo_data.R) + if not any( + format_species(species) == format_species(taxon) for taxon in platforms_taxons + ): + return {accession: f"TAXON MISMATCH: {platforms_taxons}"} + + return {} + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# EXPORT +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + def export_dataset_metadatas( datasets: list[dict], output_file: str, clean_columns: bool = True ): @@ -708,7 +722,7 @@ def main(): Pool(processes=args.nb_cpus) as p, tqdm(total=len(datasets)) as pbar, ): - for result in p.imap_unordered(parse_metadata, datasets): + for result in p.imap_unordered(fetch_dataset_metadata, datasets): pbar.update() pbar.refresh() if result is None: @@ -716,7 +730,7 @@ def main(): augmented_datasets.append(result) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # CHECKING DATASET METADATA + # VALIDATING DATASETS # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ logger.info(f"Validating {len(augmented_datasets)} datasets") @@ -750,12 +764,12 @@ def main(): ) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # CHECKING PLATFORM METADATA + # VALIDATING EACH PLATFORM SEPARATELY, DATASET BY DATASET # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - logger.info(f"Checking platform metadata for {len(checked_datasets)} datasets") + logger.info(f"Checking each platform for {len(checked_datasets)} datasets") func = partial( - check_platform_metadata, + check_dataset_platforms, accession_to_platform_metadata=accession_to_platform_metadata, species=args.species, ) diff --git a/modules/local/expressionatlas/getdata/main.nf b/modules/local/expressionatlas/getdata/main.nf index f907e9a9..6a5a2ece 100644 --- a/modules/local/expressionatlas/getdata/main.nf +++ b/modules/local/expressionatlas/getdata/main.nf @@ -15,15 +15,14 @@ process EXPRESSIONATLAS_GETDATA { val accession output: - tuple val(meta), path("*.counts.csv"), optional: true, emit: counts - tuple val(meta), path("*.design.csv"), optional: true, emit: design + path("*.counts.csv"), optional: true, emit: counts + path("*.design.csv"), optional: true, emit: design tuple val(accession), path("failure_reason.txt"), optional: true, topic: eatlas_failure_reason tuple val(accession), path("warning_reason.txt"), optional: true, topic: eatlas_warning_reason tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //"'), topic: versions tuple val("${task.process}"), val('ExpressionAtlas'), eval('Rscript -e "cat(as.character(packageVersion(\'ExpressionAtlas\')))"'), topic: versions script: - meta = [accession: accession] """ download_eatlas_data.R --accession $accession """ diff --git a/modules/local/geo/getaccessions/main.nf b/modules/local/geo/getaccessions/main.nf index 040c6f2b..1bc6061d 100644 --- a/modules/local/geo/getaccessions/main.nf +++ b/modules/local/geo/getaccessions/main.nf @@ -17,7 +17,7 @@ process GEO_GETACCESSIONS { val accessions output: - path "accessions.tsv", optional: true, emit: accessions + path "accessions.txt", optional: true, emit: accessions path "geo_selected_datasets.metadata.tsv", optional: true, topic: geo_selected_datasets path "geo_all_datasets.metadata.tsv", optional: true, topic: geo_all_datasets path "geo_rejected_datasets.metadata.tsv", optional: true, topic: geo_rejected_datasets diff --git a/modules/local/geo/getdata/main.nf b/modules/local/geo/getdata/main.nf index 410da0ba..c7369e53 100644 --- a/modules/local/geo/getdata/main.nf +++ b/modules/local/geo/getdata/main.nf @@ -12,12 +12,12 @@ process GEO_GETDATA { 'community.wave.seqera.io/library/bioconductor-geoquery_r-base_r-dplyr_r-optparse:fcd002470b7d6809' }" input: - tuple val(meta), val(accession) + val accession val species output: - tuple val(meta), path("*.counts.csv"), optional: true, emit: counts - tuple val(meta), path("*.design.csv"), optional: true, emit: design + path("*.counts.csv"), optional: true, emit: counts + path("*.design.csv"), optional: true, emit: design tuple val(accession), path("failure_reason.txt"), optional: true, topic: geo_failure_reason tuple val(accession), path("warning_reason.txt"), optional: true, topic: geo_warning_reason tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //"'), topic: versions @@ -25,7 +25,6 @@ process GEO_GETDATA { tuple val("${task.process}"), val('dplyr'), eval('Rscript -e "cat(as.character(packageVersion(\'dplyr\')))"'), topic: versions script: - meta = meta + [accession: accession] """ download_geo_data.R \\ --accession $accession \\ diff --git a/subworkflows/local/geo_fetchdata/main.nf b/subworkflows/local/geo_fetchdata/main.nf index 62a2e5f4..4b0fe744 100644 --- a/subworkflows/local/geo_fetchdata/main.nf +++ b/subworkflows/local/geo_fetchdata/main.nf @@ -71,8 +71,7 @@ workflow GEO_FETCHDATA { ) GEO_GETACCESSIONS.out.accessions - .splitCsv(header: true, sep: '\t') - .map { row -> [ [ platform_taxon: row["platform_taxon"] ], row["accession"] ] } + .splitText() .set { ch_fetched_accessions } } @@ -85,17 +84,10 @@ workflow GEO_FETCHDATA { Channel.fromList( params.geo_accessions.tokenize(',') ) .mix( ch_geo_accessions_file.splitText() ) + .mix( ch_fetched_accessions ) .unique() .filter { acc -> acc.startsWith('GSE') } .map { acc -> acc.trim() } - .set { ch_input_accessions } - - // appending to accessions provided by the user - // ensures that no accessions is present twice (provided by the user and fetched from GEO) - ch_input_accessions - .map { accession -> [ [ platform_taxon: species ], accession ] } - .mix( ch_fetched_accessions ) - .unique() .set { ch_accessions } // ------------------------------------------------------------------------------------ diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index dfffd6ab..6ce914b4 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -323,9 +323,9 @@ def formatVersionsToYAML( ch_versions ) { def addDatasetIdToMetadata( ch_files ) { return ch_files .map { - meta, file -> - def new_meta = meta + [ dataset: file.getSimpleName() ] - [new_meta, file] + file -> + def meta = [ dataset: file.getSimpleName() ] + [meta, file] } } @@ -402,3 +402,24 @@ def getWholeDatasetSize( ch_counts ) { .reduce { size_1, size_2 -> size_1 + size_2 } .flatten() } + + +/* +======================================================================================== + FUNCTIONS FOR DISPLAYING INFORMATION ABOUT DATA +======================================================================================== +*/ + +def checkCounts(ch_counts) { + // display a warning if no datasets are found + def msg = ( + "No dataset found. " + + "Please note that for the moment only Microarray count datasets are fetched from NCBI GEO. " + + "\nYou can check at https://www.ncbi.nlm.nih.gov/gds if there are raw RNA-seq count datasets for this species. " + ) + ch_counts.count().map { n -> + if( n == 0 ) { + log.warn(msg) + } + } +} diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 57bb96ee..ba05fb06 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -18,6 +18,9 @@ include { AGGREGATE_RESULTS } from '../modules/local/aggreg include { DASH_APP } from '../modules/local/dash_app' include { storeDatasetSize } from '../subworkflows/local/utils_nfcore_stableexpression_pipeline' +include { checkCounts } from '../subworkflows/local/utils_nfcore_stableexpression_pipeline' + + /* @@ -64,10 +67,11 @@ workflow STABLEEXPRESSION { .concat( GEO_FETCHDATA.out.downloaded_datasets ) .set { ch_counts } + // store nb of genes and nb f samples at this stage in the meta maps ch_counts = storeDatasetSize( ch_counts, "nb_genes", "nb_samples" ) - // display a warning if no datasets are found - ch_counts.count().map { n -> if( n == 0 ) { log.warn "No datasets found" } } + // displays a message if no dataset was found + checkCounts( ch_counts ) if ( !params.accessions_only && !params.download_only ) { From 3b093643394d46cc4ffe138017e199603bb5ebf1 Mon Sep 17 00:00:00 2001 From: Olivier Date: Tue, 11 Nov 2025 16:42:36 +0100 Subject: [PATCH 153/258] allow geo datasets containing multiple datasets --- bin/download_geo_data.R | 74 +++++++++++-------- modules/local/geo/getaccessions/main.nf | 4 +- .../local/expressionatlas_fetchdata/main.nf | 5 +- subworkflows/local/geo_fetchdata/main.nf | 7 +- tests/default.nf.test | 30 +++++++- 5 files changed, 80 insertions(+), 40 deletions(-) diff --git a/bin/download_geo_data.R b/bin/download_geo_data.R index 01fb660c..53054b2a 100755 --- a/bin/download_geo_data.R +++ b/bin/download_geo_data.R @@ -51,7 +51,7 @@ get_samples_for_species <- function(eset, species) { } # return a data.frame with matching samples - pheno$geo_accession[keep] + return(pheno$geo_accession[keep]) } @@ -158,52 +158,62 @@ clean_count_data <- function(df) { message("Cleaning counts") # removes rows that are all NA df <- df[rowSums(!is.na(df)) > 0, ] - + return(df) } process_data <- function(geo_data, accession, species) { - eset <- geo_data[[ 1 ]] - #print(exprs(eset)) - # Get metadata table - metadata_df <- pData(eset) - design_df <- build_design_dataframe(metadata_df, accession) + for (i in 1:length(geo_data)) { - # get samples corresponding to species - species_samples <- get_samples_for_species(eset, species) + eset <- geo_data[[ i ]] - # filter design dataframe - design_df <- design_df %>% - filter(sample %in% species_samples) + #print(exprs(eset)) + # Get metadata table + metadata_df <- pData(eset) + design_df <- build_design_dataframe(metadata_df, accession) - if ( length(names(geo_data)) > 1 ) { - warning("Multiple data files were found") - write("EXPERIMENT CONTAINS MULTIPLE FILES", file = FAILURE_REASON_FILE) - quit(save = "no", status = 0) - } + # get samples corresponding to species + species_samples <- get_samples_for_species(eset, species) + + # filter design dataframe + design_df <- design_df %>% + filter(sample %in% species_samples) - file <- names(geo_data)[[ 1 ]] + file <- names(geo_data)[[ i ]] - data <- geo_data [[ file ]] + data <- geo_data [[ file ]] - # get count data for samples corresponding to the species of interest - count_df <- data.frame(exprs(data)) %>% - select(all_of(species_samples)) + # keeping only non empty data + if (nrow(data) == 0) { + message(paste0("No data found for ", file)) + next + } - # checking that data are from RMA pipeline and followed proper normalisation - # raises error otherwise - check_microarray_normalisation(count_df) + # get count data for samples corresponding to the species of interest + count_df <- data.frame(exprs(data)) %>% + select(all_of(species_samples)) - # clean counts: - # * removes rows that are all NA - count_df <- clean_count_data(count_df) + # keeping only non empty data + if (nrow(count_df) == 0 || ncol(count_df) == 0) { + message(paste0("No data found for ", file)) + next + } - # exporting count data to CSV - export_count_data(count_df, accession) + # checking that data are from RMA pipeline and followed proper normalisation + # raises error otherwise + check_microarray_normalisation(count_df) - # exporting metadata to CSV - export_metadata(design_df, accession) + # clean counts: + # * removes rows that are all NA + count_df <- clean_count_data(count_df) + + # exporting count data to CSV + export_count_data(count_df, accession) + + # exporting metadata to CSV + export_metadata(design_df, accession) + } } diff --git a/modules/local/geo/getaccessions/main.nf b/modules/local/geo/getaccessions/main.nf index 1bc6061d..1294c0f1 100644 --- a/modules/local/geo/getaccessions/main.nf +++ b/modules/local/geo/getaccessions/main.nf @@ -13,7 +13,7 @@ process GEO_GETACCESSIONS { val species val keywords val platform - val excluded_accessions_file + path excluded_accessions_file val accessions output: @@ -39,7 +39,7 @@ process GEO_GETACCESSIONS { if ( platform != 'none' ) { args += " --platform $platform" } - if ( excluded_accessions_file != 'none' ) { + if ( excluded_accessions_file != [] ) { args += " --exclude-accessions-in $excluded_accessions_file" } if ( accessions != 'none' ) { diff --git a/subworkflows/local/expressionatlas_fetchdata/main.nf b/subworkflows/local/expressionatlas_fetchdata/main.nf index 81d200aa..1904858d 100644 --- a/subworkflows/local/expressionatlas_fetchdata/main.nf +++ b/subworkflows/local/expressionatlas_fetchdata/main.nf @@ -79,8 +79,9 @@ workflow EXPRESSIONATLAS_FETCHDATA { EXPRESSIONATLAS_GETDATA( ch_accessions ) // adding dataset id (accession + data_type) in the file meta - ch_design = addDatasetIdToMetadata( EXPRESSIONATLAS_GETDATA.out.design ) - ch_counts = addDatasetIdToMetadata( EXPRESSIONATLAS_GETDATA.out.counts ) + // flattening in case multiple files are returned at once + ch_design = addDatasetIdToMetadata( EXPRESSIONATLAS_GETDATA.out.design.flatten() ) + ch_counts = addDatasetIdToMetadata( EXPRESSIONATLAS_GETDATA.out.counts.flatten() ) // adding design files to the meta of their respective count files ch_eatlas_datasets = groupFilesByDatasetId( ch_design, ch_counts ) diff --git a/subworkflows/local/geo_fetchdata/main.nf b/subworkflows/local/geo_fetchdata/main.nf index 4b0fe744..28e95135 100644 --- a/subworkflows/local/geo_fetchdata/main.nf +++ b/subworkflows/local/geo_fetchdata/main.nf @@ -49,7 +49,7 @@ workflow GEO_FETCHDATA { sort: true, newLine: true ) - .ifEmpty('none') + .ifEmpty( [] ) .set { ch_excluded_accessions_file } // ------------------------------------------------------------------------------------ @@ -103,8 +103,9 @@ workflow GEO_FETCHDATA { ) // adding dataset id (accession + data_type) in the file meta - ch_design = addDatasetIdToMetadata( GEO_GETDATA.out.design ) - ch_counts = addDatasetIdToMetadata( GEO_GETDATA.out.counts ) + // flattening in case multiple files are returned at once + ch_design = addDatasetIdToMetadata( GEO_GETDATA.out.design.flatten() ) + ch_counts = addDatasetIdToMetadata( GEO_GETDATA.out.counts.flatten() ) // adding design files to the meta of their respective count files ch_datasets = groupFilesByDatasetId( ch_design, ch_counts ) diff --git a/tests/default.nf.test b/tests/default.nf.test index 8429ade2..02b29a21 100644 --- a/tests/default.nf.test +++ b/tests/default.nf.test @@ -64,7 +64,7 @@ nextflow_pipeline { when { params { - species = 'beta vulgaris' + species = 'homo_sapiens' accessions_only = true outdir = "$outputDir" } @@ -248,6 +248,34 @@ nextflow_pipeline { } } + test("-profile test_no_dataset_found") { + + when { + params { + species = 'marmota_marmota_marmota' + outdir = "$outputDir" + } + } + + then { + // stable_name: All files + folders in ${params.outdir}/ with a stable name + def stable_name = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}']) + // stable_path: All files in ${params.outdir}/ with stable content + def stable_path = getAllFilesFromDir(params.outdir, ignoreFile: 'tests/.nftignore') + assertAll( + { assert workflow.success}, + { 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_stableexpression_software_mqc_versions.yml"), + // All stable path name, with a relative path + stable_name, + // All files with stable contents + stable_path + ).match() } + ) + } + } + test("-profile test_full") { when { From c6bbfade25d86f0f0e7b1b2130671706eb55969c Mon Sep 17 00:00:00 2001 From: Olivier Date: Tue, 11 Nov 2025 18:09:57 +0100 Subject: [PATCH 154/258] change base.config times --- conf/base.config | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/conf/base.config b/conf/base.config index 9cfe4df8..c0f825d5 100644 --- a/conf/base.config +++ b/conf/base.config @@ -34,22 +34,22 @@ process { withLabel:process_low { cpus = { 2 } memory = { 4.GB * task.attempt } - time = { 1.h * task.attempt } + time = { 2.h * task.attempt } } withLabel:process_medium { cpus = { 6 * task.attempt } memory = { 10.GB * task.attempt } - time = { 2.h * task.attempt } + time = { 4.h * task.attempt } } withLabel:process_high_cpus { cpus = { 12 * task.attempt } memory = { 10.GB * task.attempt } - time = { 2.h * task.attempt } + time = { 8.h * task.attempt } } withLabel:process_high { cpus = { 12 * task.attempt } memory = { 20.GB * task.attempt } - time = { 4.h * task.attempt } + time = { 8.h * task.attempt } } withLabel:error_ignore { errorStrategy = 'ignore' From 3821964b3cd38f81d6b2b0e49d4eddd2cf19a55a Mon Sep 17 00:00:00 2001 From: Olivier Date: Tue, 11 Nov 2025 18:30:49 +0100 Subject: [PATCH 155/258] add troubleshooting doc --- README.md | 13 ++++++++++++- docs/troubleshooting.md | 32 ++++++++++++++++++++++++++++++++ docs/usage.md | 16 ++++++++++------ 3 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 docs/troubleshooting.md diff --git a/README.md b/README.md index 5790973f..3cf6abce 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,18 @@ ## Introduction -**nf-core/stableexpression** is a bioinformatics pipeline that aims at finding the most stable genes among a single or multiple public / local count datasets. It takes as main inputs a species name (mandatory), keywords for expression atlas search (optional) and / or a CSV input file listing local raw / normalised count datasets (optional). **A typical usage is to find the most suitable qPCR housekeeping genes for a specific species (and optionally specific conditions)**. +**nf-core/stableexpression** is a bioinformatics pipeline aiming to aggregate multiple count datasets (public / provided by the user) for a specific species and find the most stable genes. + +It takes as main inputs : + * a species name (mandatory) + * keywords for Expression Atlas / GEO search (optional) + * a CSV input file listing your own raw / normalised count datasets (optional). + +**Use cases**: + * **find the most suitable genes as RT-qPCR reference genes for a specific species (and optionally specific conditions)** + * download all Expression Atlas and NCBI GEO datasets (microarray only!) for a species + +

    diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 00000000..ffbc613f --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,32 @@ +# nf-core/stableexpression: Troubleshooting + +## Ǹo dataset found + +>[!IMPORTANT] +> For the time being, only Microarray count datasets are fetched from NCBI GEO. + +For species that are not on Expression Atlas and that do not have microarray data on NCBI data, the pipeline will not be able to find suitable datasets and will log the following message: + +``` +WARN: No dataset found. Please note that for the moment only Microarray count datasets are fetched from NCBI GEO. +You can check at https://www.ncbi.nlm.nih.gov/gds if there are raw RNA-seq count datasets for this species. +``` + +You may want to check if there are any raw RNA-seq count datasets available for this species on [NCBI GEO](https://www.ncbi.nlm.nih.gov/gds). You can then relaunch the pipeline by providing your own count datasets. + +## Java heap space + +In some cases, in particular when running the pipeline on a very large number of datasets (such as for `Homo sapiens`), the Nextflow Java virtual machines can start to request a large amount of memory. You may happen to see the following error: + +``` +java.lang.OutOfMemoryError: Java heap space +``` + +We recommend adding the following line to your environment to limit this (typically in `~/.bashrc` or `~./bash_profile`): +```bash +NXF_OPTS='-Xms1g -Xmx4g' +``` + +or running the pipeline with: +```bash +NXF_OPTS='-Xms1g -Xmx4g' nextflow run nf-core/stableexpression ... diff --git a/docs/usage.md b/docs/usage.md index c14fa1ce..1cd03b3d 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -5,6 +5,9 @@ > [!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). +> [!TIP] +> In case of issues with the pipeline, please check the [troubleshooting page](troubleshooting.md) or [report a new issue](https://github.com/nf-core/stableexpression/issues). + > _Documentation of pipeline parameters is generated automatically from the pipeline schema and can no longer be found in markdown files._ @@ -40,6 +43,7 @@ nextflow run nf-core/stableexpression \ > - Multiple keywords must be separated by commas. > - Note that the keywords are additive: you will get datasets that fit with __either of the keywords__. > - A dataset will be downloaded if a keyword is found in its summary or in the same of a sample. +> - The natural language processing [`ǹltk`](https://www.nltk.org/) python package is used to find keywords as well as derived words. For example, the `leaf` keyword should match 'leaf', 'leaves', 'leafy', etc. ## 3. Provide your own accessions @@ -62,7 +66,7 @@ nextflow run nf-core/stableexpression \ ``` > [!WARNING] -> If you want to download only the datasets corresponding to the accessions supplied, ou must set the `--skip_fetch_eatlas_accessions` and `--skip_fetch_geo_accessions`. +> If you want to download only the datasets corresponding to the accessions supplied, you must set the `--skip_fetch_eatlas_accessions` and `--skip_fetch_geo_accessions`. > [!NOTE] > If you provide accessions through `--eatlas_accessions_file` or `--geo_accessions_file`, there must be one accession per line. The extension of the file does not matter. @@ -85,11 +89,11 @@ Fetched accessions with their respective metadata will be available in ` You can of course provide your own counts datasets / experimental designs. > [!NOTE] -> - To ensure all RNAseq datasets are processed the same way, it is better to provide them raw. -> - In case you want to provide normalise counts, please provide CPMs (counts per million) in order to stay aligned with the way raw datasets are processed in the pipeline. +> - To ensure all RNAseq datasets are processed the same way, you should provide **raw counts**. +> - In case normalised counts are provided, you should provide the same normalisation method for all of them (TPM, FPKM, etc.). > [!WARNING] -> Microarray data must be already normalised. To be compliant with Expression Atlas, you should use the `RMA` methods. +> Microarray data must be already normalised. When mixing your own datasets with public ones in a single run, you should use the `RMA` method to be compliant with Expression Atlas and GEO datasets. First, prepare a samplesheet listing the different count datasets you want to use. Each row represents a specific dataset and must contain: @@ -171,7 +175,7 @@ nextflow run nf-core/stableexpression \ > The `--skip_fetch_eatlas_accessions` and `--skip_fetch_geo_accessions` parameters are supplied here to show how to analyse __only your own dataset__. You may remove these parameters if you want to mix you dataset(s) with public ones. > [!IMPORTANT] -> By default, the pipeline tries to map gene IDs to Ensembl gene IDs. __All genes that cannot be mapped are discarded from the analysis__. This ensures that all genes are named the same between datasets and allows comparing multiple datasets with each other. If you are confident that your genes have the same name between your different datasets or if you think that your gene IDs won't be mapped properly, you can disable this mapping by adding the `--skip_id_mapping` parameter. In such case, it is recommended to supply your own gene id mapping file and gene metadata file with the `--gene_id_mapping` and `--gene_metadata` parameters. See [next section](#5-custom-gene-id-mapping-and-metadata) for further details. +> By default, the pipeline tries to map gene IDs to Ensembl gene IDs. __All genes that cannot be mapped are discarded from the analysis__. This ensures that all genes are named the same between datasets and allows comparing multiple datasets with each other. If you are confident that your genes have the same name between your different datasets or if you think that your gene IDs won't be mapped properly, you can disable this mapping by adding the `--skip_id_mapping` parameter. In such case, you may supply your own gene id mapping file and gene metadata file with the `--gene_id_mapping` and `--gene_metadata` parameters respectively. See [next section](#5-custom-gene-id-mapping-and-metadata) for further details. > [!TIP] > You can check if your gene IDs can be mapped using the [g:Profiler server](https://biit.cs.ut.ee/gprofiler/convert). @@ -230,7 +234,7 @@ OTHERmappedgeneID,My OTHER Gene,Another description ### 6. More advanced scenarios -For advanced scenarios and if you want the entire list of avalable parameters, you can see the [parameter documentation](https://nf-co.re/stableexpression/parameters). +For advanced scenarios, you can see the list of available parameters in the [parameter documentation](https://nf-co.re/stableexpression/parameters). ## Pipeline output From 90d5b871873a31cf32395a40b6da0c0e9aa8e60c Mon Sep 17 00:00:00 2001 From: Olivier Date: Thu, 13 Nov 2025 19:15:33 +0100 Subject: [PATCH 156/258] pass pipeline nf-tests --- ...only.config => test_dataset_eatlas.config} | 5 +- modules/local/dash_app/main.nf | 2 +- nextflow.config | 2 +- tests/default.nf.test | 96 +- tests/default.nf.test.snap | 2717 ++++++----------- .../local/aggregate_results/main.nf.test.snap | 100 + .../local/geo/getaccessions/main.nf.test | 4 +- .../main.nf.test.snap | 26 +- tests/test_data/input_datasets/input_big.yaml | 4 - 9 files changed, 1137 insertions(+), 1819 deletions(-) rename conf/{test_dataset_only.config => test_dataset_eatlas.config} (80%) create mode 100644 tests/modules/local/aggregate_results/main.nf.test.snap delete mode 100644 tests/test_data/input_datasets/input_big.yaml diff --git a/conf/test_dataset_only.config b/conf/test_dataset_eatlas.config similarity index 80% rename from conf/test_dataset_only.config rename to conf/test_dataset_eatlas.config index 35f517a3..e4a0553d 100644 --- a/conf/test_dataset_only.config +++ b/conf/test_dataset_eatlas.config @@ -17,8 +17,9 @@ params { // Input data species = 'mus_musculus' + eatlas_accessions = "E-MTAB-2262" skip_fetch_eatlas_accessions = true skip_fetch_geo_accessions = true - datasets = 'tests/test_data/input_datasets/input_big.yaml' - outdir = "results/test_dataset_only" + datasets = 'https://raw.githubusercontent.com/nf-core/test-datasets/stableexpression/test_data/input_datasets/input_big.yaml' + outdir = "results/test_dataset_eatlas" } diff --git a/modules/local/dash_app/main.nf b/modules/local/dash_app/main.nf index e472352d..34fac912 100644 --- a/modules/local/dash_app/main.nf +++ b/modules/local/dash_app/main.nf @@ -49,7 +49,7 @@ process DASH_APP { # trying to launch the app # if the resulting exit code is not 124 (exit code of timeout) then there is an error - timeout 20 python app.py || exit_code=\$?; [ "\$exit_code" -eq 124 ] && exit 0 || exit 100 + timeout 10 python app.py || exit_code=\$?; [ "\$exit_code" -eq 124 ] && exit 0 || exit 100 """ } diff --git a/nextflow.config b/nextflow.config index eb0a1ff2..83ddf867 100644 --- a/nextflow.config +++ b/nextflow.config @@ -213,7 +213,7 @@ profiles { test { includeConfig 'conf/test.config' } test_full { includeConfig 'conf/test_full.config' } - test_dataset_only { includeConfig 'conf/test_dataset_only.config' } + test_dataset_eatlas { includeConfig 'conf/test_dataset_eatlas.config' } local { includeConfig 'conf/local.config' } } // Load nf-core custom profiles from different institutions diff --git a/tests/default.nf.test b/tests/default.nf.test index 02b29a21..ecf1f039 100644 --- a/tests/default.nf.test +++ b/tests/default.nf.test @@ -6,6 +6,8 @@ nextflow_pipeline { test("-profile test") { + tag "test" + when { params { species = 'beta vulgaris' @@ -33,38 +35,13 @@ nextflow_pipeline { } } - test("-profile test_dataset_only") { - - when { - params { - species = 'mus musculus' - skip_fetch_eatlas_accessions = true - skip_fetch_geo_accessions = true - datasets = 'tests/test_data/input_datasets/input_big.yaml' - outdir = "$outputDir" - } - } - - then { - def stable_name = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}']) - def stable_path = getAllFilesFromDir(params.outdir, ignoreFile: 'tests/.nftignore') - assertAll( - { assert workflow.success}, - { assert snapshot( - removeNextflowVersion("$outputDir/pipeline_info/nf_core_stableexpression_software_mqc_versions.yml"), - stable_name, - stable_path - ).match() } - ) - } - } - - test("-profile test_accessions_only") { + tag "test_accessions_only" + when { params { - species = 'homo_sapiens' + species = 'beta vulgaris' accessions_only = true outdir = "$outputDir" } @@ -91,6 +68,8 @@ nextflow_pipeline { test("-profile test_download_only") { + tag "test_download_only" + when { params { species = 'beta vulgaris' @@ -120,64 +99,14 @@ nextflow_pipeline { test("-profile test_one_accession_low_gene_count") { + tag "test_one_accession_low_gene_count" + when { params { species = 'arabidopsis thaliana' eatlas_accessions = "E-GEOD-51720" skip_fetch_eatlas_accessions = true skip_fetch_geo_accessions = true - outdir = "results/test_one_accession_low_gene_count" - outdir = "$outputDir" - } - } - - then { - def stable_name = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}']) - def stable_path = getAllFilesFromDir(params.outdir, ignoreFile: 'tests/.nftignore') - assertAll( - { assert workflow.success}, - { assert snapshot( - removeNextflowVersion("$outputDir/pipeline_info/nf_core_stableexpression_software_mqc_versions.yml"), - stable_name, - stable_path - ).match() } - ) - } - } - - test("-profile test_local_and_downloaded") { - - when { - params { - species = 'solanum tuberosum' - skip_fetch_eatlas_accessions = true - skip_fetch_geo_accessions = true - eatlas_accessions = "E-MTAB-7711" - datasets = "${projectDir}/tests/test_data/input_datasets/input.csv" - outdir = "$outputDir" - } - } - - then { - def stable_name = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}']) - def stable_path = getAllFilesFromDir(params.outdir, ignoreFile: 'tests/.nftignore') - assertAll( - { assert workflow.success}, - { assert snapshot( - removeNextflowVersion("$outputDir/pipeline_info/nf_core_stableexpression_software_mqc_versions.yml"), - stable_name, - stable_path - ).match() } - ) - } - } - - test("-profile test_run_genorm") { - - when { - params { - species = 'beta vulgaris' - run_genorm = true outdir = "$outputDir" } } @@ -197,6 +126,7 @@ nextflow_pipeline { } test("-profile test_eatlas_only_with_keywords") { + tag "test_eatlas_only_with_keywords" when { params { @@ -221,7 +151,9 @@ nextflow_pipeline { } } + /* test("-profile test_dataset_custom_mapping") { + tag "test_dataset_custom_mapping" when { params { @@ -247,8 +179,10 @@ nextflow_pipeline { ) } } + */ test("-profile test_no_dataset_found") { + tag "test_no_dataset_found" when { params { @@ -277,10 +211,12 @@ nextflow_pipeline { } test("-profile test_full") { + tag "test_full" when { params { species = 'arabidopsis_lyrata' + run_genorm = true outdir = "$outputDir" } } diff --git a/tests/default.nf.test.snap b/tests/default.nf.test.snap index eda17089..1e3bbbc5 100644 --- a/tests/default.nf.test.snap +++ b/tests/default.nf.test.snap @@ -1,5 +1,5 @@ { - "-profile test_eatlas_only_with_keywords": { + "-profile test_one_accession_low_gene_count": { "content": [ null, [ @@ -8,13 +8,12 @@ "aggregate_results/all_genes_summary.csv", "aggregate_results/top_stable_genes_summary.csv", "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", - "base_statistics", - "base_statistics/all", - "base_statistics/all/stats_all_genes.csv", - "base_statistics/rnaseq", - "base_statistics/rnaseq/rnaseq.stats_all_genes.csv", "clean_count_data", "clean_count_data/cleaned_counts_filtered.parquet", + "compute_base_statistics", + "compute_base_statistics/stats_all_genes.csv", + "compute_base_statistics_for_rnaseq", + "compute_base_statistics_for_rnaseq/rnaseq.stats_all_genes.csv", "compute_stability_scores", "compute_stability_scores/stats_with_scores.csv", "dash_app", @@ -68,74 +67,27 @@ "dash_app/src/utils/style.py", "dash_app/versions.yml", "dataset_statistics", - "dataset_statistics/E_GEOD_61690_rnaseq.dataset_stats.csv", - "dataset_statistics/E_GEOD_77826_rnaseq.dataset_stats.csv", - "dataset_statistics/E_MTAB_4251_rnaseq.dataset_stats.csv", - "dataset_statistics/E_MTAB_4301_rnaseq.dataset_stats.csv", - "dataset_statistics/E_MTAB_5038_rnaseq.dataset_stats.csv", - "dataset_statistics/E_MTAB_5215_rnaseq.dataset_stats.csv", - "dataset_statistics/E_MTAB_552_rnaseq.dataset_stats.csv", - "dataset_statistics/E_MTAB_7711_rnaseq.dataset_stats.csv", + "dataset_statistics/E_GEOD_51720_rnaseq.dataset_stats.csv", "errors", "expression_atlas", - "expression_atlas/accessions", - "expression_atlas/accessions/accessions.txt", - "expression_atlas/accessions/selected_experiments.metadata.tsv", - "expression_atlas/accessions/species_experiments.metadata.tsv", "expression_atlas/datasets", - "expression_atlas/datasets/E_GEOD_61690_rnaseq.design.csv", - "expression_atlas/datasets/E_GEOD_61690_rnaseq.rnaseq.raw.counts.csv", - "expression_atlas/datasets/E_GEOD_77826_rnaseq.design.csv", - "expression_atlas/datasets/E_GEOD_77826_rnaseq.rnaseq.raw.counts.csv", - "expression_atlas/datasets/E_MTAB_4251_rnaseq.design.csv", - "expression_atlas/datasets/E_MTAB_4251_rnaseq.rnaseq.raw.counts.csv", - "expression_atlas/datasets/E_MTAB_4301_rnaseq.design.csv", - "expression_atlas/datasets/E_MTAB_4301_rnaseq.rnaseq.raw.counts.csv", - "expression_atlas/datasets/E_MTAB_5038_rnaseq.design.csv", - "expression_atlas/datasets/E_MTAB_5038_rnaseq.rnaseq.raw.counts.csv", - "expression_atlas/datasets/E_MTAB_5215_rnaseq.design.csv", - "expression_atlas/datasets/E_MTAB_5215_rnaseq.rnaseq.raw.counts.csv", - "expression_atlas/datasets/E_MTAB_552_rnaseq.design.csv", - "expression_atlas/datasets/E_MTAB_552_rnaseq.rnaseq.raw.counts.csv", - "expression_atlas/datasets/E_MTAB_7711_rnaseq.design.csv", - "expression_atlas/datasets/E_MTAB_7711_rnaseq.rnaseq.raw.counts.csv", + "expression_atlas/datasets/E_GEOD_51720_rnaseq.design.csv", + "expression_atlas/datasets/E_GEOD_51720_rnaseq.rnaseq.raw.counts.csv", "geo", "geo/excluded_geo_accessions.txt", "get_candidate_genes", "get_candidate_genes/candidate_counts.parquet", "idmapping", - "idmapping/datasets", - "idmapping/datasets/E_GEOD_61690_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/datasets/E_GEOD_61690_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/datasets/E_GEOD_61690_rnaseq.rnaseq.raw.counts.renamed.csv", - "idmapping/datasets/E_GEOD_77826_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/datasets/E_GEOD_77826_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/datasets/E_GEOD_77826_rnaseq.rnaseq.raw.counts.renamed.csv", - "idmapping/datasets/E_MTAB_4251_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/datasets/E_MTAB_4251_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/datasets/E_MTAB_4251_rnaseq.rnaseq.raw.counts.renamed.csv", - "idmapping/datasets/E_MTAB_4301_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/datasets/E_MTAB_4301_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/datasets/E_MTAB_4301_rnaseq.rnaseq.raw.counts.renamed.csv", - "idmapping/datasets/E_MTAB_5038_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/datasets/E_MTAB_5038_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/datasets/E_MTAB_5038_rnaseq.rnaseq.raw.counts.renamed.csv", - "idmapping/datasets/E_MTAB_5215_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/datasets/E_MTAB_5215_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/datasets/E_MTAB_5215_rnaseq.rnaseq.raw.counts.renamed.csv", - "idmapping/datasets/E_MTAB_552_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/datasets/E_MTAB_552_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/datasets/E_MTAB_552_rnaseq.rnaseq.raw.counts.renamed.csv", - "idmapping/datasets/E_MTAB_7711_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/datasets/E_MTAB_7711_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/datasets/E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/E_GEOD_51720_rnaseq.rnaseq.raw.counts.mapping.csv", + "idmapping/E_GEOD_51720_rnaseq.rnaseq.raw.counts.metadata.csv", + "idmapping/E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", + "merge_all_counts", + "merge_all_counts/all_counts.parquet", + "merge_rnaseq_counts", + "merge_rnaseq_counts/all_counts.parquet", "merged_datasets", - "merged_datasets/all", - "merged_datasets/all/all_counts.parquet", - "merged_datasets/rnaseq", - "merged_datasets/rnaseq/all_counts.parquet", "merged_datasets/whole_design.csv", "multiqc", "multiqc/multiqc_data", @@ -144,8 +96,6 @@ "multiqc/multiqc_data/multiqc.parquet", "multiqc/multiqc_data/multiqc_citations.txt", "multiqc/multiqc_data/multiqc_data.json", - "multiqc/multiqc_data/multiqc_eatlas_all_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", "multiqc/multiqc_data/multiqc_gene_statistics.txt", "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", @@ -153,211 +103,194 @@ "multiqc/multiqc_data/multiqc_sources.txt", "multiqc/multiqc_plots", "multiqc/multiqc_plots/pdf", - "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", "multiqc/multiqc_plots/pdf/gene_statistics.pdf", "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", "multiqc/multiqc_plots/png", - "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", - "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", "multiqc/multiqc_plots/png/gene_statistics.png", "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", "multiqc/multiqc_plots/svg", - "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", "multiqc/multiqc_plots/svg/gene_statistics.svg", "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", "normalised", - "normalised/E_GEOD_61690_rnaseq", - "normalised/E_GEOD_61690_rnaseq/normalisation_deseq2", - "normalised/E_GEOD_61690_rnaseq/normalisation_deseq2/E_GEOD_61690_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", - "normalised/E_GEOD_77826_rnaseq", - "normalised/E_GEOD_77826_rnaseq/normalisation_deseq2", - "normalised/E_GEOD_77826_rnaseq/normalisation_deseq2/E_GEOD_77826_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", - "normalised/E_MTAB_4251_rnaseq", - "normalised/E_MTAB_4251_rnaseq/normalisation_deseq2", - "normalised/E_MTAB_4251_rnaseq/normalisation_deseq2/E_MTAB_4251_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", - "normalised/E_MTAB_4301_rnaseq", - "normalised/E_MTAB_4301_rnaseq/normalisation_deseq2", - "normalised/E_MTAB_4301_rnaseq/normalisation_deseq2/E_MTAB_4301_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", - "normalised/E_MTAB_5038_rnaseq", - "normalised/E_MTAB_5038_rnaseq/normalisation_deseq2", - "normalised/E_MTAB_5038_rnaseq/normalisation_deseq2/E_MTAB_5038_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", - "normalised/E_MTAB_5215_rnaseq", - "normalised/E_MTAB_5215_rnaseq/normalisation_deseq2", - "normalised/E_MTAB_5215_rnaseq/normalisation_deseq2/E_MTAB_5215_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", - "normalised/E_MTAB_552_rnaseq", - "normalised/E_MTAB_552_rnaseq/normalisation_deseq2", - "normalised/E_MTAB_552_rnaseq/normalisation_deseq2/E_MTAB_552_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", - "normalised/E_MTAB_7711_rnaseq", - "normalised/E_MTAB_7711_rnaseq/normalisation_deseq2", - "normalised/E_MTAB_7711_rnaseq/normalisation_deseq2/E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normalised/E_GEOD_51720_rnaseq", + "normalised/E_GEOD_51720_rnaseq/normalisation_deseq2", + "normalised/E_GEOD_51720_rnaseq/normalisation_deseq2/E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normfinder", + "normfinder/stability_values.normfinder.csv", "pipeline_info", "pipeline_info/software_mqc_versions.yml", "quantile_normalised", - "quantile_normalised/E_GEOD_61690_rnaseq", - "quantile_normalised/E_GEOD_61690_rnaseq/E_GEOD_61690_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", - "quantile_normalised/E_GEOD_77826_rnaseq", - "quantile_normalised/E_GEOD_77826_rnaseq/E_GEOD_77826_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", - "quantile_normalised/E_MTAB_4251_rnaseq", - "quantile_normalised/E_MTAB_4251_rnaseq/E_MTAB_4251_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", - "quantile_normalised/E_MTAB_4301_rnaseq", - "quantile_normalised/E_MTAB_4301_rnaseq/E_MTAB_4301_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", - "quantile_normalised/E_MTAB_5038_rnaseq", - "quantile_normalised/E_MTAB_5038_rnaseq/E_MTAB_5038_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", - "quantile_normalised/E_MTAB_5215_rnaseq", - "quantile_normalised/E_MTAB_5215_rnaseq/E_MTAB_5215_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", - "quantile_normalised/E_MTAB_552_rnaseq", - "quantile_normalised/E_MTAB_552_rnaseq/E_MTAB_552_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", - "quantile_normalised/E_MTAB_7711_rnaseq", - "quantile_normalised/E_MTAB_7711_rnaseq/E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", - "stability_scoring", - "stability_scoring/normfinder", - "stability_scoring/normfinder/stability_values.normfinder.csv", + "quantile_normalised/E_GEOD_51720_rnaseq", + "quantile_normalised/E_GEOD_51720_rnaseq/E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", "warnings" ], [ - "all_counts_filtered.parquet:md5,0c137b525500950f567679844f65949d", - "all_genes_summary.csv:md5,c63a5b24e8fc4a17d71ca8c2b9d503e8", - "top_stable_genes_summary.csv:md5,20dbabfbf15ccca9ba5ea0f935299d71", - "top_stable_genes_transposed_counts_filtered.csv:md5,005bd9630f3d6ee27ddaf81a1f6d4e69", - "stats_all_genes.csv:md5,6efef4640852629afe0b445016643032", - "rnaseq.stats_all_genes.csv:md5,c94a361119c2356fd5791c2bdd1bb682", - "cleaned_counts_filtered.parquet:md5,ea8d067bc05c52c5a28b3fc0ba8376c2", - "stats_with_scores.csv:md5,e84c1d840dc02efc686c5bad67ee54cd", + "all_counts_filtered.parquet:md5,0eed740e271b9838212fddbd91700198", + "all_genes_summary.csv:md5,b2858858c2db1fc583d2aa91b82fcfb6", + "top_stable_genes_summary.csv:md5,e462975f7e21414420b17e6fc98be70d", + "top_stable_genes_transposed_counts_filtered.csv:md5,34befd9532a2055c22cd82d594b33efd", + "cleaned_counts_filtered.parquet:md5,5cb41caa6a4f0a2bbd2c2bc44c0bd4a7", + "stats_all_genes.csv:md5,28d5526c41e39a26836c862b1d1d96b6", + "rnaseq.stats_all_genes.csv:md5,2c01fc90ce64a89b9c508dc6ef916501", + "stats_with_scores.csv:md5,5ae77ecc6e76e74bde5fc58698816e60", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_counts.parquet:md5,a9964d8e753fc14019a91efc7b7b5d80", - "all_genes_summary.csv:md5,c63a5b24e8fc4a17d71ca8c2b9d503e8", - "whole_design.csv:md5,112168cfd2cc4aad6154fd0aefa7e8fa", + "all_counts.parquet:md5,c2c4a4eea8c2091bd4969d34fd7ff0e1", + "all_genes_summary.csv:md5,b2858858c2db1fc583d2aa91b82fcfb6", + "whole_design.csv:md5,d3aa542c4ad07d0051a84482fe6cd81c", "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", - "common.cpython-313.pyc:md5,958a2fb58c8a1eb49c1f53be5bde113a", - "genes.cpython-313.pyc:md5,1f940d227f2468334516013ce1848f7c", - "samples.cpython-313.pyc:md5,bb3b1848128474bb1e36882760a6498a", + "common.cpython-313.pyc:md5,5679a63ef45c9734d7a97b3e9d725cdf", + "genes.cpython-313.pyc:md5,d4724081f39b670c7456493e9eeaba4e", + "samples.cpython-313.pyc:md5,7f9a6de869a2dcc79d93a812ff4b3c9e", "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", - "graphs.cpython-313.pyc:md5,6e0e3892ca18d05cab7084f480439cda", - "right_sidebar.cpython-313.pyc:md5,20ec462058c57130125f6bbf438ac263", - "stores.cpython-313.pyc:md5,a243e0fa3bb2a1b0abf2ddc868ed233a", - "tables.cpython-313.pyc:md5,7eeb96e1483ef3ba3493d0da4f88ac8b", - "tooltips.cpython-313.pyc:md5,b0cbabb5a65a68425cabe8c138b2f3a7", - "top.cpython-313.pyc:md5,00794b788a7fdccfff4d55d1c2157b76", + "graphs.cpython-313.pyc:md5,1a6ca6f62c9057aa0b53318a4638f292", + "right_sidebar.cpython-313.pyc:md5,741fac5305a328b9f42f9325915a51e6", + "stores.cpython-313.pyc:md5,732530c11da62620189f7f1e66cf3f75", + "tables.cpython-313.pyc:md5,a3007cd8919e0ccfa3a2c15eff11dea5", + "tooltips.cpython-313.pyc:md5,0393a3e8c1a9c17f3537489be39d1dbf", + "top.cpython-313.pyc:md5,adc6059ed209a47a058734447ac35a42", "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", - "genes.cpython-313.pyc:md5,01dbdd80898244a0f08c192ad96fb211", - "samples.cpython-313.pyc:md5,26a2e6920275a87c9fa17f629345266d", + "genes.cpython-313.pyc:md5,93ec2bbc17fcb4d21eec837b9a86f9c7", + "samples.cpython-313.pyc:md5,bf034337885b1172d48aeae1dbc91cfc", "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", "tables.py:md5,84d17ad7f43458412e550f74a24560e5", "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.cpython-313.pyc:md5,1268d2fbbae804fb7e9b310d3dabcbf7", - "data_management.cpython-313.pyc:md5,0f9244fa1a171c4ffaccf810adbb749e", - "style.cpython-313.pyc:md5,b705b5dc7b559296968085945b5e110e", + "config.cpython-313.pyc:md5,9a4884625b79f671334af231867b37af", + "data_management.cpython-313.pyc:md5,d6ce46aaea7c100bf4f89882cdc1b37c", + "style.cpython-313.pyc:md5,b3a815c7ae0123dd33d1a3bfd553a24d", "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", "style.py:md5,b9f1207f06464e43c05e6e3912f12731", "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "E_GEOD_61690_rnaseq.dataset_stats.csv:md5,0f8770070155190203b6f75a3459f909", - "E_GEOD_77826_rnaseq.dataset_stats.csv:md5,e27c4f8df94bcf0078cbd6a2d1e7c62a", - "E_MTAB_4251_rnaseq.dataset_stats.csv:md5,ada6698eef0e01eb826499e3dc8d4f0c", - "E_MTAB_4301_rnaseq.dataset_stats.csv:md5,16b789cc315f71e70214a27be5138429", - "E_MTAB_5038_rnaseq.dataset_stats.csv:md5,2938b1557ef677021c956b8a56c2559c", - "E_MTAB_5215_rnaseq.dataset_stats.csv:md5,aed791a4e0083904594d55411fc2beb6", - "E_MTAB_552_rnaseq.dataset_stats.csv:md5,8d4ad8f52274943b9bfa3563f26cc0d7", - "E_MTAB_7711_rnaseq.dataset_stats.csv:md5,fc513b84eee3cc48466d260b8cfec4ac", - "accessions.txt:md5,dad63ac7a1715277fa44567bc40b5872", - "selected_experiments.metadata.tsv:md5,15baa430176fcadf7bf03034fa9e95c6", - "species_experiments.metadata.tsv:md5,15baa430176fcadf7bf03034fa9e95c6", - "E_GEOD_61690_rnaseq.design.csv:md5,ba807caf74e0b55f5c3fe23810a89560", - "E_GEOD_61690_rnaseq.rnaseq.raw.counts.csv:md5,2e3f1a125b3d41d622e2d24447620eb3", - "E_GEOD_77826_rnaseq.design.csv:md5,5aa61df754aa9c6c107b247c642d2e53", - "E_GEOD_77826_rnaseq.rnaseq.raw.counts.csv:md5,85cea79c602a9924d5a4d6b597ef5530", - "E_MTAB_4251_rnaseq.design.csv:md5,4f3ef7b76ca6ed1ec3157295bc4a8d84", - "E_MTAB_4251_rnaseq.rnaseq.raw.counts.csv:md5,5cf27be0e00b93d5d431754ba8058687", - "E_MTAB_4301_rnaseq.design.csv:md5,165eeef7d612c01fd62baae1a3f296ae", - "E_MTAB_4301_rnaseq.rnaseq.raw.counts.csv:md5,1ab49feea238e7b1419937b5037952b5", - "E_MTAB_5038_rnaseq.design.csv:md5,352ed3163d7deef2be35d899418d5ad4", - "E_MTAB_5038_rnaseq.rnaseq.raw.counts.csv:md5,b4acb3d7c39cdb2bd6cef6c9314c5b2a", - "E_MTAB_5215_rnaseq.design.csv:md5,2741dcd5b45bacce865db632f626a273", - "E_MTAB_5215_rnaseq.rnaseq.raw.counts.csv:md5,273704bdf762c342271b33958a84d1e7", - "E_MTAB_552_rnaseq.design.csv:md5,b81490696f638e90c1cf14236bb0c08c", - "E_MTAB_552_rnaseq.rnaseq.raw.counts.csv:md5,830f50b60b17b62f9ca2f6a163a2879f", - "E_MTAB_7711_rnaseq.design.csv:md5,3e7748b54a0c25c008d9bd2ddbf1bf00", - "E_MTAB_7711_rnaseq.rnaseq.raw.counts.csv:md5,3c02cf432c29d3751c978439539df388", - "excluded_geo_accessions.txt:md5,3b29ba0fe90a301ef71639760fc8e5a9", - "candidate_counts.parquet:md5,93a3bcbeabf1afa3cb0e7fe75cb0000c", - "E_GEOD_61690_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", - "E_GEOD_61690_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", - "E_GEOD_61690_rnaseq.rnaseq.raw.counts.renamed.csv:md5,ccb6efdfb49bc4057618a2a10ec880b7", - "E_GEOD_77826_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", - "E_GEOD_77826_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", - "E_GEOD_77826_rnaseq.rnaseq.raw.counts.renamed.csv:md5,9f933a357deca364f73ca96aa6fa8c5a", - "E_MTAB_4251_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", - "E_MTAB_4251_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", - "E_MTAB_4251_rnaseq.rnaseq.raw.counts.renamed.csv:md5,74aa87fe4102ae828b1bfb0dc7e2bb3a", - "E_MTAB_4301_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", - "E_MTAB_4301_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", - "E_MTAB_4301_rnaseq.rnaseq.raw.counts.renamed.csv:md5,d7a9d4f33e612a803d0032cf1a8b6677", - "E_MTAB_5038_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", - "E_MTAB_5038_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", - "E_MTAB_5038_rnaseq.rnaseq.raw.counts.renamed.csv:md5,07fb1b79dff4a159c9814225dd947d30", - "E_MTAB_5215_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", - "E_MTAB_5215_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", - "E_MTAB_5215_rnaseq.rnaseq.raw.counts.renamed.csv:md5,f3a308689af31ba086fc5f341f0589a4", - "E_MTAB_552_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", - "E_MTAB_552_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", - "E_MTAB_552_rnaseq.rnaseq.raw.counts.renamed.csv:md5,449f7c179fc675784f15f58735196644", - "E_MTAB_7711_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", - "E_MTAB_7711_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", - "E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.csv:md5,3856796c675c7ee3bb6975185f1b869b", - "whole_gene_id_mapping.csv:md5,87c58803a087a768eff2403b40868614", - "whole_gene_metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", - "all_counts.parquet:md5,a9964d8e753fc14019a91efc7b7b5d80", - "all_counts.parquet:md5,1d9b59f7d379961c4939a1bdf6b0deca", - "whole_design.csv:md5,112168cfd2cc4aad6154fd0aefa7e8fa", + "E_GEOD_51720_rnaseq.dataset_stats.csv:md5,428c31aba1b6ba8af014d7a80e21bd97", + "E_GEOD_51720_rnaseq.design.csv:md5,80805afb29837b6fbb73a6aa6f3a461b", + "E_GEOD_51720_rnaseq.rnaseq.raw.counts.csv:md5,07cd448196fc2fea4663bd9705da2b98", + "excluded_geo_accessions.txt:md5,cdbec776e8b1d9dc7d0aa44aaf52aa50", + "candidate_counts.parquet:md5,1d25a87d5e815b78c0cf2aa018130319", + "E_GEOD_51720_rnaseq.rnaseq.raw.counts.mapping.csv:md5,1bbc8cddf18072309e81498806aa73ff", + "E_GEOD_51720_rnaseq.rnaseq.raw.counts.metadata.csv:md5,08861c29159a6a2fed38efe523ed9c56", + "E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.csv:md5,ed0d6193d4f39e5e4000f1ddbee521bf", + "whole_gene_id_mapping.csv:md5,1bbc8cddf18072309e81498806aa73ff", + "whole_gene_metadata.csv:md5,a08ab44a98542a8529d2a4b5a47e4408", + "all_counts.parquet:md5,c2c4a4eea8c2091bd4969d34fd7ff0e1", + "all_counts.parquet:md5,c2c4a4eea8c2091bd4969d34fd7ff0e1", + "whole_design.csv:md5,d3aa542c4ad07d0051a84482fe6cd81c", "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", - "multiqc_eatlas_all_experiments_metadata.txt:md5,a2b6cf46faf4b8c7dcdae06e17b35432", - "multiqc_eatlas_selected_experiments_metadata.txt:md5,a2b6cf46faf4b8c7dcdae06e17b35432", - "multiqc_expression_distributions_top_stable_genes.txt:md5,4e75993c1e1af48e9cfcb036e4aa517e", - "multiqc_gene_statistics.txt:md5,74568ad71d674619779988bdf43b4ec7", - "multiqc_ranked_top_stable_genes_summary.txt:md5,131c3632500353311f9508dd893ad681", + "multiqc_expression_distributions_top_stable_genes.txt:md5,66a44969a7b827943a31406bb6e1eca7", + "multiqc_gene_statistics.txt:md5,2a7dfaf41ff7ea555808d01be43616fb", + "multiqc_ranked_top_stable_genes_summary.txt:md5,f00ef628aa055283744c1653fcb9e5b7", "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030", - "E_GEOD_61690_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,4bc05ef9eff93062591fc37ae93b830d", - "E_GEOD_77826_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,0e4f0b0a0276f3a835d26d4b518296f2", - "E_MTAB_4251_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,c9d0a14d809fe994abb1280ee7c00612", - "E_MTAB_4301_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,cdd0db06fdcfb4261853a63b5527c4a1", - "E_MTAB_5038_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,7f72ebf195c0dfb26d13c09157ea1914", - "E_MTAB_5215_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,e427282767b3833f899b1a03bc40edc2", - "E_MTAB_552_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,26e7b283f3c901cc417c1fe0e6b9d555", - "E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,f7374c86950c01bc6a6b8d8fd415cc8b", - "E_GEOD_61690_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,31eef9ce4f886a9368a6d9b0d9d8ac24", - "E_GEOD_77826_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,8270dee3af5c4704ece1bbe2e446d7a2", - "E_MTAB_4251_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,c064b8e2023ef043a0d7d210701c7a88", - "E_MTAB_4301_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,63dc1f0932ef000446ed2e9d553fd0d1", - "E_MTAB_5038_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,bd68b4e0e6120301aedd3885210407cd", - "E_MTAB_5215_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,e4f7631cde43c1b876b0f08a8fe0e3c1", - "E_MTAB_552_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,f5432d1b7b1185b08ee899bd620d73f8", - "E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,ac4c45230db9a59247c51063db68eef3", - "stability_values.normfinder.csv:md5,d2cec0958cad90bfaa30795ea3e0d5cc" + "E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,7b76036297abbe80891c05f2e0af9647", + "stability_values.normfinder.csv:md5,c32298f5ba2ab39ea954925fe86d2d64", + "E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,3a76d77541b2fe103ed56b7e7b54de8f" ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T17:05:13.978857392" + "timestamp": "2025-11-13T11:24:15.307429751" }, - "-profile test": { + "-profile test_download_only": { + "content": [ + null, + [ + "errors", + "errors/geo_failure_reasons.csv", + "expression_atlas", + "expression_atlas/accessions", + "expression_atlas/accessions/accessions.txt", + "expression_atlas/accessions/selected_experiments.metadata.tsv", + "expression_atlas/accessions/species_experiments.metadata.tsv", + "expression_atlas/datasets", + "expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", + "expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", + "geo", + "geo/accessions", + "geo/accessions/accessions.txt", + "geo/accessions/geo_all_datasets.metadata.tsv", + "geo/accessions/geo_selected_datasets.metadata.tsv", + "geo/datasets", + "geo/datasets/failure_reason.txt", + "multiqc", + "multiqc/multiqc_data", + "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_eatlas_all_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_failure_reasons.txt", + "multiqc/multiqc_data/multiqc_geo_selected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_software_versions.txt", + "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_plots", + "multiqc/multiqc_plots/pdf", + "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_failure_reasons.pdf", + "multiqc/multiqc_plots/pdf/geo_selected_experiments_metadata.pdf", + "multiqc/multiqc_plots/png", + "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", + "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_failure_reasons.png", + "multiqc/multiqc_plots/png/geo_selected_experiments_metadata.png", + "multiqc/multiqc_plots/svg", + "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_failure_reasons.svg", + "multiqc/multiqc_plots/svg/geo_selected_experiments_metadata.svg", + "multiqc/multiqc_report.html", + "multiqc/versions.yml", + "pipeline_info", + "pipeline_info/software_mqc_versions.yml", + "warnings" + ], + [ + "geo_failure_reasons.csv:md5,17976bf17f7f0a5f0deca2cbaf28fac6", + "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", + "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", + "accessions.txt:md5,530c21fd138405dc6bcdd275633c07ef", + "geo_all_datasets.metadata.tsv:md5,b810eae9a69f6a1b1e1db9444fa593fa", + "geo_selected_datasets.metadata.tsv:md5,b810eae9a69f6a1b1e1db9444fa593fa", + "failure_reason.txt:md5,2631bb8c3ae982a1b869107f8dbfa107", + "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", + "multiqc_eatlas_all_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", + "multiqc_eatlas_selected_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", + "multiqc_geo_all_experiments_metadata.txt:md5,faf1630bcea828f358a5dbe934a0d7b6", + "multiqc_geo_failure_reasons.txt:md5,8eb73de29967d7d43b28d7edf167dcac", + "multiqc_geo_selected_experiments_metadata.txt:md5,faf1630bcea828f358a5dbe934a0d7b6", + "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-13T11:22:05.641306836" + }, + "-profile test_eatlas_only_with_keywords": { "content": [ null, [ @@ -366,13 +299,12 @@ "aggregate_results/all_genes_summary.csv", "aggregate_results/top_stable_genes_summary.csv", "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", - "base_statistics", - "base_statistics/all", - "base_statistics/all/stats_all_genes.csv", - "base_statistics/rnaseq", - "base_statistics/rnaseq/rnaseq.stats_all_genes.csv", "clean_count_data", "clean_count_data/cleaned_counts_filtered.parquet", + "compute_base_statistics", + "compute_base_statistics/stats_all_genes.csv", + "compute_base_statistics_for_rnaseq", + "compute_base_statistics_for_rnaseq/rnaseq.stats_all_genes.csv", "compute_stability_scores", "compute_stability_scores/stats_with_scores.csv", "dash_app", @@ -437,23 +369,19 @@ "expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", "expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", "geo", - "geo/accessions", - "geo/accessions/geo_all_datasets.metadata.tsv", - "geo/accessions/geo_rejected_datasets.metadata.tsv", "get_candidate_genes", "get_candidate_genes/candidate_counts.parquet", "idmapping", - "idmapping/datasets", - "idmapping/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/E_MTAB_8187_rnaseq.rnaseq.raw.counts.mapping.csv", + "idmapping/E_MTAB_8187_rnaseq.rnaseq.raw.counts.metadata.csv", + "idmapping/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", + "merge_all_counts", + "merge_all_counts/all_counts.parquet", + "merge_rnaseq_counts", + "merge_rnaseq_counts/all_counts.parquet", "merged_datasets", - "merged_datasets/all", - "merged_datasets/all/all_counts.parquet", - "merged_datasets/rnaseq", - "merged_datasets/rnaseq/all_counts.parquet", "merged_datasets/whole_design.csv", "multiqc", "multiqc/multiqc_data", @@ -466,8 +394,6 @@ "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", "multiqc/multiqc_data/multiqc_gene_statistics.txt", - "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_rejected_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", @@ -477,24 +403,18 @@ "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", "multiqc/multiqc_plots/pdf/gene_statistics.pdf", - "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_rejected_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", "multiqc/multiqc_plots/png", "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", "multiqc/multiqc_plots/png/gene_statistics.png", - "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_rejected_experiments_metadata.png", "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", "multiqc/multiqc_plots/svg", "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", "multiqc/multiqc_plots/svg/gene_statistics.svg", - "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_rejected_experiments_metadata.svg", "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", @@ -502,178 +422,95 @@ "normalised/E_MTAB_8187_rnaseq", "normalised/E_MTAB_8187_rnaseq/normalisation_deseq2", "normalised/E_MTAB_8187_rnaseq/normalisation_deseq2/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normfinder", + "normfinder/stability_values.normfinder.csv", "pipeline_info", "pipeline_info/software_mqc_versions.yml", "quantile_normalised", "quantile_normalised/E_MTAB_8187_rnaseq", "quantile_normalised/E_MTAB_8187_rnaseq/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", - "stability_scoring", - "stability_scoring/normfinder", - "stability_scoring/normfinder/stability_values.normfinder.csv", "warnings" ], [ - "all_counts_filtered.parquet:md5,d66efb71c32e755405c5be7a5a31f66b", - "all_genes_summary.csv:md5,09ce07d606691ea26f5874ff1151829d", - "top_stable_genes_summary.csv:md5,8aac1b65ece21128ee9fbf621ccffbb2", - "top_stable_genes_transposed_counts_filtered.csv:md5,4f6efc781edc2022d5d0aac665f8a553", - "stats_all_genes.csv:md5,9e3ff278dab74d6903da2f0963332626", - "rnaseq.stats_all_genes.csv:md5,13da83b1a202966be37b5d46e1a73acd", - "cleaned_counts_filtered.parquet:md5,2bb5f895eeba97d72a079987ff707434", - "stats_with_scores.csv:md5,a3475ec54e6bc54525be0274d31ac215", + "all_counts_filtered.parquet:md5,57325f7ce7f6571616b4e7f2a534c040", + "all_genes_summary.csv:md5,c382ff75abdd1b206e047edb83699708", + "top_stable_genes_summary.csv:md5,7b0892950a087f3853980486a01508b9", + "top_stable_genes_transposed_counts_filtered.csv:md5,d76c1c041caa7e4903357a88e51e72b9", + "cleaned_counts_filtered.parquet:md5,cb769635217b0b8a17d3b220a446d283", + "stats_all_genes.csv:md5,7dfe60333595d41b2e4da1e62ced9e17", + "rnaseq.stats_all_genes.csv:md5,2a2704fd5f9b2a4d60467903d044638a", + "stats_with_scores.csv:md5,06b2973f3633a6dec92f709793f7b35d", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_counts.parquet:md5,54e3036081c965bbf62134589c8ab233", - "all_genes_summary.csv:md5,09ce07d606691ea26f5874ff1151829d", + "all_counts.parquet:md5,4b678f7f93296b65e35d80ce3fcdcd15", + "all_genes_summary.csv:md5,c382ff75abdd1b206e047edb83699708", "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", - "common.cpython-313.pyc:md5,49a00af9f5e82077e5e9adbbb76302a3", - "genes.cpython-313.pyc:md5,03ebd17927065dc8529af62a12777415", - "samples.cpython-313.pyc:md5,bc843403cf03611f3f9f17d493cc9ab7", + "common.cpython-313.pyc:md5,37cbf2a5933ace1aa714e6c49682459b", + "genes.cpython-313.pyc:md5,668854f409530bac16d2d5bf4dc781c9", + "samples.cpython-313.pyc:md5,ae5890963055f92e70b4995397080273", "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", - "graphs.cpython-313.pyc:md5,f660df258b1b3d3e75a3d470387a322a", - "right_sidebar.cpython-313.pyc:md5,8c2e303516678c1697599797add55b8f", - "stores.cpython-313.pyc:md5,7aba77cce4937589d40329006134b3f9", - "tables.cpython-313.pyc:md5,1bb516368922912fd118d02d609dd33f", - "tooltips.cpython-313.pyc:md5,c2c233eb5535a39b6646ffafcedd2569", - "top.cpython-313.pyc:md5,47075a146bfbb405377c75f0663f5dc7", + "graphs.cpython-313.pyc:md5,4d0a925cf3fb3f100d40e628302563a5", + "right_sidebar.cpython-313.pyc:md5,3558e524493a80dbbeb4159e4440acf4", + "stores.cpython-313.pyc:md5,0969dd13170cc9864913a33b07eabac7", + "tables.cpython-313.pyc:md5,5397d489be50816b7c943fb22dd178ec", + "tooltips.cpython-313.pyc:md5,7fa81ed404b4707a122f63ec59cf3f5c", + "top.cpython-313.pyc:md5,dc919308bd181abb3268b25a41de18fe", "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", - "genes.cpython-313.pyc:md5,51c18544b1887ba5bdfdff8cf8f365cd", - "samples.cpython-313.pyc:md5,244c53655495c130f7ff37c2b795c13f", + "genes.cpython-313.pyc:md5,30f74fb5a6c03914481e3c18614aadb6", + "samples.cpython-313.pyc:md5,14c18df8caff8c7715f8e7eb759cd359", "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", "tables.py:md5,84d17ad7f43458412e550f74a24560e5", "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.cpython-313.pyc:md5,0d26637d4326ec2e5e7a0314ae13b8d5", - "data_management.cpython-313.pyc:md5,4333c9f3294879374fbccfc575a94f7d", - "style.cpython-313.pyc:md5,861432da7e3a71fae86de3f200965048", + "config.cpython-313.pyc:md5,a5f1a30f13a693977de20b1946038832", + "data_management.cpython-313.pyc:md5,5fef556524e3dc0dbb2b76e8ca9c5776", + "style.cpython-313.pyc:md5,8bc8df7c03346e65470a217d6989dafb", "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", "style.py:md5,b9f1207f06464e43c05e6e3912f12731", "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "E_MTAB_8187_rnaseq.dataset_stats.csv:md5,efad119f2f4b3d777a05f0bfe2070411", + "E_MTAB_8187_rnaseq.dataset_stats.csv:md5,2f8f45b238e4bfa187765b72b0215447", "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", - "geo_all_datasets.metadata.tsv:md5,f561d3c842808e002a4d8809c1ed848f", - "geo_rejected_datasets.metadata.tsv:md5,9fca49d7ebe56660430411a09a3b50e0", - "candidate_counts.parquet:md5,25cfcc61693a546f5576695d01767a06", + "candidate_counts.parquet:md5,03846e729c0b88802ff3e514bdab0f2f", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.mapping.csv:md5,d95581c2b10beed3ba4a4556c1afc44f", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.metadata.csv:md5,faf9789296e5578fd31efee8d0c40ca9", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv:md5,f8889bf65500b67c37b7b7549df57285", "whole_gene_id_mapping.csv:md5,d95581c2b10beed3ba4a4556c1afc44f", "whole_gene_metadata.csv:md5,faf9789296e5578fd31efee8d0c40ca9", - "all_counts.parquet:md5,54e3036081c965bbf62134589c8ab233", - "all_counts.parquet:md5,54e3036081c965bbf62134589c8ab233", + "all_counts.parquet:md5,4b678f7f93296b65e35d80ce3fcdcd15", + "all_counts.parquet:md5,4b678f7f93296b65e35d80ce3fcdcd15", "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", "multiqc_eatlas_all_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", "multiqc_eatlas_selected_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", - "multiqc_expression_distributions_top_stable_genes.txt:md5,443d0c694839b2948f7a1757fab5593e", - "multiqc_gene_statistics.txt:md5,26d99a1e06b450e651677cb22ec41ee0", - "multiqc_geo_all_experiments_metadata.txt:md5,e17accfafbd2fdfcb2710b23ef7e55cf", - "multiqc_geo_rejected_experiments_metadata.txt:md5,633627930268074fa7d81fc8efa7f4d6", - "multiqc_ranked_top_stable_genes_summary.txt:md5,9b5950b2ffdb53b8aee4bf81ec37344e", + "multiqc_expression_distributions_top_stable_genes.txt:md5,8de2cb4a0136a973122e3cb4fab52b23", + "multiqc_gene_statistics.txt:md5,143120a0f58cf66cbb444039b3b1b4fc", + "multiqc_ranked_top_stable_genes_summary.txt:md5,d4ef7039f60f434c109e95240499c049", "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,285ad46e7f5da70041815ecbcd9b5601", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,f03095b91c3e2a9c5e39bba1ad2b3a45", - "stability_values.normfinder.csv:md5,4996024e5b9f99e7a755aa17e9a20ca8" - ] - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-08T16:53:41.161206065" - }, - "-profile test_accessions_only": { - "content": [ - null, - [ - "errors", - "expression_atlas", - "expression_atlas/accessions", - "expression_atlas/accessions/accessions.txt", - "expression_atlas/accessions/selected_experiments.metadata.tsv", - "expression_atlas/accessions/species_experiments.metadata.tsv", - "geo", - "geo/accessions", - "geo/accessions/accessions.tsv", - "geo/accessions/geo_all_datasets.metadata.tsv", - "geo/accessions/geo_rejected_datasets.metadata.tsv", - "geo/accessions/geo_selected_datasets.metadata.tsv", - "multiqc", - "multiqc/multiqc_data", - "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_eatlas_all_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_rejected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_selected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_software_versions.txt", - "multiqc/multiqc_data/multiqc_sources.txt", - "multiqc/multiqc_plots", - "multiqc/multiqc_plots/pdf", - "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_rejected_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_selected_experiments_metadata.pdf", - "multiqc/multiqc_plots/png", - "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", - "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_rejected_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_selected_experiments_metadata.png", - "multiqc/multiqc_plots/svg", - "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_rejected_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_selected_experiments_metadata.svg", - "multiqc/multiqc_report.html", - "multiqc/versions.yml", - "pipeline_info", - "pipeline_info/software_mqc_versions.yml", - "warnings" - ], - [ - "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", - "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", - "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", - "accessions.tsv:md5,095fb7f3a666b4382d4ba7296053451b", - "geo_all_datasets.metadata.tsv:md5,d77cabbca17e0502e562e2f85632cd26", - "geo_rejected_datasets.metadata.tsv:md5,cf907a70f381df40e044186db1a320a2", - "geo_selected_datasets.metadata.tsv:md5,d77cabbca17e0502e562e2f85632cd26", - "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", - "multiqc_eatlas_all_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", - "multiqc_eatlas_selected_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", - "multiqc_geo_all_experiments_metadata.txt:md5,67a00882973fd2702e4ea1fef3e84c08", - "multiqc_geo_rejected_experiments_metadata.txt:md5,ea6234d7e4c463fc249aebdf91788df8", - "multiqc_geo_selected_experiments_metadata.txt:md5,67a00882973fd2702e4ea1fef3e84c08", - "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030" + "stability_values.normfinder.csv:md5,0fa8281f140b1c2e727d6b34c2f94245", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,e815eb3150d1c97e890142fadd16219e" ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T16:54:05.938565374" + "timestamp": "2025-11-13T11:26:48.449442025" }, - "-profile test_one_accession_low_gene_count": { + "-profile test": { "content": [ null, [ @@ -682,13 +519,12 @@ "aggregate_results/all_genes_summary.csv", "aggregate_results/top_stable_genes_summary.csv", "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", - "base_statistics", - "base_statistics/all", - "base_statistics/all/stats_all_genes.csv", - "base_statistics/rnaseq", - "base_statistics/rnaseq/rnaseq.stats_all_genes.csv", "clean_count_data", "clean_count_data/cleaned_counts_filtered.parquet", + "compute_base_statistics", + "compute_base_statistics/stats_all_genes.csv", + "compute_base_statistics_for_rnaseq", + "compute_base_statistics_for_rnaseq/rnaseq.stats_all_genes.csv", "compute_stability_scores", "compute_stability_scores/stats_with_scores.csv", "dash_app", @@ -742,28 +578,34 @@ "dash_app/src/utils/style.py", "dash_app/versions.yml", "dataset_statistics", - "dataset_statistics/E_GEOD_51720_rnaseq.dataset_stats.csv", + "dataset_statistics/E_MTAB_8187_rnaseq.dataset_stats.csv", "errors", "expression_atlas", + "expression_atlas/accessions", + "expression_atlas/accessions/accessions.txt", + "expression_atlas/accessions/selected_experiments.metadata.tsv", + "expression_atlas/accessions/species_experiments.metadata.tsv", "expression_atlas/datasets", - "expression_atlas/datasets/E_GEOD_51720_rnaseq.design.csv", - "expression_atlas/datasets/E_GEOD_51720_rnaseq.rnaseq.raw.counts.csv", + "expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", + "expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", "geo", - "geo/excluded_geo_accessions.txt", + "geo/accessions", + "geo/accessions/accessions.txt", + "geo/accessions/geo_all_datasets.metadata.tsv", + "geo/accessions/geo_rejected_datasets.metadata.tsv", "get_candidate_genes", "get_candidate_genes/candidate_counts.parquet", "idmapping", - "idmapping/datasets", - "idmapping/datasets/E_GEOD_51720_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/datasets/E_GEOD_51720_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/datasets/E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/E_MTAB_8187_rnaseq.rnaseq.raw.counts.mapping.csv", + "idmapping/E_MTAB_8187_rnaseq.rnaseq.raw.counts.metadata.csv", + "idmapping/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", + "merge_all_counts", + "merge_all_counts/all_counts.parquet", + "merge_rnaseq_counts", + "merge_rnaseq_counts/all_counts.parquet", "merged_datasets", - "merged_datasets/all", - "merged_datasets/all/all_counts.parquet", - "merged_datasets/rnaseq", - "merged_datasets/rnaseq/all_counts.parquet", "merged_datasets/whole_design.csv", "multiqc", "multiqc/multiqc_data", @@ -772,116 +614,254 @@ "multiqc/multiqc_data/multiqc.parquet", "multiqc/multiqc_data/multiqc_citations.txt", "multiqc/multiqc_data/multiqc_data.json", + "multiqc/multiqc_data/multiqc_eatlas_all_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", "multiqc/multiqc_data/multiqc_gene_statistics.txt", + "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_rejected_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", "multiqc/multiqc_plots", "multiqc/multiqc_plots/pdf", + "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", "multiqc/multiqc_plots/pdf/gene_statistics.pdf", + "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_rejected_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", "multiqc/multiqc_plots/png", + "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", + "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", "multiqc/multiqc_plots/png/gene_statistics.png", + "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_rejected_experiments_metadata.png", "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", "multiqc/multiqc_plots/svg", + "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", "multiqc/multiqc_plots/svg/gene_statistics.svg", + "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_rejected_experiments_metadata.svg", "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", "normalised", - "normalised/E_GEOD_51720_rnaseq", - "normalised/E_GEOD_51720_rnaseq/normalisation_deseq2", - "normalised/E_GEOD_51720_rnaseq/normalisation_deseq2/E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normalised/E_MTAB_8187_rnaseq", + "normalised/E_MTAB_8187_rnaseq/normalisation_deseq2", + "normalised/E_MTAB_8187_rnaseq/normalisation_deseq2/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normfinder", + "normfinder/stability_values.normfinder.csv", "pipeline_info", "pipeline_info/software_mqc_versions.yml", "quantile_normalised", - "quantile_normalised/E_GEOD_51720_rnaseq", - "quantile_normalised/E_GEOD_51720_rnaseq/E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", - "stability_scoring", - "stability_scoring/normfinder", - "stability_scoring/normfinder/stability_values.normfinder.csv", + "quantile_normalised/E_MTAB_8187_rnaseq", + "quantile_normalised/E_MTAB_8187_rnaseq/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", "warnings" ], [ - "all_counts_filtered.parquet:md5,0eed740e271b9838212fddbd91700198", - "all_genes_summary.csv:md5,b2858858c2db1fc583d2aa91b82fcfb6", - "top_stable_genes_summary.csv:md5,e462975f7e21414420b17e6fc98be70d", - "top_stable_genes_transposed_counts_filtered.csv:md5,34befd9532a2055c22cd82d594b33efd", - "stats_all_genes.csv:md5,28d5526c41e39a26836c862b1d1d96b6", - "rnaseq.stats_all_genes.csv:md5,2c01fc90ce64a89b9c508dc6ef916501", - "cleaned_counts_filtered.parquet:md5,5cb41caa6a4f0a2bbd2c2bc44c0bd4a7", - "stats_with_scores.csv:md5,5ae77ecc6e76e74bde5fc58698816e60", + "all_counts_filtered.parquet:md5,a8813b9cea1043b0cce3f7cfafba6d68", + "all_genes_summary.csv:md5,f0b57b4b71c51e81496892aa8cca7318", + "top_stable_genes_summary.csv:md5,bffda029a93b8471e6a1e5649c24bd74", + "top_stable_genes_transposed_counts_filtered.csv:md5,a85d810bb7e488b13216d1d1a70a2eff", + "cleaned_counts_filtered.parquet:md5,da5555270311efc0959d78c76de0bd4c", + "stats_all_genes.csv:md5,69ab95beb2e773179757dda3cc853e49", + "rnaseq.stats_all_genes.csv:md5,93e06aabacf03cbdce5bff6ef86e308e", + "stats_with_scores.csv:md5,b0d81e8d7a62b37296267a05677fb9b3", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_counts.parquet:md5,c2c4a4eea8c2091bd4969d34fd7ff0e1", - "all_genes_summary.csv:md5,b2858858c2db1fc583d2aa91b82fcfb6", - "whole_design.csv:md5,d3aa542c4ad07d0051a84482fe6cd81c", + "all_counts.parquet:md5,5e78e46c71ffd1be40f69d1bb7c02185", + "all_genes_summary.csv:md5,f0b57b4b71c51e81496892aa8cca7318", + "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", - "common.cpython-313.pyc:md5,31ba90c5decc28d3c8db90fc80e69802", - "genes.cpython-313.pyc:md5,701e8f349cf8468b099ca838b1bcbb22", - "samples.cpython-313.pyc:md5,92d9f02865116de32ba5833afe509160", + "common.cpython-313.pyc:md5,5e5ccd935515ba3c0f90079f24bc5d53", + "genes.cpython-313.pyc:md5,18a991b02ba16b4be45e1125b1d991b6", + "samples.cpython-313.pyc:md5,824af1d55406e148cb4ee2957681d8a5", "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", - "graphs.cpython-313.pyc:md5,f594bc74529100a0e3bab48f08ee88e6", - "right_sidebar.cpython-313.pyc:md5,4d9f40f324acef2ff3a1c8d532936ea8", - "stores.cpython-313.pyc:md5,787f503dbdccdd8899ec51c56c9f3eaf", - "tables.cpython-313.pyc:md5,d96e0875cccaa47043e1a230479bfdf8", - "tooltips.cpython-313.pyc:md5,fde0d1296c024c75a78dcb9a1c0d2f41", - "top.cpython-313.pyc:md5,246b2053230d2973c86f3a51a0c0280e", + "graphs.cpython-313.pyc:md5,c8b4d9bee8ca889b9ac85fccc7d0d613", + "right_sidebar.cpython-313.pyc:md5,a33f4fcdf93b205813c7a65276e712f6", + "stores.cpython-313.pyc:md5,dc49fe39a05272df7a1d05b4d740c1cf", + "tables.cpython-313.pyc:md5,866b8529dd92c2386d584f702a03bb6e", + "tooltips.cpython-313.pyc:md5,839576f99f0094b731e9f4d65b80077c", + "top.cpython-313.pyc:md5,084cbfbd74000494104b7c723e3621c4", "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", - "genes.cpython-313.pyc:md5,8c67ccad315fde815aacf702177d7e84", - "samples.cpython-313.pyc:md5,d55185e3ef9dfc2f18e44521b05f8275", + "genes.cpython-313.pyc:md5,341ba19409ceea3adaf85d079f6d6055", + "samples.cpython-313.pyc:md5,f2cfc03f08dbf3338d7b5db448d417c1", "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", "tables.py:md5,84d17ad7f43458412e550f74a24560e5", "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.cpython-313.pyc:md5,f2017f0ac6b76bca39f44ec186dd5d79", - "data_management.cpython-313.pyc:md5,1aacb3c94a7821aa50fdef062f87090e", - "style.cpython-313.pyc:md5,dac4d44bc702b378bc5a5119ffc9ffb6", + "config.cpython-313.pyc:md5,0bf517c900fc7f22b8b294f93bc22d50", + "data_management.cpython-313.pyc:md5,16bd285c10baaa3a8b63bfed24cf7af5", + "style.cpython-313.pyc:md5,54800903072fb7f9881e47edab7315b2", "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", "style.py:md5,b9f1207f06464e43c05e6e3912f12731", "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "E_GEOD_51720_rnaseq.dataset_stats.csv:md5,428c31aba1b6ba8af014d7a80e21bd97", - "E_GEOD_51720_rnaseq.design.csv:md5,80805afb29837b6fbb73a6aa6f3a461b", - "E_GEOD_51720_rnaseq.rnaseq.raw.counts.csv:md5,07cd448196fc2fea4663bd9705da2b98", - "excluded_geo_accessions.txt:md5,cdbec776e8b1d9dc7d0aa44aaf52aa50", - "candidate_counts.parquet:md5,1d25a87d5e815b78c0cf2aa018130319", - "E_GEOD_51720_rnaseq.rnaseq.raw.counts.mapping.csv:md5,1bbc8cddf18072309e81498806aa73ff", - "E_GEOD_51720_rnaseq.rnaseq.raw.counts.metadata.csv:md5,08861c29159a6a2fed38efe523ed9c56", - "E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.csv:md5,ed0d6193d4f39e5e4000f1ddbee521bf", - "whole_gene_id_mapping.csv:md5,1bbc8cddf18072309e81498806aa73ff", - "whole_gene_metadata.csv:md5,a08ab44a98542a8529d2a4b5a47e4408", - "all_counts.parquet:md5,c2c4a4eea8c2091bd4969d34fd7ff0e1", - "all_counts.parquet:md5,c2c4a4eea8c2091bd4969d34fd7ff0e1", - "whole_design.csv:md5,d3aa542c4ad07d0051a84482fe6cd81c", + "E_MTAB_8187_rnaseq.dataset_stats.csv:md5,544cd73d8d9d34f668b3f5891853ffb2", + "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", + "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", + "accessions.txt:md5,d41d8cd98f00b204e9800998ecf8427e", + "geo_all_datasets.metadata.tsv:md5,773c2ef1b8f15c6a4624a54c89edca54", + "geo_rejected_datasets.metadata.tsv:md5,7e3bbb01196cb034ab2a201083e20e0b", + "candidate_counts.parquet:md5,a70d38ba1fd86533c01e79a0a37a56a4", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.mapping.csv:md5,d95581c2b10beed3ba4a4556c1afc44f", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.metadata.csv:md5,faf9789296e5578fd31efee8d0c40ca9", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv:md5,f8889bf65500b67c37b7b7549df57285", + "whole_gene_id_mapping.csv:md5,d95581c2b10beed3ba4a4556c1afc44f", + "whole_gene_metadata.csv:md5,faf9789296e5578fd31efee8d0c40ca9", + "all_counts.parquet:md5,5e78e46c71ffd1be40f69d1bb7c02185", + "all_counts.parquet:md5,5e78e46c71ffd1be40f69d1bb7c02185", + "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", - "multiqc_expression_distributions_top_stable_genes.txt:md5,66a44969a7b827943a31406bb6e1eca7", - "multiqc_gene_statistics.txt:md5,2a7dfaf41ff7ea555808d01be43616fb", - "multiqc_ranked_top_stable_genes_summary.txt:md5,f00ef628aa055283744c1653fcb9e5b7", + "multiqc_eatlas_all_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", + "multiqc_eatlas_selected_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", + "multiqc_expression_distributions_top_stable_genes.txt:md5,98dea2ab83b991a464dac4d6646ed9dc", + "multiqc_gene_statistics.txt:md5,5549573adfaa235f23b6246bcf28a15c", + "multiqc_geo_all_experiments_metadata.txt:md5,69462c654446efee39fa72978065fb53", + "multiqc_geo_rejected_experiments_metadata.txt:md5,320da3c2a8ae094956e30d56c4312bea", + "multiqc_ranked_top_stable_genes_summary.txt:md5,710a41b54b281337c2a87627a6c511a8", "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030", - "E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,7b76036297abbe80891c05f2e0af9647", - "E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,3a76d77541b2fe103ed56b7e7b54de8f", - "stability_values.normfinder.csv:md5,c32298f5ba2ab39ea954925fe86d2d64" + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,285ad46e7f5da70041815ecbcd9b5601", + "stability_values.normfinder.csv:md5,a256a7f4aaa9ad77456e1e7a6ee65f0c", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,85cfb9f6a28841e4299c57dcb3b78960" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-13T11:20:32.249161175" + }, + "-profile test_accessions_only": { + "content": [ + null, + [ + "errors", + "expression_atlas", + "expression_atlas/accessions", + "expression_atlas/accessions/accessions.txt", + "expression_atlas/accessions/selected_experiments.metadata.tsv", + "expression_atlas/accessions/species_experiments.metadata.tsv", + "geo", + "geo/accessions", + "geo/accessions/accessions.txt", + "geo/accessions/geo_all_datasets.metadata.tsv", + "geo/accessions/geo_selected_datasets.metadata.tsv", + "multiqc", + "multiqc/multiqc_data", + "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_eatlas_all_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_selected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_software_versions.txt", + "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_plots", + "multiqc/multiqc_plots/pdf", + "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_selected_experiments_metadata.pdf", + "multiqc/multiqc_plots/png", + "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", + "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_selected_experiments_metadata.png", + "multiqc/multiqc_plots/svg", + "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_selected_experiments_metadata.svg", + "multiqc/multiqc_report.html", + "multiqc/versions.yml", + "pipeline_info", + "pipeline_info/software_mqc_versions.yml", + "warnings" + ], + [ + "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", + "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "accessions.txt:md5,530c21fd138405dc6bcdd275633c07ef", + "geo_all_datasets.metadata.tsv:md5,843b27695ec965265a4926e5de656112", + "geo_selected_datasets.metadata.tsv:md5,843b27695ec965265a4926e5de656112", + "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", + "multiqc_eatlas_all_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", + "multiqc_eatlas_selected_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", + "multiqc_geo_all_experiments_metadata.txt:md5,e7581df8fc257738d299989bc8257978", + "multiqc_geo_selected_experiments_metadata.txt:md5,e7581df8fc257738d299989bc8257978", + "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-13T11:21:09.701097728" + }, + "-profile test_no_dataset_found": { + "content": [ + null, + [ + "errors", + "expression_atlas", + "expression_atlas/accessions", + "expression_atlas/accessions/accessions.txt", + "expression_atlas/accessions/species_experiments.metadata.tsv", + "geo", + "geo/accessions", + "geo/accessions/accessions.txt", + "idmapping", + "merged_datasets", + "multiqc", + "multiqc/multiqc_data", + "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_software_versions.txt", + "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_report.html", + "multiqc/versions.yml", + "pipeline_info", + "pipeline_info/software_mqc_versions.yml", + "warnings" + ], + [ + "accessions.txt:md5,d41d8cd98f00b204e9800998ecf8427e", + "species_experiments.metadata.tsv:md5,68b329da9893e34099c7d8ad5cb9c940", + "accessions.txt:md5,d41d8cd98f00b204e9800998ecf8427e", + "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", + "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030" ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T16:58:37.385701449" + "timestamp": "2025-11-12T15:54:15.001920938" }, - "-profile test_run_normfinder_genorm": { + "-profile test_full": { "content": [ null, [ @@ -890,13 +870,14 @@ "aggregate_results/all_genes_summary.csv", "aggregate_results/top_stable_genes_summary.csv", "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", - "base_statistics", - "base_statistics/all", - "base_statistics/all/stats_all_genes.csv", - "base_statistics/rnaseq", - "base_statistics/rnaseq/rnaseq.stats_all_genes.csv", "clean_count_data", "clean_count_data/cleaned_counts_filtered.parquet", + "compute_base_statistics", + "compute_base_statistics/stats_all_genes.csv", + "compute_base_statistics_for_rnaseq", + "compute_base_statistics_for_rnaseq/rnaseq.stats_all_genes.csv", + "compute_m_measure", + "compute_m_measure/m_measures.csv", "compute_stability_scores", "compute_stability_scores/stats_with_scores.csv", "cross_join", @@ -1104,12 +1085,17 @@ "dash_app/src/utils/style.py", "dash_app/versions.yml", "dataset_statistics", - "dataset_statistics/E_MTAB_7711_rnaseq.dataset_stats.csv", + "dataset_statistics/E_MTAB_5072_rnaseq.dataset_stats.csv", "errors", + "errors/geo_failure_reasons.csv", "expression_atlas", + "expression_atlas/accessions", + "expression_atlas/accessions/accessions.txt", + "expression_atlas/accessions/selected_experiments.metadata.tsv", + "expression_atlas/accessions/species_experiments.metadata.tsv", "expression_atlas/datasets", - "expression_atlas/datasets/E_MTAB_7711_rnaseq.design.csv", - "expression_atlas/datasets/E_MTAB_7711_rnaseq.rnaseq.raw.counts.csv", + "expression_atlas/datasets/E_MTAB_5072_rnaseq.design.csv", + "expression_atlas/datasets/E_MTAB_5072_rnaseq.rnaseq.raw.counts.csv", "expression_ratio", "expression_ratio/ratios.0.0.parquet", "expression_ratio/ratios.0.1.parquet", @@ -1265,13 +1251,19 @@ "expression_ratio/ratios.8.9.parquet", "expression_ratio/ratios.9.9.parquet", "geo", + "geo/accessions", + "geo/accessions/accessions.txt", + "geo/accessions/geo_all_datasets.metadata.tsv", + "geo/accessions/geo_rejected_datasets.metadata.tsv", + "geo/accessions/geo_selected_datasets.metadata.tsv", + "geo/datasets", + "geo/datasets/failure_reason.txt", "get_candidate_genes", "get_candidate_genes/candidate_counts.parquet", "idmapping", - "idmapping/datasets", - "idmapping/datasets/E_MTAB_7711_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/datasets/E_MTAB_7711_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/datasets/E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/E_MTAB_5072_rnaseq.rnaseq.raw.counts.mapping.csv", + "idmapping/E_MTAB_5072_rnaseq.rnaseq.raw.counts.metadata.csv", + "idmapping/E_MTAB_5072_rnaseq.rnaseq.raw.counts.renamed.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", "make_chunks", @@ -1292,11 +1284,11 @@ "make_chunks/count_chunk.7.parquet", "make_chunks/count_chunk.8.parquet", "make_chunks/count_chunk.9.parquet", + "merge_all_counts", + "merge_all_counts/all_counts.parquet", + "merge_rnaseq_counts", + "merge_rnaseq_counts/all_counts.parquet", "merged_datasets", - "merged_datasets/all", - "merged_datasets/all/all_counts.parquet", - "merged_datasets/rnaseq", - "merged_datasets/rnaseq/all_counts.parquet", "merged_datasets/whole_design.csv", "multiqc", "multiqc/multiqc_data", @@ -1305,35 +1297,61 @@ "multiqc/multiqc_data/multiqc.parquet", "multiqc/multiqc_data/multiqc_citations.txt", "multiqc/multiqc_data/multiqc_data.json", + "multiqc/multiqc_data/multiqc_eatlas_all_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", "multiqc/multiqc_data/multiqc_gene_statistics.txt", + "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_failure_reasons.txt", + "multiqc/multiqc_data/multiqc_geo_rejected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_selected_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", "multiqc/multiqc_plots", "multiqc/multiqc_plots/pdf", + "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", "multiqc/multiqc_plots/pdf/gene_statistics.pdf", + "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_failure_reasons.pdf", + "multiqc/multiqc_plots/pdf/geo_rejected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_selected_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", "multiqc/multiqc_plots/png", + "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", + "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", "multiqc/multiqc_plots/png/gene_statistics.png", + "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_failure_reasons.png", + "multiqc/multiqc_plots/png/geo_rejected_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_selected_experiments_metadata.png", "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", "multiqc/multiqc_plots/svg", + "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", "multiqc/multiqc_plots/svg/gene_statistics.svg", + "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_failure_reasons.svg", + "multiqc/multiqc_plots/svg/geo_rejected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_selected_experiments_metadata.svg", "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", "normalised", - "normalised/E_MTAB_7711_rnaseq", - "normalised/E_MTAB_7711_rnaseq/normalisation_deseq2", - "normalised/E_MTAB_7711_rnaseq/normalisation_deseq2/E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normalised/E_MTAB_5072_rnaseq", + "normalised/E_MTAB_5072_rnaseq/normalisation_deseq2", + "normalised/E_MTAB_5072_rnaseq/normalisation_deseq2/E_MTAB_5072_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normfinder", + "normfinder/stability_values.normfinder.csv", "pipeline_info", "pipeline_info/software_mqc_versions.yml", "quantile_normalised", - "quantile_normalised/E_MTAB_7711_rnaseq", - "quantile_normalised/E_MTAB_7711_rnaseq/E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", + "quantile_normalised/E_MTAB_5072_rnaseq", + "quantile_normalised/E_MTAB_5072_rnaseq/E_MTAB_5072_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", "ratio_standard_variation", "ratio_standard_variation/std.0.0.parquet", "ratio_standard_variation/std.0.1.parquet", @@ -1488,1285 +1506,572 @@ "ratio_standard_variation/std.8.8.parquet", "ratio_standard_variation/std.8.9.parquet", "ratio_standard_variation/std.9.9.parquet", - "stability_scoring", - "stability_scoring/genorm", - "stability_scoring/genorm/m_measures.csv", - "stability_scoring/normfinder", - "stability_scoring/normfinder/stability_values.normfinder.csv", "warnings" ], [ - "all_counts_filtered.parquet:md5,65d131627063eb9934bbb4f5adc98bc0", - "all_genes_summary.csv:md5,9323e53e94eba5317ec0418041610a9d", - "top_stable_genes_summary.csv:md5,70a73b2f4458113f9b842bf1aeb1c74e", - "top_stable_genes_transposed_counts_filtered.csv:md5,dea022542f54cdec7365b9a921afa841", - "stats_all_genes.csv:md5,b18933f229b7846edfe91886883a8a54", - "rnaseq.stats_all_genes.csv:md5,fc3c4b9b2365f2aeca3f0967edcd7b15", - "cleaned_counts_filtered.parquet:md5,4b40a0560f45426f0c09f347efce253f", - "stats_with_scores.csv:md5,9df06189d08aff0f9065f8a5786ece45", - "cross_join.0.0.parquet:md5,7e76ae2c5bc60ece6e875663a34f6906", - "cross_join.0.1.parquet:md5,f3239e993dd42558bb35ee793761a82e", - "cross_join.0.10.parquet:md5,0aa5b14054b2045c7d060b1f6cbb1da2", - "cross_join.0.11.parquet:md5,b0f3ca05ea3d5a1af36980080a67bed7", - "cross_join.0.12.parquet:md5,c2903251a9062614f79ff8c795354487", - "cross_join.0.13.parquet:md5,4ceb9e71b8327f1e29f5a91161961ab0", - "cross_join.0.14.parquet:md5,b1a0069c67ba0f86f610640d9d4ae161", - "cross_join.0.15.parquet:md5,940975300e1e218df3400cc65c940a0d", - "cross_join.0.16.parquet:md5,904420c55df8af8f299892e1200a5c70", - "cross_join.0.2.parquet:md5,c9eb3049e1003e21f0ce6d133e2c5958", - "cross_join.0.3.parquet:md5,d6dc799ce78643f72a7431d1372604bc", - "cross_join.0.4.parquet:md5,fc0c7a169541ff1ccb9d1f54f6a02eeb", - "cross_join.0.5.parquet:md5,2cb746cb1c2cd758d629b08eb3a36f5b", - "cross_join.0.6.parquet:md5,28c531142803de10b09beece294fff7d", - "cross_join.0.7.parquet:md5,601ba9b3e6e44e496a9c1299b4c5e70b", - "cross_join.0.8.parquet:md5,06688bf36f60cc39ecbceb7a66e8122f", - "cross_join.0.9.parquet:md5,a75bd71e3b31bfe4acad7a766c253f9b", - "cross_join.1.1.parquet:md5,d77719ff32309955c8b17ad1b61bdbee", - "cross_join.1.10.parquet:md5,40dc3f4d056bee644c1504265682878e", - "cross_join.1.11.parquet:md5,aaa7daea414e3e466d086d0379dbd6e0", - "cross_join.1.12.parquet:md5,c99898f1e848fa592c6a5de965903e4a", - "cross_join.1.13.parquet:md5,09233e2c81b092c812c92472eddd9cb4", - "cross_join.1.14.parquet:md5,1510b25ba5fcdbf4780407f0de5cf1f4", - "cross_join.1.15.parquet:md5,7da33e84472fbdceda0156d6f75adc3d", - "cross_join.1.16.parquet:md5,4b68c051e85be6f78c9945d48c336df2", - "cross_join.1.2.parquet:md5,ee2cfeceff695fd8a737698d78381e93", - "cross_join.1.3.parquet:md5,e3fd3f04c6be97978fb259d6112b68a4", - "cross_join.1.4.parquet:md5,592b9887dbabf7a4e1e1fcd7a2fc8a23", - "cross_join.1.5.parquet:md5,16f23da82c8b9919a3843ce316d7c581", - "cross_join.1.6.parquet:md5,707c65cb3acfcda0226e0947cf40c27b", - "cross_join.1.7.parquet:md5,272b98cf7d9a657e08e8d4fc6b52474b", - "cross_join.1.8.parquet:md5,8b3e497055119e27503a42cde68d3f12", - "cross_join.1.9.parquet:md5,d1e292ea4b15e0ad3d6e1b619f669971", - "cross_join.10.10.parquet:md5,6d95a8e4df684aa1186fe88105b0d771", - "cross_join.10.11.parquet:md5,d9cc7eacab721f14a1952e521ac68b80", - "cross_join.10.12.parquet:md5,fd20184d3d9e3b3e06f19f26842458f2", - "cross_join.10.13.parquet:md5,67f21c1f7e52278b78eb4553acf26acf", - "cross_join.10.14.parquet:md5,12a2559266cab49986f53d90fd5cbc5f", - "cross_join.10.15.parquet:md5,d307b4fc1fff13410770fe80e4627fe8", - "cross_join.10.16.parquet:md5,a7bbd0d28bb122ddd250f34b33b089ca", - "cross_join.10.2.parquet:md5,03ee9f3eb5606ada3575eb0f0bdf1921", - "cross_join.10.3.parquet:md5,a1372fc98a3d090b010cf00f66de51a9", - "cross_join.10.4.parquet:md5,b6b3f8276ab1f1d78ed68441c1f8d273", - "cross_join.10.5.parquet:md5,f378b2cf1adab3ed49696e4db1e24fab", - "cross_join.10.6.parquet:md5,0c6b32c8da9ed3632ec82788d452e705", - "cross_join.10.7.parquet:md5,175621b5483923ec4d0cbaa5f50e3084", - "cross_join.10.8.parquet:md5,219766da467e926c8c7daedbf92c8e11", - "cross_join.10.9.parquet:md5,1973f9214a0d5a567f555f17163fba7d", - "cross_join.11.11.parquet:md5,be4d4bcd311a1e0bc8403bbcd21671c2", - "cross_join.11.12.parquet:md5,468cc0dbeec0c4b5d6362aa8b5febf83", - "cross_join.11.13.parquet:md5,8a1798e49d050dea5a45c5122863eb20", - "cross_join.11.14.parquet:md5,ad9cfdb85e7a6c32232ea2f232f210d0", - "cross_join.11.15.parquet:md5,9037285954b111f87a8e28c4767b80a2", - "cross_join.11.16.parquet:md5,b0570f3c710c478448446636372d0b0e", - "cross_join.11.2.parquet:md5,0c2b761b9fd07d412a1ae541797458df", - "cross_join.11.3.parquet:md5,66c6eba5162d49a9c99ed730e2767cc2", - "cross_join.11.4.parquet:md5,36e3781d877256a69fae1b740e2f79b6", - "cross_join.11.5.parquet:md5,7f9e43d253ad11a97dbfb189e75571ec", - "cross_join.11.6.parquet:md5,0a61be9f810fb3f58c2176fe48710598", - "cross_join.11.7.parquet:md5,91b09cb87582c897a75e292448efcb1c", - "cross_join.11.8.parquet:md5,e0e77fd9de40a66f01758fa7c1bbfe8a", - "cross_join.11.9.parquet:md5,631572ba939b5a0404743424eb62d239", - "cross_join.12.12.parquet:md5,cb28628d8731aa57361a349025a7a706", - "cross_join.12.13.parquet:md5,96e1990c873894440ca841c51260346f", - "cross_join.12.14.parquet:md5,282e757d3673308a464428359b419582", - "cross_join.12.15.parquet:md5,eea9c40c7c953c24f7698ddbc001f530", - "cross_join.12.16.parquet:md5,d3b31846618dc835ab71f053eb8b97b5", - "cross_join.12.2.parquet:md5,d967f7f7895bbd44eebab63592e49894", - "cross_join.12.3.parquet:md5,50c90d2a1702eba78d6db85155e84554", - "cross_join.12.4.parquet:md5,88bfaf60385ee591339bc57baf10e4ff", - "cross_join.12.5.parquet:md5,63bf06cb06bdace2db39253b2e85a025", - "cross_join.12.6.parquet:md5,0a161f5b85b964fa239db840f57c8611", - "cross_join.12.7.parquet:md5,e23da7b3ab34a2a4b1f860118538a4b1", - "cross_join.12.8.parquet:md5,79542f9143dad125924a083ec1d7df41", - "cross_join.12.9.parquet:md5,066fc6ceca7e54c1979e077ba6405218", - "cross_join.13.13.parquet:md5,7e37267a571bb95b506ae6672c8a9557", - "cross_join.13.14.parquet:md5,b0cf0bc64d1663ba982d0605d153ece1", - "cross_join.13.15.parquet:md5,8b011d02a96be64aa6171eb4c3707b90", - "cross_join.13.16.parquet:md5,95ab0f3806614abfc26f43cdb4c84bce", - "cross_join.13.2.parquet:md5,0e212df392ed0239ff778961dd40e46e", - "cross_join.13.3.parquet:md5,a3fed95f81dd6cdb0437d60952d5fef6", - "cross_join.13.4.parquet:md5,0f7f006f7d0423e0b7a91e6e650cd197", - "cross_join.13.5.parquet:md5,67af140a74fcf2df0905190bde8f3d39", - "cross_join.13.6.parquet:md5,1c24eae1cf16cd01b05cfecf73ae51ce", - "cross_join.13.7.parquet:md5,42b2a4cdd3e637d1bf278d53c122f0e7", - "cross_join.13.8.parquet:md5,55acaa54412ee87f253c40af0f4720a4", - "cross_join.13.9.parquet:md5,9fd116028ec743df3ef50d948689cf8a", - "cross_join.14.14.parquet:md5,d06bcb7a086822d2238c0ab644adfcf8", - "cross_join.14.15.parquet:md5,7f17ae4eca41a0435d911e6e7ad73382", - "cross_join.14.16.parquet:md5,a4740227f9848ed1def781a0fc07009c", - "cross_join.14.2.parquet:md5,5102c49b7c0c5c1465482530c70aeed1", - "cross_join.14.3.parquet:md5,a181402abb6fdaca79dd74c58a287248", - "cross_join.14.4.parquet:md5,7c2cb03f830058fc18a1606b665f94f4", - "cross_join.14.5.parquet:md5,af066c203e660622b2e6e6dd37a4e06e", - "cross_join.14.6.parquet:md5,8830e4c70e8c624ccffa74fde9a5ac8f", - "cross_join.14.7.parquet:md5,796d8d492d9854f7452a71bfc7dd3a31", - "cross_join.14.8.parquet:md5,b5d49e15174dbe5a386d174755962063", - "cross_join.14.9.parquet:md5,3c76f5e802b616221a5f9ec6f38a4072", - "cross_join.15.15.parquet:md5,8af8def9e60bb4c8fe18e59fa866be1e", - "cross_join.15.16.parquet:md5,3330749f39d30058fec249bc39ce6215", - "cross_join.15.2.parquet:md5,e79a754fcb0f59c262402949c4c0fda6", - "cross_join.15.3.parquet:md5,13faddf90e86419788d5627e00e8e282", - "cross_join.15.4.parquet:md5,be779e8db543cfdf8b10ab378f668651", - "cross_join.15.5.parquet:md5,221cc3fa7a8a27ef721de72fdfb94276", - "cross_join.15.6.parquet:md5,64aad6280f50523202994059995dec64", - "cross_join.15.7.parquet:md5,02fd2392273b5cac4fb81ba1eaeff0ac", - "cross_join.15.8.parquet:md5,4829cc82747059080e382dd1ad44f3b6", - "cross_join.15.9.parquet:md5,ccfec7a98eef5b5445c16857a5d74b3a", - "cross_join.16.16.parquet:md5,b3d1639fa96b20d7eacd910d2016af62", - "cross_join.16.2.parquet:md5,53003afb997e0997d792144db6304557", - "cross_join.16.3.parquet:md5,9640e18450ccf4f5432f72d7a003bcce", - "cross_join.16.4.parquet:md5,03d287f1763a29d3f3bdc74003034146", - "cross_join.16.5.parquet:md5,6e053271fb906b8583623eb96694a66b", - "cross_join.16.6.parquet:md5,8c0d6aa210abde390cccaca7f49664ba", - "cross_join.16.7.parquet:md5,08e199e75a3c80e3bc3debc98d7db7a2", - "cross_join.16.8.parquet:md5,d4c465c45c56806a302233e3c81ac6cd", - "cross_join.16.9.parquet:md5,1e65bffb9e1b899c00535f058e784e33", - "cross_join.2.2.parquet:md5,ef4c3a0f342c4b9692b29a3987477937", - "cross_join.2.3.parquet:md5,b284154698adfa08cd341662b735133c", - "cross_join.2.4.parquet:md5,ab97a2f6d9eefd07478f7d87ac21f1d5", - "cross_join.2.5.parquet:md5,cff079280d60da457d6090d983a78602", - "cross_join.2.6.parquet:md5,6cc58636207f0b85c61729077d50dfe2", - "cross_join.2.7.parquet:md5,e79a84834f60a97377f16d79e2c70b52", - "cross_join.2.8.parquet:md5,4340e9787155977c5d1dca36d1f5b291", - "cross_join.2.9.parquet:md5,f5d2706522c1a8ae9ff2199747d3d8ba", - "cross_join.3.3.parquet:md5,7b18de4ddd5e94fee08c4f2dde442849", - "cross_join.3.4.parquet:md5,7757d071ca5195441917b25e0fc2584b", - "cross_join.3.5.parquet:md5,0e9056434cf760941e14792c3072d8df", - "cross_join.3.6.parquet:md5,0de03058e0dda96292057ac6a6b411d5", - "cross_join.3.7.parquet:md5,f883c241d7d327f592494d6ee2faf7bb", - "cross_join.3.8.parquet:md5,bc7e226977a7cef8cc97b6ea8f7dae18", - "cross_join.3.9.parquet:md5,a2f0b34e3665e8269b0713fe28415c00", - "cross_join.4.4.parquet:md5,c49c2963a6c4fa69cf490f66f814e2a8", - "cross_join.4.5.parquet:md5,a2123fbda2b34fb9f141480bdaff2340", - "cross_join.4.6.parquet:md5,5d6e362d7276975963d09d59133bcc66", - "cross_join.4.7.parquet:md5,ce5adfcd008cc8b1aa2726b548e56b14", - "cross_join.4.8.parquet:md5,d82edd26f55b844b2ca5739e94ff2326", - "cross_join.4.9.parquet:md5,dc6a5cc140e9e05d877190257fb5fc92", - "cross_join.5.5.parquet:md5,f13837a3108e4abdb101661282121d9b", - "cross_join.5.6.parquet:md5,04fe81e433b97db698e44be70b979e60", - "cross_join.5.7.parquet:md5,484dda88b6882f12e3bad7ba9e6c0a56", - "cross_join.5.8.parquet:md5,bafbf426b355f228d72a20b2e0b4f717", - "cross_join.5.9.parquet:md5,ca96c9ab5e9538174a80e2a8a7fd58e5", - "cross_join.6.6.parquet:md5,e35baa2f4667516d663a5525a36a3ecb", - "cross_join.6.7.parquet:md5,905387980011661d51e3a05e47d4d501", - "cross_join.6.8.parquet:md5,0aeb5f8e18ba4d730e342db7191fea02", - "cross_join.6.9.parquet:md5,5c7be098bca498dd835f313047f628ce", - "cross_join.7.7.parquet:md5,32a3730594b57ee826b5bbb25fd3d78a", - "cross_join.7.8.parquet:md5,4abddb54f721c32b7645b5ae2553c485", - "cross_join.7.9.parquet:md5,e5beeb167d286e81b3b350a238273ef1", - "cross_join.8.8.parquet:md5,6a96ab00f0867eda804826145cd8344e", - "cross_join.8.9.parquet:md5,d815351114a073dbc19d2914cd457f2d", - "cross_join.9.9.parquet:md5,accc09150e6b9103bd54c0c9e4ec13a0", + "all_counts_filtered.parquet:md5,109de37753cdd7012e041a89ebd2b8a3", + "all_genes_summary.csv:md5,8ff2dbe05616a18a2f2c5c33c249ec4b", + "top_stable_genes_summary.csv:md5,1bf3eb0779e2db255e26c1397e3933b6", + "top_stable_genes_transposed_counts_filtered.csv:md5,be1270a3c44d0dca96ff6461686e3aca", + "cleaned_counts_filtered.parquet:md5,01f8763f2ccd1c35b8a36236a5c6998a", + "stats_all_genes.csv:md5,cc94561057b4ce572aa676d10d0b661f", + "rnaseq.stats_all_genes.csv:md5,7530f9d42a6bc91d154f6156255fe273", + "m_measures.csv:md5,bd71bb4975077b9b8190d1335b4c9f4c", + "stats_with_scores.csv:md5,3e6a0b164106f0045189e7c6475b4dc3", + "cross_join.0.0.parquet:md5,5fd6a1dae67dc1df624139d484dbcb29", + "cross_join.0.1.parquet:md5,1e3a29760a38effb552cd0ac63e5a3c6", + "cross_join.0.10.parquet:md5,6d4334ebecedc7b72de46049826f8b51", + "cross_join.0.11.parquet:md5,bd1bcd03ed0a9e3e7e30a1082f627739", + "cross_join.0.12.parquet:md5,fb2b0410d4b8f3c230ac64842f60f3b7", + "cross_join.0.13.parquet:md5,c23e71879e6e505c1782369818d7b8f4", + "cross_join.0.14.parquet:md5,26afcbb9299dd8cb099d0a7771769e2e", + "cross_join.0.15.parquet:md5,3f7b2f991a2ffe19d9882ab892c4b645", + "cross_join.0.16.parquet:md5,ae4862e830a6dae33c3b19846ff80305", + "cross_join.0.2.parquet:md5,fb3c22ebd3979e6ec06e8428daaf14d7", + "cross_join.0.3.parquet:md5,0c851dad5b72e2ba48cb52afc0aacc23", + "cross_join.0.4.parquet:md5,75d537dd86c150824877aa5d7b6de912", + "cross_join.0.5.parquet:md5,8bdc78157b03d927545953c1d4b79f8d", + "cross_join.0.6.parquet:md5,f844f69a06953a5220617d8f0392ad74", + "cross_join.0.7.parquet:md5,732b1d409adb280dc32f56f5f1bd9c72", + "cross_join.0.8.parquet:md5,b92fc809b64b9bcae873945646bc178c", + "cross_join.0.9.parquet:md5,b786fdf8284e88d07d37344d347a858d", + "cross_join.1.1.parquet:md5,d9437c640e4e7f3b91364355f2e9c078", + "cross_join.1.10.parquet:md5,74aec5724dfad2b1ae7fe36e39de2d95", + "cross_join.1.11.parquet:md5,8af11e225a102b21f97a27bc9478618e", + "cross_join.1.12.parquet:md5,933bf8c423028a1b5c94de19db36052c", + "cross_join.1.13.parquet:md5,b0398692508b3f5adb564045f323ffbf", + "cross_join.1.14.parquet:md5,2de06195ef078f3ddabcb5c76bc7eae4", + "cross_join.1.15.parquet:md5,19305e8939f5876d999406f5e5623617", + "cross_join.1.16.parquet:md5,c1d6196e5b71d39f6ff70ab1b52802b3", + "cross_join.1.2.parquet:md5,efe4b4719b8ffc88eb51f042b268e2fb", + "cross_join.1.3.parquet:md5,bb6f16dfeb5e0463df4ccbd99cda0d0d", + "cross_join.1.4.parquet:md5,40f9db742f31ef65723960ee0e2a2b96", + "cross_join.1.5.parquet:md5,59048c9c0ae4f4b158ad4559a559118a", + "cross_join.1.6.parquet:md5,5ea8cf5a06b2ed1f79d204eac22399ef", + "cross_join.1.7.parquet:md5,24a267230327e89947f96c1a7b4ea93a", + "cross_join.1.8.parquet:md5,9e9374269f92aceba9cdbe2865864c10", + "cross_join.1.9.parquet:md5,e1ca479630ba09943a7e9ee845a838e2", + "cross_join.10.10.parquet:md5,244b9a859ba819563709bccbefdff97c", + "cross_join.10.11.parquet:md5,f2f2c84c860fdef63d978559bb121117", + "cross_join.10.12.parquet:md5,9310659e5e8b18634b34be2f183f7cc7", + "cross_join.10.13.parquet:md5,5f159adfb0c6f73bb5e9b233dc46b44a", + "cross_join.10.14.parquet:md5,bb44792fa83792223cf8db601b2a411c", + "cross_join.10.15.parquet:md5,b70dc09265a65556ae8a59c7404eafb9", + "cross_join.10.16.parquet:md5,84a5d07a174f446e202f4b064a1daa2c", + "cross_join.10.2.parquet:md5,5a3a75f4f4005283fa8815829dc632e4", + "cross_join.10.3.parquet:md5,980ce729b70c57bfb624c07ebde14a81", + "cross_join.10.4.parquet:md5,3be21cb2fbe6b2844e8aace359ec8e3d", + "cross_join.10.5.parquet:md5,17c5cf6b75fc4d5f2bfab295a56b8085", + "cross_join.10.6.parquet:md5,b472a936ca54b0a82be87ee92f1ec7e4", + "cross_join.10.7.parquet:md5,f9b9ea716ec9289a564a15c96a128ad2", + "cross_join.10.8.parquet:md5,65ca99c0ddef5be894c09475d6100dd9", + "cross_join.10.9.parquet:md5,b215ac67666e6c4e7259f069be716f51", + "cross_join.11.11.parquet:md5,079c2f972444dd419dfa924bd4984659", + "cross_join.11.12.parquet:md5,fe3a0baa33d37a9ccc233604e11d26da", + "cross_join.11.13.parquet:md5,3aded985226e07fe1cded2fed6be487c", + "cross_join.11.14.parquet:md5,754682bc82ac1ea5d6e6247c8d8abe1f", + "cross_join.11.15.parquet:md5,b7da4a1bb1107fbbd7d1b19140006d5e", + "cross_join.11.16.parquet:md5,7dfc8b3edf266e90257b85745b0e61cd", + "cross_join.11.2.parquet:md5,627bc600ef080daa45c7f79604db8e05", + "cross_join.11.3.parquet:md5,2fef0c7260ee3a77ca42ac3e63bb2424", + "cross_join.11.4.parquet:md5,2ba0c698b1161ad5a86847457c0e1992", + "cross_join.11.5.parquet:md5,68e57305be79cf8689b4cce228a2e7f1", + "cross_join.11.6.parquet:md5,42a9af5c563be4a933838220c396e4f4", + "cross_join.11.7.parquet:md5,cc9365bc90fe45673939b8f8d0e1d005", + "cross_join.11.8.parquet:md5,4f88965590647667ade4447203f356fe", + "cross_join.11.9.parquet:md5,a37890fb4c0d25184e3e6079dbbeae72", + "cross_join.12.12.parquet:md5,f744a30ab26946b0ffdb72d13bc8093f", + "cross_join.12.13.parquet:md5,d28c083a30714cbf0d1b45905be12a13", + "cross_join.12.14.parquet:md5,c820d91e28e7cbbb1314366cc4f75c01", + "cross_join.12.15.parquet:md5,b303188b2b3d2db7211986bfe3479ec8", + "cross_join.12.16.parquet:md5,daa4c8b3cd99ed117214ccd6a692c929", + "cross_join.12.2.parquet:md5,05ed39294566841e28fec0f3e776f450", + "cross_join.12.3.parquet:md5,5d7e2c939025f99d47ab1db678a87ee4", + "cross_join.12.4.parquet:md5,0bdbe47587396df4939317edd8249627", + "cross_join.12.5.parquet:md5,5749855ae83d2b4611f03e3f5c4e8a2c", + "cross_join.12.6.parquet:md5,98a2af5989c08ae83a204bc7186511bd", + "cross_join.12.7.parquet:md5,320cfc607b848c0791af8897aee40a86", + "cross_join.12.8.parquet:md5,0b046e527f232b0c58d6ba9d68049612", + "cross_join.12.9.parquet:md5,314518452a6cb830bc62c818beb5f1e2", + "cross_join.13.13.parquet:md5,36c716cb99a71bb9d040641a38cc277a", + "cross_join.13.14.parquet:md5,9b67b5364ea9084e59be1f1f07df2b39", + "cross_join.13.15.parquet:md5,5a9d644d6f37041fd6abd662ef6456b4", + "cross_join.13.16.parquet:md5,01f98ffad1cd9d36c2c6b8a5ebd32a30", + "cross_join.13.2.parquet:md5,ae1d026c82b0963bf83ce57631eed942", + "cross_join.13.3.parquet:md5,5047ee3445752221569f11dbfc197dbb", + "cross_join.13.4.parquet:md5,cfe159929a6d5a0b96f9a259760fa83b", + "cross_join.13.5.parquet:md5,8d44bc6b15b77d6277674a70f4c5494a", + "cross_join.13.6.parquet:md5,41f7706137e4f93283e63a50bd7fe0aa", + "cross_join.13.7.parquet:md5,ccf2d2ff2018b503acf662ae17839f27", + "cross_join.13.8.parquet:md5,e685e8221d57a7c389eeca0284780aee", + "cross_join.13.9.parquet:md5,144b44780ab3fbc0c32551265e67ea26", + "cross_join.14.14.parquet:md5,cdef3ac358396bfb7daddeaecc8e84cc", + "cross_join.14.15.parquet:md5,97fa15167da8e59c6f10f1f7587ed001", + "cross_join.14.16.parquet:md5,a55f95c7799f97d6fb7d3ba1593133e2", + "cross_join.14.2.parquet:md5,9f98ce2c2e9133f7fe54ac2e6f86c40f", + "cross_join.14.3.parquet:md5,7c6f697d2d615b1ccad6dbb8a6076daa", + "cross_join.14.4.parquet:md5,14fb0e5cb5fe06dbe6b7413b361fbb38", + "cross_join.14.5.parquet:md5,5b7af7eea35e3544bb37502d28757d52", + "cross_join.14.6.parquet:md5,f17a77d6af3af65e96cc9da72345bc2f", + "cross_join.14.7.parquet:md5,5f75d38297db27106453ab22a9adbc7d", + "cross_join.14.8.parquet:md5,c4bf140b8931a7a82894d4fb77d213e7", + "cross_join.14.9.parquet:md5,2e48728c6fa8a2fc4aaad3437b72bb9e", + "cross_join.15.15.parquet:md5,54002d0fc65ba9058be2d02a9daf306d", + "cross_join.15.16.parquet:md5,92731cfb4e58ba8ebd6ba0f87c9ba197", + "cross_join.15.2.parquet:md5,3354fd9691f9800ac5305c1723a7d0cf", + "cross_join.15.3.parquet:md5,37e28eb3f534b7f829d7eb2f087b8f1e", + "cross_join.15.4.parquet:md5,990ff37df8f143ef0c396d2c80c860c7", + "cross_join.15.5.parquet:md5,1bf0c54938ef505fcd43c629d0f13482", + "cross_join.15.6.parquet:md5,7dce95e2bf73756b2b58940fe3bca2a0", + "cross_join.15.7.parquet:md5,121e69b7c5f109befb8be2fd8397c125", + "cross_join.15.8.parquet:md5,9edf68ebc4fd13cd34ce1a0200b7a315", + "cross_join.15.9.parquet:md5,d40c8478aabf1621563d833b9e951364", + "cross_join.16.16.parquet:md5,a5fd4eaf58cb25fd9594b551b78315cd", + "cross_join.16.2.parquet:md5,bb38f400a2dbfad05111109d988ac3ec", + "cross_join.16.3.parquet:md5,958c90bf70813e177a9944be4f3d7980", + "cross_join.16.4.parquet:md5,e328e62d33d857077d29e9b66a16a602", + "cross_join.16.5.parquet:md5,7411115d1d8f449277c74c29b629a85c", + "cross_join.16.6.parquet:md5,cd8c5eefd5e4657b052b19eb9a181cc3", + "cross_join.16.7.parquet:md5,1ea5acffe519a38503abfe315360b015", + "cross_join.16.8.parquet:md5,0f48df1531a2fc40a88863754085ebec", + "cross_join.16.9.parquet:md5,e51b6d69f4f4ed4c184fed64db7c96bc", + "cross_join.2.2.parquet:md5,5a2f6f34e343a0c2dd7ee8299613123d", + "cross_join.2.3.parquet:md5,33249d35df701286d1735e74d6a34a7f", + "cross_join.2.4.parquet:md5,a7a00b58e62f5df689192ccf116e3c2e", + "cross_join.2.5.parquet:md5,f4a48d2afe3835d5e93fd07cf79fc02e", + "cross_join.2.6.parquet:md5,f31f3e7779caf3f519901d6b59e300c2", + "cross_join.2.7.parquet:md5,7a1d070623158efbde1e1d9500f53122", + "cross_join.2.8.parquet:md5,68b53d56cae2587196101477d6af7a8a", + "cross_join.2.9.parquet:md5,371d8849ebeadf84e016b471ebe39072", + "cross_join.3.3.parquet:md5,e54c447f8cad67ab5fbec60afe141786", + "cross_join.3.4.parquet:md5,dccc10846689d571b0a6471ce6fb484b", + "cross_join.3.5.parquet:md5,0464d29f05811f9c89c3fd45f9ee84f9", + "cross_join.3.6.parquet:md5,34091d4a7071fd831a42d64ebe14366e", + "cross_join.3.7.parquet:md5,ae98d4458becb437cdf7cd2c6d2c8d1f", + "cross_join.3.8.parquet:md5,0c2df93a10eea8029c9ad3ce5b55dc87", + "cross_join.3.9.parquet:md5,d3467b182f799b7d6fbd013cff23f6c6", + "cross_join.4.4.parquet:md5,2fbf82e2656574416570999c43f5eeaf", + "cross_join.4.5.parquet:md5,037a32ae4149210cda8e5b37111a69e5", + "cross_join.4.6.parquet:md5,231a14970e60f5f1bc361fe53f5b4e92", + "cross_join.4.7.parquet:md5,741ea6a0b9d02a6b3df70ef680632768", + "cross_join.4.8.parquet:md5,a1290d1098fcef6a87d09b1dca5deee5", + "cross_join.4.9.parquet:md5,5853c4b70c9c4301c21d461f72b2da0a", + "cross_join.5.5.parquet:md5,69274e23cf6357379df8e76d0fa575a4", + "cross_join.5.6.parquet:md5,3aabbaebf07c2d3382200082fee8156e", + "cross_join.5.7.parquet:md5,7e7c46caa8a94fd784446f96338242f9", + "cross_join.5.8.parquet:md5,6a389f98d00e90931898a54913abc79e", + "cross_join.5.9.parquet:md5,0e76b1069fdea765f2bccead9d94c87a", + "cross_join.6.6.parquet:md5,6d5f0bd4ffdb44a10bf9d72c74c29508", + "cross_join.6.7.parquet:md5,cd5614f7bd2551097682d8fef890cf2b", + "cross_join.6.8.parquet:md5,9962e5a649db1316d38f453c3206a6b2", + "cross_join.6.9.parquet:md5,39255950a677e23efddb4b6398eb4dd5", + "cross_join.7.7.parquet:md5,a4e5bcb523c0691fdf843cdde2d99f0b", + "cross_join.7.8.parquet:md5,88e64a4ea5e65033b169d59c43578252", + "cross_join.7.9.parquet:md5,a7df442687e16cd69469506e97f1af51", + "cross_join.8.8.parquet:md5,3650b5a305f79b6be474f9a05d06251c", + "cross_join.8.9.parquet:md5,df394ce09a8d51b9c24c8ea9a42428f6", + "cross_join.9.9.parquet:md5,168734bb00e24074b026b7cd344a41dc", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_counts.parquet:md5,1736df00d0caf241236e64d6da98b925", - "all_genes_summary.csv:md5,9323e53e94eba5317ec0418041610a9d", - "whole_design.csv:md5,3e7748b54a0c25c008d9bd2ddbf1bf00", + "all_counts.parquet:md5,6e1db3f9aaee4e528db8e539e381b03f", + "all_genes_summary.csv:md5,8ff2dbe05616a18a2f2c5c33c249ec4b", + "whole_design.csv:md5,c9bfd7bc8ca365222e03e67eb24b9a76", "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", - "common.cpython-313.pyc:md5,5666c1573eb3a2dd9733835d7527bc26", - "genes.cpython-313.pyc:md5,2d45db16037e74010dc3577c3d6187f7", - "samples.cpython-313.pyc:md5,866b8fedb9547d80113abd37bcbba02d", + "common.cpython-313.pyc:md5,a3a9dbb6cd73407ef698f8386abf5f35", + "genes.cpython-313.pyc:md5,4d76b9a7317be3976387020f0ed4dc80", + "samples.cpython-313.pyc:md5,55b1e0fd8ba4990140e9637411b28da8", "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", - "graphs.cpython-313.pyc:md5,52b6f67756317cd40915b16ad561f2a4", - "right_sidebar.cpython-313.pyc:md5,9b2921aae3c1c180e49eb608027233fe", - "stores.cpython-313.pyc:md5,e99ea129f976c7c1bf8747a6129c546a", - "tables.cpython-313.pyc:md5,dc4b7170b0f981b1b05be0f183593c30", - "tooltips.cpython-313.pyc:md5,98b60822eb1a4b6ee3d696a364b77378", - "top.cpython-313.pyc:md5,df92052c49b88f9aefe1f26edc510050", + "graphs.cpython-313.pyc:md5,2a728177aa7f0c02834fa04aa7232538", + "right_sidebar.cpython-313.pyc:md5,85d9de2b7a22d3f0e4185662851b73a9", + "stores.cpython-313.pyc:md5,18d95e17779b0a77c39d11b71c396509", + "tables.cpython-313.pyc:md5,e0feb495e3a3d1c952f621584dab542e", + "tooltips.cpython-313.pyc:md5,bcbcc17e7a822b5aedb8c31ba518a7ba", + "top.cpython-313.pyc:md5,e29ecc7ee5a1bc73bbb54dab659b7cd1", "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", - "genes.cpython-313.pyc:md5,3a665f20da183dc279bfd4a5d8f3861d", - "samples.cpython-313.pyc:md5,9332e759e0d216e33c3bd2d55638fce9", + "genes.cpython-313.pyc:md5,fdff1778df115949010d1b05924d8b01", + "samples.cpython-313.pyc:md5,eee24288cc5d55f31bf2dc68db0203f9", "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", "tables.py:md5,84d17ad7f43458412e550f74a24560e5", "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.cpython-313.pyc:md5,8453d70001d40aef1185d7dc2fcb7a7b", - "data_management.cpython-313.pyc:md5,c2a5f1f6bf9bab0cab754107a06bb855", - "style.cpython-313.pyc:md5,5456916a833a9145562123efb3f04ca5", + "config.cpython-313.pyc:md5,e90479a881bee16b3ed350484b74409e", + "data_management.cpython-313.pyc:md5,d6568239ff2e87516c58ac2c7f92bbd0", + "style.cpython-313.pyc:md5,42b1d52f5750d54f694421409d6265f3", "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", "style.py:md5,b9f1207f06464e43c05e6e3912f12731", "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "E_MTAB_7711_rnaseq.dataset_stats.csv:md5,370265fa28a847926852fdc1db95afcc", - "E_MTAB_7711_rnaseq.design.csv:md5,3e7748b54a0c25c008d9bd2ddbf1bf00", - "E_MTAB_7711_rnaseq.rnaseq.raw.counts.csv:md5,3c02cf432c29d3751c978439539df388", - "ratios.0.0.parquet:md5,a355c51f1c5d59bc06cf03a151c245b6", - "ratios.0.1.parquet:md5,7af8e11d4236e66ec955bfc91caa6fac", - "ratios.0.10.parquet:md5,6ffffd19518207b53b44e03f961eb2e9", - "ratios.0.11.parquet:md5,5c80fd5580a2552b4913d8b14cf1dc0f", - "ratios.0.12.parquet:md5,127fb52cd8ac2f69db1b67bcde9e4848", - "ratios.0.13.parquet:md5,7730c834668d2e4f100ecaf0a7cd7ab2", - "ratios.0.14.parquet:md5,ea36f4387a0d994c52d4891a1ae176f6", - "ratios.0.15.parquet:md5,db8e85634dce84a7d551aa1aed1471fc", - "ratios.0.16.parquet:md5,d65f4190085c7479d6e0a01aadecba28", - "ratios.0.2.parquet:md5,7e7f759e9d8664bcf3b18976b241bb38", - "ratios.0.3.parquet:md5,d2a7a077b5fcd53f1977e43150104d1c", - "ratios.0.4.parquet:md5,2686077d876375c3f1113d9e8afd48ef", - "ratios.0.5.parquet:md5,e5222bdbf5e767a932301518b09a9acf", - "ratios.0.6.parquet:md5,c69356effc1da780abaaa5a56474ac82", - "ratios.0.7.parquet:md5,42686840fc8841b627a3d92a77b92a95", - "ratios.0.8.parquet:md5,713c07013f4cacb4fa3f3db6291e48fa", - "ratios.0.9.parquet:md5,be56d666d72712799b1504b00449a45a", - "ratios.1.1.parquet:md5,ec0e5a367619e3d22bbc33611ca52b67", - "ratios.1.10.parquet:md5,9341746871f8c6ca773d5a1b9702ec35", - "ratios.1.11.parquet:md5,497642768c01b7a286992f26dfa252ce", - "ratios.1.12.parquet:md5,7ee7421c56211d93adb11868d6372939", - "ratios.1.13.parquet:md5,2fe3986abf0cfea3d5753741dd2fd783", - "ratios.1.14.parquet:md5,6e657f24d5b23ad230cc5b485da23c6c", - "ratios.1.15.parquet:md5,b7bbcf04425751f0723780e296336ddb", - "ratios.1.16.parquet:md5,548e8892eb7d69c875bf987f88b12f2c", - "ratios.1.2.parquet:md5,14a93f127cedff8d76dbe3bcd0baa36e", - "ratios.1.3.parquet:md5,f2fefe175b9624fe0332c6c0b39944df", - "ratios.1.4.parquet:md5,16403a8655ded43018cfe045007c2a73", - "ratios.1.5.parquet:md5,84b4e4d49cf1b1e946886a90186a825f", - "ratios.1.6.parquet:md5,caeddd18b9efdf6127b7cea00522c31c", - "ratios.1.7.parquet:md5,dab3faec99e46dbd3f5ed83c70d1d90c", - "ratios.1.8.parquet:md5,07543c020294f66995f8c7e3efc77728", - "ratios.1.9.parquet:md5,42009140ca8b6977cce3e3bd8800f419", - "ratios.10.10.parquet:md5,e9e146c7ff6779c36da0858f0145a2f7", - "ratios.10.11.parquet:md5,843a8bd51a4421de86120912bfc884c1", - "ratios.10.12.parquet:md5,5a1e858eb8483141deb246dd16922913", - "ratios.10.13.parquet:md5,1b78a23d0c02b51396a074da8978c3db", - "ratios.10.14.parquet:md5,df42a0df447438b8b17d1e0670ce4260", - "ratios.10.15.parquet:md5,046657b57e56c2cbda094d36dfb02163", - "ratios.10.16.parquet:md5,ca647748df2ef7bf22d620c3f95c5bc3", - "ratios.10.2.parquet:md5,925bb322c28181be16e01164ed7e37e0", - "ratios.10.3.parquet:md5,c77f9fbaefbbf6eedf30c268045f7fa5", - "ratios.10.4.parquet:md5,97f8882d2de16b46da6baff729727183", - "ratios.10.5.parquet:md5,b0a2dec08c1577255ba3bd204787488c", - "ratios.10.6.parquet:md5,73fa3845a1aefdf5c7da42ae1e6a54e8", - "ratios.10.7.parquet:md5,66212a87260e1ed357d2816a1452ac47", - "ratios.10.8.parquet:md5,ae400cecd68a636b7b235669b2de4c10", - "ratios.10.9.parquet:md5,bebeeeb1bb03016b4e1ed09ab829cb47", - "ratios.11.11.parquet:md5,36176d02607dac19baaf40b3768e7110", - "ratios.11.12.parquet:md5,fda19923313abbb26790797bda1e5bd0", - "ratios.11.13.parquet:md5,fb0b5e3a9b2b9e6025c7ef466b5f366f", - "ratios.11.14.parquet:md5,a37face0a8388fe416252f897603ffb7", - "ratios.11.15.parquet:md5,80b952f9ade0e7bbe5af33ed2f01a32c", - "ratios.11.16.parquet:md5,db7e2bfe6d7c513ac4b2fc88f2e4b595", - "ratios.11.2.parquet:md5,3467389c29749422573d228ee4489bfb", - "ratios.11.3.parquet:md5,560bb56a6393bc2e0b44f45b649ab189", - "ratios.11.4.parquet:md5,2fbcf5460aaae803a02e9a7148207f64", - "ratios.11.5.parquet:md5,b1785dc5c196ac31866a891edf0f7ab1", - "ratios.11.6.parquet:md5,7bb397eb08c842e729a4dd54cc9e3f47", - "ratios.11.7.parquet:md5,e5721a9e48952759ab11672babb8fc7e", - "ratios.11.8.parquet:md5,7522cdd752e2f1efbd51c997a38d8d85", - "ratios.11.9.parquet:md5,40740c54ac5ead214ca2bafba86c7a4f", - "ratios.12.12.parquet:md5,7b8bfd703c4662fd908290a82ac00f70", - "ratios.12.13.parquet:md5,156ddc68093be2492398d1ef7d997595", - "ratios.12.14.parquet:md5,44ff915c18d65e4cce7ff757e0db3cd0", - "ratios.12.15.parquet:md5,8e8b96e66dd19ab7ebfe2af3257d2e41", - "ratios.12.16.parquet:md5,f820dbe54d70bc24888a6a9c84031447", - "ratios.12.2.parquet:md5,618c02d7c2b9ac3f9ad58606f2c94f04", - "ratios.12.3.parquet:md5,1a7ec23f4e61a404b42153d1e591b98a", - "ratios.12.4.parquet:md5,eac3b290600eaceaddf15cfdb888e9d1", - "ratios.12.5.parquet:md5,68b84e0a27328c887cfab8934484e6b3", - "ratios.12.6.parquet:md5,89ee7fcfa1db720f78f25e45b9e246de", - "ratios.12.7.parquet:md5,df48def02992f838c587ce991468906c", - "ratios.12.8.parquet:md5,8df792a8bed292b93dfdfb96d274aa92", - "ratios.12.9.parquet:md5,a99b6107f6c4f2ec5c7d56c8110f7460", - "ratios.13.13.parquet:md5,5d5f4eee0230969df74a399af7a497cd", - "ratios.13.14.parquet:md5,1c0270fb676760dcb79c2bce06a36142", - "ratios.13.15.parquet:md5,37a17f3a3727e9bfc4431a325caee0fb", - "ratios.13.16.parquet:md5,71c2ed1fb614e8ee912d01110232a40b", - "ratios.13.2.parquet:md5,8653ba639912aab0c954edb4e5e15919", - "ratios.13.3.parquet:md5,88d07a234489857d167f6f872eae5b11", - "ratios.13.4.parquet:md5,ef26cc0a80279b7f0a7bc7327020d70e", - "ratios.13.5.parquet:md5,212f06af08076e7ac12729289128b5f2", - "ratios.13.6.parquet:md5,0b6fc56a7d69628ac24f00379ceb6ab8", - "ratios.13.7.parquet:md5,bca91bdc92a2ab5bfba7cf444b212b51", - "ratios.13.8.parquet:md5,1892d90b46ad394e7499c60dfc9e832f", - "ratios.13.9.parquet:md5,c52a5ac166f333b2a551956067d4d895", - "ratios.14.14.parquet:md5,db0d10a29abf198c2543fa408761cc10", - "ratios.14.15.parquet:md5,9ed98af50494eac98b90a1a8695ca909", - "ratios.14.16.parquet:md5,d53e2b25d03a595bfe934d3e90548a31", - "ratios.14.2.parquet:md5,dcda662a066d65f8ec64eba714d2569b", - "ratios.14.3.parquet:md5,bb88047558e1805edd2b8b0fb07c2d82", - "ratios.14.4.parquet:md5,5caa19f1290bcba7e58060570acf1abf", - "ratios.14.5.parquet:md5,c7ec4aafafcea3089b6a8ff7c53b64d8", - "ratios.14.6.parquet:md5,19b3ecc90f28a467cb3221a2fb6a6b9a", - "ratios.14.7.parquet:md5,097fa4c610288b023e1675162522e812", - "ratios.14.8.parquet:md5,daac142b47596223909740abb0ade1a1", - "ratios.14.9.parquet:md5,1ab7ca2cead2b993268c97a71baaa4bc", - "ratios.15.15.parquet:md5,5b632e0bd1e525e1385b4ba58527321b", - "ratios.15.16.parquet:md5,e529537ab99108e86c33b51adce26783", - "ratios.15.2.parquet:md5,204b4b1be2bf3d031629d76c063d431f", - "ratios.15.3.parquet:md5,7bcc3f85ab24483b6a989aed0f0020c3", - "ratios.15.4.parquet:md5,e90af424ca5bed5bb60a485b7a8ffd60", - "ratios.15.5.parquet:md5,b6fa5ff144f89e68ba2f2af156f27835", - "ratios.15.6.parquet:md5,22061738d4790957a03d4511fd760a8f", - "ratios.15.7.parquet:md5,f302453d1b4901e1c80ab4a07051dd74", - "ratios.15.8.parquet:md5,12674658ce09535aad75ac2dc3ff532c", - "ratios.15.9.parquet:md5,2419458e5ae5f680a4cc4d85f8d62b22", - "ratios.16.16.parquet:md5,61695d3d91a6a1078c7e82adb426aed4", - "ratios.16.2.parquet:md5,5f7ac087a3d090e3fffc65fabbb24c0c", - "ratios.16.3.parquet:md5,131129091bb689a518374aedbd9ff3ca", - "ratios.16.4.parquet:md5,552e4c25759e7d403eaf223d8093194a", - "ratios.16.5.parquet:md5,2b55de4c99e0b272421ffb2281fde1c1", - "ratios.16.6.parquet:md5,080a9d3c5540e5de0d727845ddc6ded6", - "ratios.16.7.parquet:md5,663b1d45e3d60a79149118c7c94c4b45", - "ratios.16.8.parquet:md5,1e09757bff7dfc8419571e5f5a1ff9a0", - "ratios.16.9.parquet:md5,ae735c5c054ab823404d1772463b7b7f", - "ratios.2.2.parquet:md5,63fac44ece88fd443d1483cbeba73072", - "ratios.2.3.parquet:md5,e825c4c94a74875ec35dbc547dbf9628", - "ratios.2.4.parquet:md5,95ae91d43579cfa43a2da4be3b866595", - "ratios.2.5.parquet:md5,511f5f94b767570e5859d19e44590889", - "ratios.2.6.parquet:md5,56c1859fc02eeb0e555e9e92b5237a36", - "ratios.2.7.parquet:md5,5c4f45de615c52db6addfc6cfd65ad47", - "ratios.2.8.parquet:md5,3cd34e8912caf86f8a4a012d38838c17", - "ratios.2.9.parquet:md5,faf7eea2b8baa37132472b1ba49f443d", - "ratios.3.3.parquet:md5,76f440e48b0c25c4995e89fceb6a993a", - "ratios.3.4.parquet:md5,d7ef79ea3d022a02bd3614b69936021e", - "ratios.3.5.parquet:md5,1d5a0d8f2e13424376748bb3d1a7a3bf", - "ratios.3.6.parquet:md5,be8dab0a4a28229ebb716006721c30d0", - "ratios.3.7.parquet:md5,af2ee8da0a77c49f1ed5eb59a6a2f3fc", - "ratios.3.8.parquet:md5,04b227af993ebf74d8b18fcfa0c7b608", - "ratios.3.9.parquet:md5,623903b1402fd1af6ac783b00ef3c159", - "ratios.4.4.parquet:md5,a08007710a0ef6a37e917ca3747bad1d", - "ratios.4.5.parquet:md5,e592677ccf5b184c7622504206ef0d30", - "ratios.4.6.parquet:md5,6e43b1b0cd66ec68cf4dc1832ddea5a6", - "ratios.4.7.parquet:md5,87521100a3feea742124e453f1a70d99", - "ratios.4.8.parquet:md5,4cf05fc4accc86d6397d3dd7f742df38", - "ratios.4.9.parquet:md5,3cc9d27aa578e3f55ba86f4c0b458bd6", - "ratios.5.5.parquet:md5,ea468f80be61cdeea2997747dd480b70", - "ratios.5.6.parquet:md5,02a7a8d8a5cf8bb37da934c9f8c2e5a7", - "ratios.5.7.parquet:md5,52cbf872c6f03e435be1a9d9d4a0cb1f", - "ratios.5.8.parquet:md5,c1939331b44d7744630e1ba78933928a", - "ratios.5.9.parquet:md5,d6f1b39b43472c96aa488c275931eaf2", - "ratios.6.6.parquet:md5,1efa2e29dd6767dc091a9cc05c8a260c", - "ratios.6.7.parquet:md5,e72dddcb5a8c0df89a228fefbfce6056", - "ratios.6.8.parquet:md5,347bcc650582a383d6fb1831729f8bed", - "ratios.6.9.parquet:md5,0e39f2297f79cc66d45b810b52144819", - "ratios.7.7.parquet:md5,ddedf622177fab25bf41c1f0cbc6bc2b", - "ratios.7.8.parquet:md5,585a089e6f34ca5a798d6cbbc7b78944", - "ratios.7.9.parquet:md5,a7a57928a5f9b08aac32132715e4a52d", - "ratios.8.8.parquet:md5,5589622b9f578f5fb944e096c5150d3a", - "ratios.8.9.parquet:md5,afa0680c4ed261ef14b6e2d4985b0b40", - "ratios.9.9.parquet:md5,50aa54172efd0ec34d0742676ddadb64", - "candidate_counts.parquet:md5,84ef96b59bebdabc5824a46ca58c5cd5", - "E_MTAB_7711_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", - "E_MTAB_7711_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", - "E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.csv:md5,3856796c675c7ee3bb6975185f1b869b", - "whole_gene_id_mapping.csv:md5,87c58803a087a768eff2403b40868614", - "whole_gene_metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", - "count_chunk.0.parquet:md5,945ce07af2f6912bc1b04a042ffa4df3", - "count_chunk.1.parquet:md5,f6d15b5960cd58a69ac38c524bfeb44a", - "count_chunk.10.parquet:md5,3f3884f92ca0568ee284ac8d2ee5756a", - "count_chunk.11.parquet:md5,bb694537b13260c63aee27c418fa987a", - "count_chunk.12.parquet:md5,a3f609a812dbe2eeeb1c1fa4deed4bd6", - "count_chunk.13.parquet:md5,d1273e61b59f6edcb292449d4fae3d7d", - "count_chunk.14.parquet:md5,2eb7ecf00cb497883bc81c5cc0ebacc2", - "count_chunk.15.parquet:md5,969d5fb8c8097e36f28b72d29447c33c", - "count_chunk.16.parquet:md5,097c0a41c635a06a81b63deea1c6f098", - "count_chunk.2.parquet:md5,c02192585bbbb94228889f25d1079eb5", - "count_chunk.3.parquet:md5,a935b0bff8e1aba538253fb2d282a815", - "count_chunk.4.parquet:md5,462a8505afe8b705011e56f0532bdfac", - "count_chunk.5.parquet:md5,e499c929f4067ef318ccb55c2168ef34", - "count_chunk.6.parquet:md5,7bc40c9b0a132ddf34c55b5b4762f29a", - "count_chunk.7.parquet:md5,01e560ed2446670049baf47838526663", - "count_chunk.8.parquet:md5,6be163f20e7032633dafa9060b19a364", - "count_chunk.9.parquet:md5,7db8d9e3cd0fbd41fcb2edb2dd3c228d", - "all_counts.parquet:md5,1736df00d0caf241236e64d6da98b925", - "all_counts.parquet:md5,1736df00d0caf241236e64d6da98b925", - "whole_design.csv:md5,3e7748b54a0c25c008d9bd2ddbf1bf00", + "E_MTAB_5072_rnaseq.dataset_stats.csv:md5,408b24a208933eba5f442580cbbf165b", + "geo_failure_reasons.csv:md5,a8b10197854e9ace4e90b21b3f2f1470", + "accessions.txt:md5,561a967c16b2ef6c29fb643cd4002947", + "selected_experiments.metadata.tsv:md5,385b4d7ed40829f1e764857e805d6dc4", + "species_experiments.metadata.tsv:md5,385b4d7ed40829f1e764857e805d6dc4", + "E_MTAB_5072_rnaseq.design.csv:md5,a1f33d126dde387a2d542381c44bc1f3", + "E_MTAB_5072_rnaseq.rnaseq.raw.counts.csv:md5,c41c84899a380515d759b99eeccfe43e", + "ratios.0.0.parquet:md5,405cd590509b6ffdd110f912222159d2", + "ratios.0.1.parquet:md5,422000f89ed0abedbd7f8b4017161535", + "ratios.0.10.parquet:md5,bb316fa0b9865dfffe747024443c059f", + "ratios.0.11.parquet:md5,eb36d82ada1ada899f80330dbd9a9e5a", + "ratios.0.12.parquet:md5,76dc7a3e2bd38c4f9d0c6fb5dbd6c3ac", + "ratios.0.13.parquet:md5,550e521560552ec799d0c09b10678890", + "ratios.0.14.parquet:md5,1145da4038b87582acddec6406467fa1", + "ratios.0.15.parquet:md5,b4d59d365a6bd5e8ef1a5d8f01cb7aac", + "ratios.0.16.parquet:md5,a16d505a5c022e2c5b3bfeb056f42155", + "ratios.0.2.parquet:md5,3bf28e74f74e119b2695adf3de551cc4", + "ratios.0.3.parquet:md5,cf55a5fdced77f76d42a1143a03124ef", + "ratios.0.4.parquet:md5,857bc2beaecfc6c1e0fec999ecc1476a", + "ratios.0.5.parquet:md5,cb07ee4affd907f3d16829e08954ae34", + "ratios.0.6.parquet:md5,ae53fe36ef593146b015fbdbb7be8bf6", + "ratios.0.7.parquet:md5,d47d8ba25f0edea0985da08ef3d601da", + "ratios.0.8.parquet:md5,17e24fec2f7229238a6f0dbc8705feaf", + "ratios.0.9.parquet:md5,f958751ed09623544d6a15620f876c63", + "ratios.1.1.parquet:md5,7b53e0104015ab8dc01c3d13527bbb85", + "ratios.1.10.parquet:md5,a3f4f8391668fd0723715c1ad7d5d521", + "ratios.1.11.parquet:md5,b2283c7a1c94f945d8faa9df83bb6fe4", + "ratios.1.12.parquet:md5,a5bcafad29f7a88f3098ca4678d97f8d", + "ratios.1.13.parquet:md5,f2945f24639146afddb43ef9b02cd05a", + "ratios.1.14.parquet:md5,3068d035a5d264683912e6398138a6f8", + "ratios.1.15.parquet:md5,fa3ed2c57e601c88b6dfcfd645734de9", + "ratios.1.16.parquet:md5,d99121ac26ec81b474e06971257a81f5", + "ratios.1.2.parquet:md5,ae7f696da83019ece1acc0aece6443f8", + "ratios.1.3.parquet:md5,23c18c27fa602fd6101769e69ce6ece3", + "ratios.1.4.parquet:md5,867e6b18f597fc1fe8e35ccc36704595", + "ratios.1.5.parquet:md5,5c41b2d632aa1d6fb2a3d2a8fd4148b4", + "ratios.1.6.parquet:md5,67e36407b266528bda7bfde5d847068f", + "ratios.1.7.parquet:md5,64a405bbcb23cf0e879ecb72cdc3b013", + "ratios.1.8.parquet:md5,604b3c093dc44b3e46bd62d7cc52008e", + "ratios.1.9.parquet:md5,4ca2b8858dbd711fa2474a7fc14a4b6d", + "ratios.10.10.parquet:md5,a2db1f83e81584fe1f64d8b117963556", + "ratios.10.11.parquet:md5,9295b90ade210e2bc3470172b920b233", + "ratios.10.12.parquet:md5,b7376d51719795d650611219656a90ef", + "ratios.10.13.parquet:md5,b4eefd83901c4776c8c131e3ff185676", + "ratios.10.14.parquet:md5,d7589a490b45710eb3ed74aaeadc18c3", + "ratios.10.15.parquet:md5,81257c2b9cca1a896a9a9ad411618bb8", + "ratios.10.16.parquet:md5,e6557f71aeb4bbbae16bc635aee3cf7e", + "ratios.10.2.parquet:md5,202cdf8eeb5b4fe6f18e2dfa787fa4ae", + "ratios.10.3.parquet:md5,f44e1a1a3cdd33459a73ff56d27ba7ea", + "ratios.10.4.parquet:md5,1903cb8be4ff1cc776a8871d3889be19", + "ratios.10.5.parquet:md5,dcd7414ba98689f8ef556c4c257e5814", + "ratios.10.6.parquet:md5,22927faa69b228fbc9cdb5bd8bd6c652", + "ratios.10.7.parquet:md5,8c28697499c50434e16e2d6a43c12f7f", + "ratios.10.8.parquet:md5,fbdc1fa2bd8b5a4cf1768c953dec4891", + "ratios.10.9.parquet:md5,deacdbf8e48988760d871a5a0be03369", + "ratios.11.11.parquet:md5,94424355bdeba61b40d2c93991bbaf6a", + "ratios.11.12.parquet:md5,401e83de0874ce1a69c07024a0478218", + "ratios.11.13.parquet:md5,c95029cc153845024e1696f95d39a6d4", + "ratios.11.14.parquet:md5,bc9403d30c0efe7bef7e07e256d2b619", + "ratios.11.15.parquet:md5,a6038b92df76732a18e12685719a9336", + "ratios.11.16.parquet:md5,39f64ad00730b6ca9d9addf366120072", + "ratios.11.2.parquet:md5,444d3372efbb7d09c57eab0a1af273a6", + "ratios.11.3.parquet:md5,89897da97b78cbf64e688c3d977d9f75", + "ratios.11.4.parquet:md5,ae9dcace2aac1ea83a4ef923c85a97c2", + "ratios.11.5.parquet:md5,228327a8ec587240f3ff83ce571042a7", + "ratios.11.6.parquet:md5,45319c62c09a476afbae5243b7f52918", + "ratios.11.7.parquet:md5,f96b8486134e5466566dc0e738ecb9af", + "ratios.11.8.parquet:md5,8f64d2be604280326fafa9346d033fa0", + "ratios.11.9.parquet:md5,74ea1c5ac78c4d5c0c41c7aca6b7d7f3", + "ratios.12.12.parquet:md5,d6d160877d1b0f8a0865a776056c03b4", + "ratios.12.13.parquet:md5,c68f4f035b589116fd530a7282742e0f", + "ratios.12.14.parquet:md5,1078ccca021b5913671380a732752758", + "ratios.12.15.parquet:md5,6e66e615298079ab166d357fcbd44400", + "ratios.12.16.parquet:md5,bccf17ed400e85224d3853977e496209", + "ratios.12.2.parquet:md5,0a8ff0fda5a4b3a922ccfdb61e3c1e42", + "ratios.12.3.parquet:md5,d3703721b93dd26570fd537ca9bb36c0", + "ratios.12.4.parquet:md5,fc587ea0c56d77e1dac6b2a9d0e21c19", + "ratios.12.5.parquet:md5,9407c12bc3fe995caf774570c887ca56", + "ratios.12.6.parquet:md5,c55f2b1eea563276b9a42ef64c776313", + "ratios.12.7.parquet:md5,f8e5256b1d613094b2563ea28e4ae0dd", + "ratios.12.8.parquet:md5,d0b8a24efb562be44ec3b6b54b4b7ad8", + "ratios.12.9.parquet:md5,7c164d3f8ccb7656f9b027223359efd0", + "ratios.13.13.parquet:md5,1930fee15bd7b60fdb3c207691ae639b", + "ratios.13.14.parquet:md5,804faff1979f837517410962b89ae85f", + "ratios.13.15.parquet:md5,091703e6b7133bf0fbcda1e6def53c80", + "ratios.13.16.parquet:md5,8a0a5e1492fbfa6c7defcc54f801dca9", + "ratios.13.2.parquet:md5,ec23f6ca9f0d726391b8022b747445de", + "ratios.13.3.parquet:md5,eb39f03a52eebf693f0821199d7b8f6c", + "ratios.13.4.parquet:md5,c8d4b2418e80fb9a4487b880682e420d", + "ratios.13.5.parquet:md5,2dea98b249322ec999616fbafa457dc6", + "ratios.13.6.parquet:md5,05567b323f94d300ed759bca7662a0ba", + "ratios.13.7.parquet:md5,e0a3a61f0ca707df4d9371459c510e97", + "ratios.13.8.parquet:md5,ccc815cdca00254863f1c6aecd13ab8c", + "ratios.13.9.parquet:md5,e8c513b87b20400d142ea2d259ade363", + "ratios.14.14.parquet:md5,ea8bf5c59d8db2ecc63aa4ad1692afb0", + "ratios.14.15.parquet:md5,ac53fc7f4eb59522bf96c72c3c4612ed", + "ratios.14.16.parquet:md5,2d84a0b748953a93a22dd892599afb11", + "ratios.14.2.parquet:md5,5c7e5d219506871719ff6a9234b31e93", + "ratios.14.3.parquet:md5,14ca78d4706ea4997d2a631b80b612a1", + "ratios.14.4.parquet:md5,5b424357d7976f48c8ea4893e91c75f2", + "ratios.14.5.parquet:md5,fb23c438af8f381cc809277084d9c0f7", + "ratios.14.6.parquet:md5,42a8d9d274bd7b31d9d8579ed9555835", + "ratios.14.7.parquet:md5,e177783b01f3b73ef582b248c6c7a343", + "ratios.14.8.parquet:md5,de9bce280597e9f2e426329d80754ffe", + "ratios.14.9.parquet:md5,33fd335e98b556fcb3542aea84084555", + "ratios.15.15.parquet:md5,3fc88687792ea38d7d30b9375d6a8f09", + "ratios.15.16.parquet:md5,a752a2fb3c2339c2520a53ea302fdce7", + "ratios.15.2.parquet:md5,18bc5c3429b6f1f41527baffe7215bfd", + "ratios.15.3.parquet:md5,addf16d3bdf7cef7db80ae163bd02242", + "ratios.15.4.parquet:md5,2381031d109a7f30e53893b7d6bc79ff", + "ratios.15.5.parquet:md5,e63862575d8d89a042c3e5e9476a7aa6", + "ratios.15.6.parquet:md5,4519fdb3b0c9f40d535540d0cab5134e", + "ratios.15.7.parquet:md5,533285de38fcb85a5665df522282d84f", + "ratios.15.8.parquet:md5,d7655554d0b75c16fbb1724629751a32", + "ratios.15.9.parquet:md5,228b9c95cd843efd3dfecf5f2fb6c2d4", + "ratios.16.16.parquet:md5,af6f4cf815fc94279fb8eca946300999", + "ratios.16.2.parquet:md5,67772b84ce8250d6da541374de7b3c0c", + "ratios.16.3.parquet:md5,40074f34554fd6867b8ea418e14e8bdd", + "ratios.16.4.parquet:md5,c35d20b1e405d0caebd1b052c27f40b7", + "ratios.16.5.parquet:md5,64a6139c4387507823a157ca7a633a7b", + "ratios.16.6.parquet:md5,af8c03de7516955f2e6b322b4e791342", + "ratios.16.7.parquet:md5,9fea140cb4bf24dfceeb3877ba79f713", + "ratios.16.8.parquet:md5,4c6710e78ddfe5b884bddc68075469a8", + "ratios.16.9.parquet:md5,bcc9f266e8b4a413ed5891c8f9004c15", + "ratios.2.2.parquet:md5,c7a8cfad502bba4584464886765669d6", + "ratios.2.3.parquet:md5,a2b971819baba756a59f54bd01a8f717", + "ratios.2.4.parquet:md5,cada010249b816fa5af1a83cbabb58e8", + "ratios.2.5.parquet:md5,d3166ea2f6007f7daebdd6a2f01f9af8", + "ratios.2.6.parquet:md5,12cc3dd399c46404dde0992758bfc12d", + "ratios.2.7.parquet:md5,d0736c1d14f36e22470fec6aaa12558a", + "ratios.2.8.parquet:md5,8c1aa3b16c8e9cefaa8c5ea3eb04f071", + "ratios.2.9.parquet:md5,00ecc1b89e5606f62a674656a576ff23", + "ratios.3.3.parquet:md5,69e387ec438162a7276ddbc1c8ad4018", + "ratios.3.4.parquet:md5,464795964bff4de3267b4849519dcca7", + "ratios.3.5.parquet:md5,d51f9b33fcbc5764a1a5f129e9c39e3b", + "ratios.3.6.parquet:md5,97d240a5624a341af59fcac729fdfe70", + "ratios.3.7.parquet:md5,57fb1098fa56c7d4ad2ba82e3993d774", + "ratios.3.8.parquet:md5,5b672e3976a735100f50e098b4062750", + "ratios.3.9.parquet:md5,0e23b6d7a9c33324ad1ccadb36a5da68", + "ratios.4.4.parquet:md5,d60a5f2789db5ac57a7f8faa091bb20c", + "ratios.4.5.parquet:md5,60eebdde4c11d7f32281f65745d2525f", + "ratios.4.6.parquet:md5,370bede0e31df9914bf1aee7773c2eea", + "ratios.4.7.parquet:md5,b33605e9c8da1021c926af7c6fd0620d", + "ratios.4.8.parquet:md5,2a68fcdd641475c74175f0fa79c7d3d0", + "ratios.4.9.parquet:md5,3845d9a7ba740c90a4c0c9e9d949b2ce", + "ratios.5.5.parquet:md5,13e1c2fa249c78521bae4e2f55f990a4", + "ratios.5.6.parquet:md5,7429980f1826dc91d35768ae028de870", + "ratios.5.7.parquet:md5,856d74554d29a0647e68ad83ee978b34", + "ratios.5.8.parquet:md5,bd4450c0695bf3ae9b97b78e7f2ce5f3", + "ratios.5.9.parquet:md5,359a82ad0f012c860ed9642d8aca69e0", + "ratios.6.6.parquet:md5,1f6aeb5dd66ba711d794e3b222311897", + "ratios.6.7.parquet:md5,94ccc05575d8a9c1a3e6d5efd4acae53", + "ratios.6.8.parquet:md5,bc576a0a98d6363a87da4747b350c2ff", + "ratios.6.9.parquet:md5,3c559259321928f055aae93a9eea1929", + "ratios.7.7.parquet:md5,2e60bfcc6557ff6a044887de00c8fdc8", + "ratios.7.8.parquet:md5,a2da6ceecf34eb4fdd94d9dbd32b901c", + "ratios.7.9.parquet:md5,00b3bfa42b5aa45d7e5ce63482c32674", + "ratios.8.8.parquet:md5,0f56ce921fe189e254a2d1975a67177f", + "ratios.8.9.parquet:md5,f90e4fca2c5e91a6762164c90f52c905", + "ratios.9.9.parquet:md5,e088b0dd1b55d37fcbc4885101a4b08c", + "accessions.txt:md5,624ae1990d8fa2dccb2c5b74c66f4956", + "geo_all_datasets.metadata.tsv:md5,e80c4bc5a3a16a5db7678979e3ab3cb3", + "geo_rejected_datasets.metadata.tsv:md5,d8e091bf86fe483af6d525c9127162a7", + "geo_selected_datasets.metadata.tsv:md5,4dc869829149eea9fba6ce4180a88e44", + "failure_reason.txt:md5,2631bb8c3ae982a1b869107f8dbfa107", + "candidate_counts.parquet:md5,14de589fd41b204db541dc58433b0d7b", + "E_MTAB_5072_rnaseq.rnaseq.raw.counts.mapping.csv:md5,f103d18b88f8329f5fc8e7082d251deb", + "E_MTAB_5072_rnaseq.rnaseq.raw.counts.metadata.csv:md5,8ec8f92cc8f81ca44c4c737251d3bd4a", + "E_MTAB_5072_rnaseq.rnaseq.raw.counts.renamed.csv:md5,e1eb2368e24142693643fb129d04f710", + "whole_gene_id_mapping.csv:md5,8824265badf80ba59db63b6dfa8672dc", + "whole_gene_metadata.csv:md5,8c89bfe0e0320d3354e78ba622764c88", + "count_chunk.0.parquet:md5,bd1ea826d49befb0cb5454948d920c39", + "count_chunk.1.parquet:md5,fd97dee3ae911d27bd47d4a840f46830", + "count_chunk.10.parquet:md5,7c340633da6fd513d6a20406b0620ba2", + "count_chunk.11.parquet:md5,9a39ccd89610ea06e14f690c251598cb", + "count_chunk.12.parquet:md5,03883954d0adaa635898288b4a72354a", + "count_chunk.13.parquet:md5,0df547c7277b5bc02dcc11324932db7e", + "count_chunk.14.parquet:md5,54e7169645b834bf53fd8dd36655ab88", + "count_chunk.15.parquet:md5,75207fa3253bf889333028cd92171304", + "count_chunk.16.parquet:md5,c5a678689803a6ef5ffa65f8d99f8112", + "count_chunk.2.parquet:md5,14d317611549f3dafcf8ceccd60bf5c4", + "count_chunk.3.parquet:md5,f39572e7750909ed6853c867e08b1518", + "count_chunk.4.parquet:md5,40e302da7eb770880fb6b7f6edb3ee3b", + "count_chunk.5.parquet:md5,ccbb86356291b99e9ffe40fc599a5aa8", + "count_chunk.6.parquet:md5,1f138aa451915fe477285246d79ebb0b", + "count_chunk.7.parquet:md5,dec2d4c03b6d87cf55fc54fcc6e2d97a", + "count_chunk.8.parquet:md5,3d778e59ed371124b0f569377f5ebeba", + "count_chunk.9.parquet:md5,41d26735819b933d1d95f4dcf01d12f7", + "all_counts.parquet:md5,6e1db3f9aaee4e528db8e539e381b03f", + "all_counts.parquet:md5,6e1db3f9aaee4e528db8e539e381b03f", + "whole_design.csv:md5,c9bfd7bc8ca365222e03e67eb24b9a76", "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", - "multiqc_expression_distributions_top_stable_genes.txt:md5,b0a72e2fe2bed3bf3a9f90419462a420", - "multiqc_gene_statistics.txt:md5,12ca1fa8048b26330738fb1dd41b1b4b", - "multiqc_ranked_top_stable_genes_summary.txt:md5,09c98c33edf61c2c71a08d51ec80d2bd", + "multiqc_eatlas_all_experiments_metadata.txt:md5,da7b03757b1d128f598e1b2b04925ca9", + "multiqc_eatlas_selected_experiments_metadata.txt:md5,da7b03757b1d128f598e1b2b04925ca9", + "multiqc_expression_distributions_top_stable_genes.txt:md5,0aba3d9fda34be5c31a091e17a42afba", + "multiqc_gene_statistics.txt:md5,376f2ed44f3aebb0f9dba66b3dffe31a", + "multiqc_geo_all_experiments_metadata.txt:md5,faf123e1f8ec253f1ed26d274b764eab", + "multiqc_geo_failure_reasons.txt:md5,5fef39d6bbcc18c4afafb1606517d335", + "multiqc_geo_rejected_experiments_metadata.txt:md5,f06159683fbc74f10ae5518a8325aae5", + "multiqc_geo_selected_experiments_metadata.txt:md5,c20dea1de9d7637fed8c58ed2b4719e7", + "multiqc_ranked_top_stable_genes_summary.txt:md5,ae54bc58d86a6f95bb3c5e525cd209eb", "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030", - "E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,f7374c86950c01bc6a6b8d8fd415cc8b", - "E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,424ab3200c10539953a74e6f789590a0", - "std.0.0.parquet:md5,f7f1f584fad6e2fb563a256837db16b7", - "std.0.1.parquet:md5,27dfdbac92ef850e34bca6dec964af99", - "std.0.10.parquet:md5,cb4245afac34f688a69331771928601a", - "std.0.11.parquet:md5,2a64bc3da8293d577ce0d768e0e293e9", - "std.0.12.parquet:md5,66963e19755f3921664a008ec60f77e0", - "std.0.13.parquet:md5,da5d199606ea714ab20623a6e798c9c5", - "std.0.14.parquet:md5,4183b3174dcd859ba9a134de40646c94", - "std.0.15.parquet:md5,c4d46549f4fb3ea16b2bbc8316fb1e86", - "std.0.16.parquet:md5,21920021e64e23165a169a34e20923bd", - "std.0.2.parquet:md5,18ae2ca9153343ee7d63bcde0568d360", - "std.0.3.parquet:md5,b21ba0b1790f57e1f617f94ecc6b3dc4", - "std.0.4.parquet:md5,b50352dbca9833e4f4c4fe98eeb59937", - "std.0.5.parquet:md5,7e9ba5aaa8627fc9b5a7af2d6ef7a19e", - "std.0.6.parquet:md5,09ce3da351f69293c9ab3bc6398e5027", - "std.0.7.parquet:md5,44316323b58e59b7b2f9ff58f4d85c94", - "std.0.8.parquet:md5,8882523dff550234de6e3cdec277d5d1", - "std.0.9.parquet:md5,a6992acdc47dd4ccfd6b608d65bd8f19", - "std.1.1.parquet:md5,1b15ccf947fb8fc5a27e308138644a82", - "std.1.10.parquet:md5,11f8f85f8c8d5843131ff37c929fa0a4", - "std.1.11.parquet:md5,16a10598086e9acf0b450d878f60a9af", - "std.1.12.parquet:md5,712d9eba2a240a75ea7418bd8fcca2b1", - "std.1.13.parquet:md5,18949eea624878d245d5566bf2c52eee", - "std.1.14.parquet:md5,933b7117dbbfd550db1a2c249251bb7f", - "std.1.15.parquet:md5,cf9ba2aab349f768231790e9a38ae0a7", - "std.1.16.parquet:md5,e1c982ce291ce93309b7b33543a913ae", - "std.1.2.parquet:md5,71aa5252c1c5570be7d863805da8557b", - "std.1.3.parquet:md5,f41a5c6fd9c20be6e2526271a1d2fea8", - "std.1.4.parquet:md5,15b0ba48a0359b53b91071d78fb619d3", - "std.1.5.parquet:md5,74e83ebd64b4659e50fc2034aedc3337", - "std.1.6.parquet:md5,18a691f273918b31e1ff3c45de7a33c0", - "std.1.7.parquet:md5,cf14eb2142285c6bebafbf255804dbce", - "std.1.8.parquet:md5,4eb995adbfacfc3efb0394f87b1e0fef", - "std.1.9.parquet:md5,2f8b813eb596c0c8499d335136b824cb", - "std.10.10.parquet:md5,68dc78cb4518a1fc3deead1dab9c4e98", - "std.10.11.parquet:md5,e3d0d1d54a452427afee44bb8f8f7bf7", - "std.10.12.parquet:md5,7e45a4df99343383aa4c04261e3f2ff5", - "std.10.13.parquet:md5,7feb69126e290cefe39be2091ff962bb", - "std.10.14.parquet:md5,9766cd3cb29841439cf7139a8bc36ff6", - "std.10.15.parquet:md5,bd22f0aeaaf094778eca461a10672eef", - "std.10.16.parquet:md5,f7ab77bcb1d8baab91521b98f249de2b", - "std.10.2.parquet:md5,ec05ca850903cdca70c8cdf5bca5c77f", - "std.10.3.parquet:md5,11789030e5cd0071a6bd50fe83095354", - "std.10.4.parquet:md5,462f66c742027e3a04019d0655f4c4ca", - "std.10.5.parquet:md5,343c5851906086e7afdf977e4dbc1316", - "std.10.6.parquet:md5,29ba6307c7436c68a5e0750666968dbc", - "std.10.7.parquet:md5,cae20a7090e6fc350bdd10a36ea84444", - "std.10.8.parquet:md5,bca95b87d5bb575346223bcb643f60e5", - "std.10.9.parquet:md5,31e8bc47efc76cd173605372bd1b7eea", - "std.11.11.parquet:md5,979ac87879f83d3e2702bd32bb14c8a8", - "std.11.12.parquet:md5,83cd463128f25b49e76e7e683825f60d", - "std.11.13.parquet:md5,1c643f879df783dca63d3b413b45913b", - "std.11.14.parquet:md5,56f73fa9d507e4a31d1171626541de8b", - "std.11.15.parquet:md5,acdc81f2318990158842af5cd0b02e85", - "std.11.16.parquet:md5,c28dba69d77532178b435397a88e3c77", - "std.11.2.parquet:md5,7727bb854a125e06d5b095b54d99f8e7", - "std.11.3.parquet:md5,c18bc02c96539619815e952051a7d950", - "std.11.4.parquet:md5,547d219f82204d1f0e5b81df24999875", - "std.11.5.parquet:md5,01a68ff717a3d302b21d3817694a0ff4", - "std.11.6.parquet:md5,66379e1c8a5f6a0053bb80b920f2a48e", - "std.11.7.parquet:md5,3549d8f39b52bbe580bbd43b74497271", - "std.11.8.parquet:md5,38c437a312a7c10c9babeb073be3dc0b", - "std.11.9.parquet:md5,c5af0eb1de4fdc955e662347decd6d2b", - "std.12.12.parquet:md5,857da96e1f79bd4732cc40193fff8a13", - "std.12.13.parquet:md5,65d3fdd3a0805839c0d8e5a59d5ec142", - "std.12.14.parquet:md5,47c1675d08fad5ae12904b58a383659c", - "std.12.15.parquet:md5,b416d03b0180bb0e0905acb14ba59516", - "std.12.16.parquet:md5,53d32be9ec57d7b78f5270ffb1b9d983", - "std.12.2.parquet:md5,b2001c980557515107cc16fe31ebe2ba", - "std.12.3.parquet:md5,e072bb955b14bd1d7b9dcceaf944dd64", - "std.12.4.parquet:md5,e2aac77501d07693edf0da6548921d60", - "std.12.5.parquet:md5,44757a40231677ad2b18b33823cdb4ea", - "std.12.6.parquet:md5,47ae297f8e4d33039e85f0da842b034c", - "std.12.7.parquet:md5,9715ee0d5abc89b94467f2c79f457d98", - "std.12.8.parquet:md5,19f9c12a4f8a7ee8d869373f0067e4df", - "std.12.9.parquet:md5,ccc5b0ec564b82b874020ece4ef19c7c", - "std.13.13.parquet:md5,407fc3e78588ea159f850dae379956d1", - "std.13.14.parquet:md5,298a495c0b77165ddaadd054df0fd87f", - "std.13.15.parquet:md5,9d49532e98ad4757b5fd1b52f002b40b", - "std.13.16.parquet:md5,3c4bb049e674cb8a49134b26d5394ba8", - "std.13.2.parquet:md5,4f7725062597614210e64b57f9165ae2", - "std.13.3.parquet:md5,d55749b4e3828300f97e920eb1fac155", - "std.13.4.parquet:md5,5eaeda5db329ce2aded563dd829a92ae", - "std.13.5.parquet:md5,7df214e7ff65db00d3635c003fb572be", - "std.13.6.parquet:md5,fd0769c6433c04f236f90398232eacd6", - "std.13.7.parquet:md5,9d89e6449c132d48db5211ab40fed485", - "std.13.8.parquet:md5,37326ce5d31ded417bb7b875d1b48ff1", - "std.13.9.parquet:md5,6605f549c4c564ca5aa63c05c5808bd1", - "std.14.14.parquet:md5,734bc2fe5a30df286332ddc1e8e612cc", - "std.14.15.parquet:md5,8eb2bd8a653a5ffdf89c1c10a8d64caa", - "std.14.16.parquet:md5,d8ed4f59886c8a2adf4b5fef14c3999b", - "std.14.2.parquet:md5,8e7246d123978e8c14a4b0c8ae089ee7", - "std.14.3.parquet:md5,08d35d497abf204ac33eac16ac6d8e53", - "std.14.4.parquet:md5,81cec87e8958e2627a496e70491f016e", - "std.14.5.parquet:md5,c6148a101cd3a1628bcc2ad8ae2aefc4", - "std.14.6.parquet:md5,8b76227ebeecce0a59a9a18ab3bf5eba", - "std.14.7.parquet:md5,06e1f034cfbd83980277512f26ada3c0", - "std.14.8.parquet:md5,55015f5158f5ab44644b29126f85941d", - "std.14.9.parquet:md5,d6d7403e73fa34982e7f9e03b72f44d1", - "std.15.15.parquet:md5,afb7f4e05042ffec0cae4712299f28ad", - "std.15.16.parquet:md5,b0e232a8c1c1f01b66bc4d40122d2493", - "std.15.2.parquet:md5,071700c13605ad8de1bd517aa7317dfd", - "std.15.3.parquet:md5,dd4d91da12f98439b3d94c308ceab6e0", - "std.15.4.parquet:md5,db4630238515c901f5dc65fedb5f2b92", - "std.15.5.parquet:md5,d53438dc3b40229c24822ec9eb530dcb", - "std.15.6.parquet:md5,dbd6a825933d30c466e201705e3e3cdd", - "std.15.7.parquet:md5,66323071c468bec87555a42e8275c18e", - "std.15.8.parquet:md5,fa41e9a05d7ba4d71c737347fdf2882c", - "std.15.9.parquet:md5,70c2456934546b5ea2036f22f075c0ee", - "std.16.16.parquet:md5,f7b86c99d639c7db86c36b1f1beae643", - "std.16.2.parquet:md5,10f7ee1af80dddad2938599d504ae013", - "std.16.3.parquet:md5,82160c8f78c30beb3550ca43186e888d", - "std.16.4.parquet:md5,280dcf56b02c2580a1ead8273a7e12a9", - "std.16.5.parquet:md5,3a426b7559b1a4b791483887e900567c", - "std.16.6.parquet:md5,1cc73ac6c367a1f2f932d027b9ecb051", - "std.16.7.parquet:md5,fa15b7b1c83df83e7afeaeba0ad566ed", - "std.16.8.parquet:md5,3d9b605eaac2dff80650581d6b160ed0", - "std.16.9.parquet:md5,b0864e293b295b844563a57d62c624aa", - "std.2.2.parquet:md5,e99427127de921e5cc2e7875890e4977", - "std.2.3.parquet:md5,5b4737511f9f9a5cacfb56f68c5eec23", - "std.2.4.parquet:md5,d54785b00e6d39195add2957bc2d6f9f", - "std.2.5.parquet:md5,2ac6ae4554b8950c45f834a9c108bab6", - "std.2.6.parquet:md5,382a27f122f74dc9a65687ead05a6a34", - "std.2.7.parquet:md5,41f174a7c8f7c7f65c562e1e21d655df", - "std.2.8.parquet:md5,ebcabf1007aab0bd81d2d96b210e4aac", - "std.2.9.parquet:md5,65366a1207657e5afb74f27e15dc1c7b", - "std.3.3.parquet:md5,506a62ca65ecc5804f15e6965eed6977", - "std.3.4.parquet:md5,0a9f00e1521538a27b9ecfece22ff849", - "std.3.5.parquet:md5,ed56a7c68aa98476923b09460117d81d", - "std.3.6.parquet:md5,deb6a7883efa1f57601f7656f22babb8", - "std.3.7.parquet:md5,66532d7113804d5213e7074ebe64af0e", - "std.3.8.parquet:md5,06cec858ba2e43e97a035617b51d32a1", - "std.3.9.parquet:md5,2d780e82e9e71eba70619c08f701e972", - "std.4.4.parquet:md5,7418812c4aa5b85dbe9e3796e47e2055", - "std.4.5.parquet:md5,5623218acb89fc86c7bae605c92adeef", - "std.4.6.parquet:md5,1f75e593b4e2d1bbf177c97843bad2a6", - "std.4.7.parquet:md5,c31d0118d869829a098ddb3018c3266d", - "std.4.8.parquet:md5,871ed7aee924e426e36702bf7f118e3c", - "std.4.9.parquet:md5,756eb1acfea0045a596330a85811aec2", - "std.5.5.parquet:md5,52edf8d36ef43fec76b3d9f34218403e", - "std.5.6.parquet:md5,11a81eaa8e0151fcdfa1c7429c6d32ea", - "std.5.7.parquet:md5,828dd6a6b2266ab77e4aaf3ea05a1e37", - "std.5.8.parquet:md5,6889338863e72fc94d0c75a89b2d6aa9", - "std.5.9.parquet:md5,e61e86cad95eded4ed262daf053b47a5", - "std.6.6.parquet:md5,00f6598ec67405bca4b2c87614beefec", - "std.6.7.parquet:md5,29ea4a97b554ce27d778f8606484401d", - "std.6.8.parquet:md5,16c084c6e8c8a601c150800fa7bec5d5", - "std.6.9.parquet:md5,c649e7433cbd7cde59081f2f0f744b4c", - "std.7.7.parquet:md5,20ae4657f1a34d4b5a56f6d0b87c3e56", - "std.7.8.parquet:md5,65b782704f2e6124f159677489cefcae", - "std.7.9.parquet:md5,783798bcb749ce77c05cac8d4a535706", - "std.8.8.parquet:md5,a4e42852f03df97c1dc885a3762545a8", - "std.8.9.parquet:md5,ded56086d0126f3f426eba97bb349ee4", - "std.9.9.parquet:md5,5c852bc2035d149e55ddd0c7bf2c689c", - "m_measures.csv:md5,72860d9c3f88e1a170cb84459e38ecbd", - "stability_values.normfinder.csv:md5,a15d1dc962e43a0065c2954d939cb521" - ] - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-08T17:02:15.756268007" - }, - "-profile test_local_and_downloaded": { - "content": [ - null, - [ - "pipeline_info" - ], - [ - - ] - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-08T16:58:43.646395859" - }, - "-profile test_ignore_errors": { - "content": [ - null, - [ - "pipeline_info" - ], - [ - - ] - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-08T16:58:50.112382172" - }, - "-profile test_download_only": { - "content": [ - null, - [ - "errors", - "expression_atlas", - "expression_atlas/accessions", - "expression_atlas/accessions/accessions.txt", - "expression_atlas/accessions/selected_experiments.metadata.tsv", - "expression_atlas/accessions/species_experiments.metadata.tsv", - "expression_atlas/datasets", - "expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", - "expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", - "geo", - "geo/accessions", - "geo/accessions/accessions.tsv", - "geo/accessions/geo_all_datasets.metadata.tsv", - "geo/accessions/geo_rejected_datasets.metadata.tsv", - "geo/accessions/geo_selected_datasets.metadata.tsv", - "geo/datasets", - "geo/datasets/GSE55951.design.csv", - "geo/datasets/GSE55951.microarray.normalised.counts.csv", - "multiqc", - "multiqc/multiqc_data", - "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_eatlas_all_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_rejected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_selected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_software_versions.txt", - "multiqc/multiqc_data/multiqc_sources.txt", - "multiqc/multiqc_plots", - "multiqc/multiqc_plots/pdf", - "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_rejected_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_selected_experiments_metadata.pdf", - "multiqc/multiqc_plots/png", - "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", - "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_rejected_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_selected_experiments_metadata.png", - "multiqc/multiqc_plots/svg", - "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_rejected_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_selected_experiments_metadata.svg", - "multiqc/multiqc_report.html", - "multiqc/versions.yml", - "pipeline_info", - "pipeline_info/software_mqc_versions.yml", - "warnings" - ], - [ - "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", - "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", - "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", - "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", - "accessions.tsv:md5,095fb7f3a666b4382d4ba7296053451b", - "geo_all_datasets.metadata.tsv:md5,f5385de48a2e614681d3de4820a28c1e", - "geo_rejected_datasets.metadata.tsv:md5,cf907a70f381df40e044186db1a320a2", - "geo_selected_datasets.metadata.tsv:md5,f5385de48a2e614681d3de4820a28c1e", - "GSE55951.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d", - "GSE55951.microarray.normalised.counts.csv:md5,18fd2d728ad2ec5cb78f994f73375144", - "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", - "multiqc_eatlas_all_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", - "multiqc_eatlas_selected_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", - "multiqc_geo_all_experiments_metadata.txt:md5,5e1395382d94f4a8f39c17a89509c5f8", - "multiqc_geo_rejected_experiments_metadata.txt:md5,ea6234d7e4c463fc249aebdf91788df8", - "multiqc_geo_selected_experiments_metadata.txt:md5,5e1395382d94f4a8f39c17a89509c5f8", - "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030" - ] - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-08T16:54:39.147528939" - }, - "-profile test_full": { - "content": [ - null, - [ - "aggregate_results", - "aggregate_results/all_counts_filtered.parquet", - "aggregate_results/all_genes_summary.csv", - "aggregate_results/top_stable_genes_summary.csv", - "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", - "base_statistics", - "base_statistics/all", - "base_statistics/all/stats_all_genes.csv", - "base_statistics/rnaseq", - "base_statistics/rnaseq/rnaseq.stats_all_genes.csv", - "clean_count_data", - "clean_count_data/cleaned_counts_filtered.parquet", - "compute_stability_scores", - "compute_stability_scores/stats_with_scores.csv", - "dash_app", - "dash_app/app.py", - "dash_app/assets", - "dash_app/assets/style.css", - "dash_app/data", - "dash_app/data/all_counts.parquet", - "dash_app/data/all_genes_summary.csv", - "dash_app/data/whole_design.csv", - "dash_app/file_system_backend", - "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", - "dash_app/spec-file.txt", - "dash_app/src", - "dash_app/src/callbacks", - "dash_app/src/callbacks/__pycache__", - "dash_app/src/callbacks/__pycache__/common.cpython-313.pyc", - "dash_app/src/callbacks/__pycache__/genes.cpython-313.pyc", - "dash_app/src/callbacks/__pycache__/samples.cpython-313.pyc", - "dash_app/src/callbacks/common.py", - "dash_app/src/callbacks/genes.py", - "dash_app/src/callbacks/samples.py", - "dash_app/src/components", - "dash_app/src/components/__pycache__", - "dash_app/src/components/__pycache__/graphs.cpython-313.pyc", - "dash_app/src/components/__pycache__/right_sidebar.cpython-313.pyc", - "dash_app/src/components/__pycache__/stores.cpython-313.pyc", - "dash_app/src/components/__pycache__/tables.cpython-313.pyc", - "dash_app/src/components/__pycache__/tooltips.cpython-313.pyc", - "dash_app/src/components/__pycache__/top.cpython-313.pyc", - "dash_app/src/components/graphs.py", - "dash_app/src/components/icons.py", - "dash_app/src/components/right_sidebar.py", - "dash_app/src/components/settings", - "dash_app/src/components/settings/__pycache__", - "dash_app/src/components/settings/__pycache__/genes.cpython-313.pyc", - "dash_app/src/components/settings/__pycache__/samples.cpython-313.pyc", - "dash_app/src/components/settings/genes.py", - "dash_app/src/components/settings/samples.py", - "dash_app/src/components/stores.py", - "dash_app/src/components/tables.py", - "dash_app/src/components/tooltips.py", - "dash_app/src/components/top.py", - "dash_app/src/utils", - "dash_app/src/utils/__pycache__", - "dash_app/src/utils/__pycache__/config.cpython-313.pyc", - "dash_app/src/utils/__pycache__/data_management.cpython-313.pyc", - "dash_app/src/utils/__pycache__/style.cpython-313.pyc", - "dash_app/src/utils/config.py", - "dash_app/src/utils/data_management.py", - "dash_app/src/utils/style.py", - "dash_app/versions.yml", - "dataset_statistics", - "dataset_statistics/E_GEOD_61690_rnaseq.dataset_stats.csv", - "dataset_statistics/E_GEOD_77826_rnaseq.dataset_stats.csv", - "dataset_statistics/E_MTAB_4251_rnaseq.dataset_stats.csv", - "dataset_statistics/E_MTAB_4301_rnaseq.dataset_stats.csv", - "dataset_statistics/E_MTAB_5038_rnaseq.dataset_stats.csv", - "dataset_statistics/E_MTAB_5215_rnaseq.dataset_stats.csv", - "dataset_statistics/E_MTAB_552_rnaseq.dataset_stats.csv", - "dataset_statistics/E_MTAB_7711_rnaseq.dataset_stats.csv", - "errors", - "expression_atlas", - "expression_atlas/accessions", - "expression_atlas/accessions/accessions.txt", - "expression_atlas/accessions/selected_experiments.metadata.tsv", - "expression_atlas/accessions/species_experiments.metadata.tsv", - "expression_atlas/datasets", - "expression_atlas/datasets/E_GEOD_61690_rnaseq.design.csv", - "expression_atlas/datasets/E_GEOD_61690_rnaseq.rnaseq.raw.counts.csv", - "expression_atlas/datasets/E_GEOD_77826_rnaseq.design.csv", - "expression_atlas/datasets/E_GEOD_77826_rnaseq.rnaseq.raw.counts.csv", - "expression_atlas/datasets/E_MTAB_4251_rnaseq.design.csv", - "expression_atlas/datasets/E_MTAB_4251_rnaseq.rnaseq.raw.counts.csv", - "expression_atlas/datasets/E_MTAB_4301_rnaseq.design.csv", - "expression_atlas/datasets/E_MTAB_4301_rnaseq.rnaseq.raw.counts.csv", - "expression_atlas/datasets/E_MTAB_5038_rnaseq.design.csv", - "expression_atlas/datasets/E_MTAB_5038_rnaseq.rnaseq.raw.counts.csv", - "expression_atlas/datasets/E_MTAB_5215_rnaseq.design.csv", - "expression_atlas/datasets/E_MTAB_5215_rnaseq.rnaseq.raw.counts.csv", - "expression_atlas/datasets/E_MTAB_552_rnaseq.design.csv", - "expression_atlas/datasets/E_MTAB_552_rnaseq.rnaseq.raw.counts.csv", - "expression_atlas/datasets/E_MTAB_7711_rnaseq.design.csv", - "expression_atlas/datasets/E_MTAB_7711_rnaseq.rnaseq.raw.counts.csv", - "geo", - "geo/excluded_geo_accessions.txt", - "get_candidate_genes", - "get_candidate_genes/candidate_counts.parquet", - "idmapping", - "idmapping/datasets", - "idmapping/datasets/E_GEOD_61690_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/datasets/E_GEOD_61690_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/datasets/E_GEOD_61690_rnaseq.rnaseq.raw.counts.renamed.csv", - "idmapping/datasets/E_GEOD_77826_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/datasets/E_GEOD_77826_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/datasets/E_GEOD_77826_rnaseq.rnaseq.raw.counts.renamed.csv", - "idmapping/datasets/E_MTAB_4251_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/datasets/E_MTAB_4251_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/datasets/E_MTAB_4251_rnaseq.rnaseq.raw.counts.renamed.csv", - "idmapping/datasets/E_MTAB_4301_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/datasets/E_MTAB_4301_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/datasets/E_MTAB_4301_rnaseq.rnaseq.raw.counts.renamed.csv", - "idmapping/datasets/E_MTAB_5038_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/datasets/E_MTAB_5038_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/datasets/E_MTAB_5038_rnaseq.rnaseq.raw.counts.renamed.csv", - "idmapping/datasets/E_MTAB_5215_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/datasets/E_MTAB_5215_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/datasets/E_MTAB_5215_rnaseq.rnaseq.raw.counts.renamed.csv", - "idmapping/datasets/E_MTAB_552_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/datasets/E_MTAB_552_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/datasets/E_MTAB_552_rnaseq.rnaseq.raw.counts.renamed.csv", - "idmapping/datasets/E_MTAB_7711_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/datasets/E_MTAB_7711_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/datasets/E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.csv", - "idmapping/whole_gene_id_mapping.csv", - "idmapping/whole_gene_metadata.csv", - "merged_datasets", - "merged_datasets/all", - "merged_datasets/all/all_counts.parquet", - "merged_datasets/rnaseq", - "merged_datasets/rnaseq/all_counts.parquet", - "merged_datasets/whole_design.csv", - "multiqc", - "multiqc/multiqc_data", - "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_eatlas_all_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", - "multiqc/multiqc_data/multiqc_gene_statistics.txt", - "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", - "multiqc/multiqc_data/multiqc_software_versions.txt", - "multiqc/multiqc_data/multiqc_sources.txt", - "multiqc/multiqc_plots", - "multiqc/multiqc_plots/pdf", - "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", - "multiqc/multiqc_plots/pdf/gene_statistics.pdf", - "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", - "multiqc/multiqc_plots/png", - "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", - "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", - "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", - "multiqc/multiqc_plots/png/gene_statistics.png", - "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", - "multiqc/multiqc_plots/svg", - "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", - "multiqc/multiqc_plots/svg/gene_statistics.svg", - "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", - "multiqc/multiqc_report.html", - "multiqc/versions.yml", - "normalised", - "normalised/E_GEOD_61690_rnaseq", - "normalised/E_GEOD_61690_rnaseq/normalisation_deseq2", - "normalised/E_GEOD_61690_rnaseq/normalisation_deseq2/E_GEOD_61690_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", - "normalised/E_GEOD_77826_rnaseq", - "normalised/E_GEOD_77826_rnaseq/normalisation_deseq2", - "normalised/E_GEOD_77826_rnaseq/normalisation_deseq2/E_GEOD_77826_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", - "normalised/E_MTAB_4251_rnaseq", - "normalised/E_MTAB_4251_rnaseq/normalisation_deseq2", - "normalised/E_MTAB_4251_rnaseq/normalisation_deseq2/E_MTAB_4251_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", - "normalised/E_MTAB_4301_rnaseq", - "normalised/E_MTAB_4301_rnaseq/normalisation_deseq2", - "normalised/E_MTAB_4301_rnaseq/normalisation_deseq2/E_MTAB_4301_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", - "normalised/E_MTAB_5038_rnaseq", - "normalised/E_MTAB_5038_rnaseq/normalisation_deseq2", - "normalised/E_MTAB_5038_rnaseq/normalisation_deseq2/E_MTAB_5038_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", - "normalised/E_MTAB_5215_rnaseq", - "normalised/E_MTAB_5215_rnaseq/normalisation_deseq2", - "normalised/E_MTAB_5215_rnaseq/normalisation_deseq2/E_MTAB_5215_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", - "normalised/E_MTAB_552_rnaseq", - "normalised/E_MTAB_552_rnaseq/normalisation_deseq2", - "normalised/E_MTAB_552_rnaseq/normalisation_deseq2/E_MTAB_552_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", - "normalised/E_MTAB_7711_rnaseq", - "normalised/E_MTAB_7711_rnaseq/normalisation_deseq2", - "normalised/E_MTAB_7711_rnaseq/normalisation_deseq2/E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", - "pipeline_info", - "pipeline_info/software_mqc_versions.yml", - "quantile_normalised", - "quantile_normalised/E_GEOD_61690_rnaseq", - "quantile_normalised/E_GEOD_61690_rnaseq/E_GEOD_61690_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", - "quantile_normalised/E_GEOD_77826_rnaseq", - "quantile_normalised/E_GEOD_77826_rnaseq/E_GEOD_77826_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", - "quantile_normalised/E_MTAB_4251_rnaseq", - "quantile_normalised/E_MTAB_4251_rnaseq/E_MTAB_4251_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", - "quantile_normalised/E_MTAB_4301_rnaseq", - "quantile_normalised/E_MTAB_4301_rnaseq/E_MTAB_4301_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", - "quantile_normalised/E_MTAB_5038_rnaseq", - "quantile_normalised/E_MTAB_5038_rnaseq/E_MTAB_5038_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", - "quantile_normalised/E_MTAB_5215_rnaseq", - "quantile_normalised/E_MTAB_5215_rnaseq/E_MTAB_5215_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", - "quantile_normalised/E_MTAB_552_rnaseq", - "quantile_normalised/E_MTAB_552_rnaseq/E_MTAB_552_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", - "quantile_normalised/E_MTAB_7711_rnaseq", - "quantile_normalised/E_MTAB_7711_rnaseq/E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", - "stability_scoring", - "stability_scoring/normfinder", - "stability_scoring/normfinder/stability_values.normfinder.csv", - "warnings" - ], - [ - "all_counts_filtered.parquet:md5,e4aad0044b83171b71b8a3926eccfd1a", - "all_genes_summary.csv:md5,39a8d7ef0b72232c352cd1b63b9d83ed", - "top_stable_genes_summary.csv:md5,daa39aaa66ab7c4ad0240c6d97b27226", - "top_stable_genes_transposed_counts_filtered.csv:md5,684b2eab39b01aa1f08317c437cfe0fb", - "stats_all_genes.csv:md5,979b60306cb4a375383bf7f251d420e2", - "rnaseq.stats_all_genes.csv:md5,43a6eabdd569890e442f5e95432b6fa5", - "cleaned_counts_filtered.parquet:md5,48880f086280859779d81905bacc0fc1", - "stats_with_scores.csv:md5,df4018b2d1bbfd19e29e81cad8ff6b81", - "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", - "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_counts.parquet:md5,1f19d098af2c813d0ce391b9eda53218", - "all_genes_summary.csv:md5,39a8d7ef0b72232c352cd1b63b9d83ed", - "whole_design.csv:md5,112168cfd2cc4aad6154fd0aefa7e8fa", - "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", - "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", - "common.cpython-313.pyc:md5,b09a65f4db6d64f2be3394c0cd4f5d53", - "genes.cpython-313.pyc:md5,24d7b73d83310718cd468fe966c1d393", - "samples.cpython-313.pyc:md5,ec0b78afc64755487ef0263a7b8db643", - "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", - "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", - "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", - "graphs.cpython-313.pyc:md5,ea20facda8832bc5ec08dd7058a92c5a", - "right_sidebar.cpython-313.pyc:md5,9fd04280bc6928eae4628baf0f8f931f", - "stores.cpython-313.pyc:md5,f583ebec33388147afb10fcc3bab42a1", - "tables.cpython-313.pyc:md5,e619c4a46cd6f57e3df0fffcd75fde30", - "tooltips.cpython-313.pyc:md5,ee71d4ad971477121f15309e14115166", - "top.cpython-313.pyc:md5,32bdc9e369ee7176e9e2a89dba3b4cac", - "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", - "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", - "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", - "genes.cpython-313.pyc:md5,0a9c01319b67e0b702d0d363a751b6b6", - "samples.cpython-313.pyc:md5,886f59d65dde1182be41a785727a85a4", - "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", - "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", - "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", - "tables.py:md5,84d17ad7f43458412e550f74a24560e5", - "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", - "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.cpython-313.pyc:md5,052a0caba837d667d76fbf55cf8a2224", - "data_management.cpython-313.pyc:md5,62a8bafd3de9d0e9fd8991315667d341", - "style.cpython-313.pyc:md5,67486186d18ce48b2e2be38bb2c4f2e8", - "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", - "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", - "style.py:md5,b9f1207f06464e43c05e6e3912f12731", - "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "E_GEOD_61690_rnaseq.dataset_stats.csv:md5,a32803af6640f8b67def869a8e13f3a2", - "E_GEOD_77826_rnaseq.dataset_stats.csv:md5,d7f8d0ccc6a639a8b264d2a6cba84240", - "E_MTAB_4251_rnaseq.dataset_stats.csv:md5,004f8331e0780519ffaede673bee01e8", - "E_MTAB_4301_rnaseq.dataset_stats.csv:md5,abc0d94f32b6b372e1094fe3160e08a1", - "E_MTAB_5038_rnaseq.dataset_stats.csv:md5,7f278757f7aac91d38d6898e10516e1d", - "E_MTAB_5215_rnaseq.dataset_stats.csv:md5,f7c428db2c5918a2c83fe17624bb5db7", - "E_MTAB_552_rnaseq.dataset_stats.csv:md5,c15c5c7f9a9d9f3e3616c3c72f0cc285", - "E_MTAB_7711_rnaseq.dataset_stats.csv:md5,430d11fb6abbdf111dcd1d42e95e2548", - "accessions.txt:md5,dad63ac7a1715277fa44567bc40b5872", - "selected_experiments.metadata.tsv:md5,15baa430176fcadf7bf03034fa9e95c6", - "species_experiments.metadata.tsv:md5,15baa430176fcadf7bf03034fa9e95c6", - "E_GEOD_61690_rnaseq.design.csv:md5,ba807caf74e0b55f5c3fe23810a89560", - "E_GEOD_61690_rnaseq.rnaseq.raw.counts.csv:md5,2e3f1a125b3d41d622e2d24447620eb3", - "E_GEOD_77826_rnaseq.design.csv:md5,5aa61df754aa9c6c107b247c642d2e53", - "E_GEOD_77826_rnaseq.rnaseq.raw.counts.csv:md5,85cea79c602a9924d5a4d6b597ef5530", - "E_MTAB_4251_rnaseq.design.csv:md5,4f3ef7b76ca6ed1ec3157295bc4a8d84", - "E_MTAB_4251_rnaseq.rnaseq.raw.counts.csv:md5,5cf27be0e00b93d5d431754ba8058687", - "E_MTAB_4301_rnaseq.design.csv:md5,165eeef7d612c01fd62baae1a3f296ae", - "E_MTAB_4301_rnaseq.rnaseq.raw.counts.csv:md5,1ab49feea238e7b1419937b5037952b5", - "E_MTAB_5038_rnaseq.design.csv:md5,352ed3163d7deef2be35d899418d5ad4", - "E_MTAB_5038_rnaseq.rnaseq.raw.counts.csv:md5,b4acb3d7c39cdb2bd6cef6c9314c5b2a", - "E_MTAB_5215_rnaseq.design.csv:md5,2741dcd5b45bacce865db632f626a273", - "E_MTAB_5215_rnaseq.rnaseq.raw.counts.csv:md5,273704bdf762c342271b33958a84d1e7", - "E_MTAB_552_rnaseq.design.csv:md5,b81490696f638e90c1cf14236bb0c08c", - "E_MTAB_552_rnaseq.rnaseq.raw.counts.csv:md5,830f50b60b17b62f9ca2f6a163a2879f", - "E_MTAB_7711_rnaseq.design.csv:md5,3e7748b54a0c25c008d9bd2ddbf1bf00", - "E_MTAB_7711_rnaseq.rnaseq.raw.counts.csv:md5,3c02cf432c29d3751c978439539df388", - "excluded_geo_accessions.txt:md5,3b29ba0fe90a301ef71639760fc8e5a9", - "candidate_counts.parquet:md5,b3793a0dd3a06f2d7e16023456aac9ca", - "E_GEOD_61690_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", - "E_GEOD_61690_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", - "E_GEOD_61690_rnaseq.rnaseq.raw.counts.renamed.csv:md5,ccb6efdfb49bc4057618a2a10ec880b7", - "E_GEOD_77826_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", - "E_GEOD_77826_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", - "E_GEOD_77826_rnaseq.rnaseq.raw.counts.renamed.csv:md5,9f933a357deca364f73ca96aa6fa8c5a", - "E_MTAB_4251_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", - "E_MTAB_4251_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", - "E_MTAB_4251_rnaseq.rnaseq.raw.counts.renamed.csv:md5,74aa87fe4102ae828b1bfb0dc7e2bb3a", - "E_MTAB_4301_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", - "E_MTAB_4301_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", - "E_MTAB_4301_rnaseq.rnaseq.raw.counts.renamed.csv:md5,d7a9d4f33e612a803d0032cf1a8b6677", - "E_MTAB_5038_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", - "E_MTAB_5038_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", - "E_MTAB_5038_rnaseq.rnaseq.raw.counts.renamed.csv:md5,07fb1b79dff4a159c9814225dd947d30", - "E_MTAB_5215_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", - "E_MTAB_5215_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", - "E_MTAB_5215_rnaseq.rnaseq.raw.counts.renamed.csv:md5,f3a308689af31ba086fc5f341f0589a4", - "E_MTAB_552_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", - "E_MTAB_552_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", - "E_MTAB_552_rnaseq.rnaseq.raw.counts.renamed.csv:md5,449f7c179fc675784f15f58735196644", - "E_MTAB_7711_rnaseq.rnaseq.raw.counts.mapping.csv:md5,87c58803a087a768eff2403b40868614", - "E_MTAB_7711_rnaseq.rnaseq.raw.counts.metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", - "E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.csv:md5,3856796c675c7ee3bb6975185f1b869b", - "whole_gene_id_mapping.csv:md5,87c58803a087a768eff2403b40868614", - "whole_gene_metadata.csv:md5,beeb8078e20bc0f72f7029b4cbba14f6", - "all_counts.parquet:md5,1f19d098af2c813d0ce391b9eda53218", - "all_counts.parquet:md5,e2ca2b6898a28f583f41994cff8e9e34", - "whole_design.csv:md5,112168cfd2cc4aad6154fd0aefa7e8fa", - "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", - "multiqc_eatlas_all_experiments_metadata.txt:md5,a2b6cf46faf4b8c7dcdae06e17b35432", - "multiqc_eatlas_selected_experiments_metadata.txt:md5,a2b6cf46faf4b8c7dcdae06e17b35432", - "multiqc_expression_distributions_top_stable_genes.txt:md5,a8b7a3a0d3ecdbbb3874b1a8e32c5425", - "multiqc_gene_statistics.txt:md5,c07cdee51db5d4f42f4987564098b4de", - "multiqc_ranked_top_stable_genes_summary.txt:md5,b73b4b702dea4bf30fdf5f0c62f4582c", - "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030", - "E_GEOD_61690_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,4bc05ef9eff93062591fc37ae93b830d", - "E_GEOD_77826_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,0e4f0b0a0276f3a835d26d4b518296f2", - "E_MTAB_4251_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,c9d0a14d809fe994abb1280ee7c00612", - "E_MTAB_4301_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,cdd0db06fdcfb4261853a63b5527c4a1", - "E_MTAB_5038_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,7f72ebf195c0dfb26d13c09157ea1914", - "E_MTAB_5215_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,e427282767b3833f899b1a03bc40edc2", - "E_MTAB_552_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,26e7b283f3c901cc417c1fe0e6b9d555", - "E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,f7374c86950c01bc6a6b8d8fd415cc8b", - "E_GEOD_61690_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,44769b8573e9bd8b09c46bb5e5e5404c", - "E_GEOD_77826_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,ad69a1121e3359058763506c736430ab", - "E_MTAB_4251_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,dc9bf18fc110383081768702d05e711a", - "E_MTAB_4301_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,5511120c972162c05586be3a918ba35e", - "E_MTAB_5038_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,5735835a7817e75b1789f48fed702c44", - "E_MTAB_5215_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,0a3961314238ec57c7144a9781466ac3", - "E_MTAB_552_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,8b801cd4f42277a757d1360be54c9940", - "E_MTAB_7711_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,51c0e455009e214c856a2ef6418b01d2", - "stability_values.normfinder.csv:md5,614ee8b42bd3bc4fe1a4663cc2b7463d" - ] - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-08T17:21:19.722455668" - }, - "-profile test_one_rnaseq_one_microarray": { - "content": [ - null, - [ - "aggregate_results", - "aggregate_results/all_counts_filtered.parquet", - "aggregate_results/all_genes_summary.csv", - "aggregate_results/top_stable_genes_summary.csv", - "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", - "base_statistics", - "base_statistics/all", - "base_statistics/all/stats_all_genes.csv", - "base_statistics/microarray", - "base_statistics/microarray/microarray.stats_all_genes.csv", - "base_statistics/rnaseq", - "base_statistics/rnaseq/rnaseq.stats_all_genes.csv", - "clean_count_data", - "clean_count_data/cleaned_counts_filtered.parquet", - "compute_stability_scores", - "compute_stability_scores/stats_with_scores.csv", - "dash_app", - "dash_app/app.py", - "dash_app/assets", - "dash_app/assets/style.css", - "dash_app/data", - "dash_app/data/all_counts.parquet", - "dash_app/data/all_genes_summary.csv", - "dash_app/data/whole_design.csv", - "dash_app/file_system_backend", - "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", - "dash_app/spec-file.txt", - "dash_app/src", - "dash_app/src/callbacks", - "dash_app/src/callbacks/__pycache__", - "dash_app/src/callbacks/__pycache__/common.cpython-313.pyc", - "dash_app/src/callbacks/__pycache__/genes.cpython-313.pyc", - "dash_app/src/callbacks/__pycache__/samples.cpython-313.pyc", - "dash_app/src/callbacks/common.py", - "dash_app/src/callbacks/genes.py", - "dash_app/src/callbacks/samples.py", - "dash_app/src/components", - "dash_app/src/components/__pycache__", - "dash_app/src/components/__pycache__/graphs.cpython-313.pyc", - "dash_app/src/components/__pycache__/right_sidebar.cpython-313.pyc", - "dash_app/src/components/__pycache__/stores.cpython-313.pyc", - "dash_app/src/components/__pycache__/tables.cpython-313.pyc", - "dash_app/src/components/__pycache__/tooltips.cpython-313.pyc", - "dash_app/src/components/__pycache__/top.cpython-313.pyc", - "dash_app/src/components/graphs.py", - "dash_app/src/components/icons.py", - "dash_app/src/components/right_sidebar.py", - "dash_app/src/components/settings", - "dash_app/src/components/settings/__pycache__", - "dash_app/src/components/settings/__pycache__/genes.cpython-313.pyc", - "dash_app/src/components/settings/__pycache__/samples.cpython-313.pyc", - "dash_app/src/components/settings/genes.py", - "dash_app/src/components/settings/samples.py", - "dash_app/src/components/stores.py", - "dash_app/src/components/tables.py", - "dash_app/src/components/tooltips.py", - "dash_app/src/components/top.py", - "dash_app/src/utils", - "dash_app/src/utils/__pycache__", - "dash_app/src/utils/__pycache__/config.cpython-313.pyc", - "dash_app/src/utils/__pycache__/data_management.cpython-313.pyc", - "dash_app/src/utils/__pycache__/style.cpython-313.pyc", - "dash_app/src/utils/config.py", - "dash_app/src/utils/data_management.py", - "dash_app/src/utils/style.py", - "dash_app/versions.yml", - "dataset_statistics", - "dataset_statistics/E_GEOD_21945_A_AFFY_2.dataset_stats.csv", - "dataset_statistics/E_GEOD_52806_rnaseq.dataset_stats.csv", - "errors", - "expression_atlas", - "expression_atlas/datasets", - "expression_atlas/datasets/E_GEOD_21945_A_AFFY_2.design.csv", - "expression_atlas/datasets/E_GEOD_21945_A_AFFY_2.microarray.normalised.counts.csv", - "expression_atlas/datasets/E_GEOD_52806_rnaseq.design.csv", - "expression_atlas/datasets/E_GEOD_52806_rnaseq.rnaseq.raw.counts.csv", - "geo", - "geo/excluded_geo_accessions.txt", - "get_candidate_genes", - "get_candidate_genes/candidate_counts.parquet", - "idmapping", - "idmapping/datasets", - "idmapping/datasets/E_GEOD_21945_A_AFFY_2.microarray.normalised.counts.mapping.csv", - "idmapping/datasets/E_GEOD_21945_A_AFFY_2.microarray.normalised.counts.metadata.csv", - "idmapping/datasets/E_GEOD_21945_A_AFFY_2.microarray.normalised.counts.renamed.csv", - "idmapping/datasets/E_GEOD_52806_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/datasets/E_GEOD_52806_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/datasets/E_GEOD_52806_rnaseq.rnaseq.raw.counts.renamed.csv", - "idmapping/whole_gene_id_mapping.csv", - "idmapping/whole_gene_metadata.csv", - "merged_datasets", - "merged_datasets/all", - "merged_datasets/all/all_counts.parquet", - "merged_datasets/microarray", - "merged_datasets/microarray/all_counts.parquet", - "merged_datasets/rnaseq", - "merged_datasets/rnaseq/all_counts.parquet", - "merged_datasets/whole_design.csv", - "multiqc", - "multiqc/multiqc_data", - "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_expression_distributions_top_stable_genes.txt", - "multiqc/multiqc_data/multiqc_gene_statistics.txt", - "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", - "multiqc/multiqc_data/multiqc_software_versions.txt", - "multiqc/multiqc_data/multiqc_sources.txt", - "multiqc/multiqc_plots", - "multiqc/multiqc_plots/pdf", - "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", - "multiqc/multiqc_plots/pdf/gene_statistics.pdf", - "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", - "multiqc/multiqc_plots/png", - "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", - "multiqc/multiqc_plots/png/gene_statistics.png", - "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", - "multiqc/multiqc_plots/svg", - "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", - "multiqc/multiqc_plots/svg/gene_statistics.svg", - "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", - "multiqc/multiqc_report.html", - "multiqc/versions.yml", - "normalised", - "normalised/E_GEOD_52806_rnaseq", - "normalised/E_GEOD_52806_rnaseq/normalisation_deseq2", - "normalised/E_GEOD_52806_rnaseq/normalisation_deseq2/E_GEOD_52806_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", - "pipeline_info", - "pipeline_info/software_mqc_versions.yml", - "quantile_normalised", - "quantile_normalised/E_GEOD_21945_A_AFFY_2", - "quantile_normalised/E_GEOD_21945_A_AFFY_2/E_GEOD_21945_A_AFFY_2.microarray.normalised.counts.renamed.quant_norm.parquet", - "quantile_normalised/E_GEOD_52806_rnaseq", - "quantile_normalised/E_GEOD_52806_rnaseq/E_GEOD_52806_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", - "stability_scoring", - "stability_scoring/normfinder", - "stability_scoring/normfinder/stability_values.normfinder.csv", - "warnings" - ], - [ - "all_counts_filtered.parquet:md5,f866985e43c480776dfcb05c8c06e49b", - "all_genes_summary.csv:md5,69ba69d9a58db2d10d4f17aecc41697b", - "top_stable_genes_summary.csv:md5,aed44b3253bcf143a636941ceefc3f9e", - "top_stable_genes_transposed_counts_filtered.csv:md5,a1f20aa526443cd3f54daafa6402b55d", - "stats_all_genes.csv:md5,0b52f1317e69e96d80d3690096bbce92", - "microarray.stats_all_genes.csv:md5,391c29159d970a72e03060c45437061d", - "rnaseq.stats_all_genes.csv:md5,d7ff8d9fa2d05ada58739875d88e06ce", - "cleaned_counts_filtered.parquet:md5,be312e13ec2ab8b57c663f3ecdde87f3", - "stats_with_scores.csv:md5,5c548d370d73f8cfb43fcd13a876e993", - "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", - "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_counts.parquet:md5,ddf26512efc2ea97257f6f634619491f", - "all_genes_summary.csv:md5,69ba69d9a58db2d10d4f17aecc41697b", - "whole_design.csv:md5,91d69f908aab581087541383a9cc1025", - "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", - "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", - "common.cpython-313.pyc:md5,cc7cb7ed16434b4a43ab5782241627de", - "genes.cpython-313.pyc:md5,8a92ae29ed9974eb29248c4e21e0715c", - "samples.cpython-313.pyc:md5,fb346c2f612de7500a1f5b76afee7e8d", - "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", - "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", - "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", - "graphs.cpython-313.pyc:md5,eea82c4a14212014260922068ae5ed04", - "right_sidebar.cpython-313.pyc:md5,94c760ce32040f3b5ea37b5d3e2c3a19", - "stores.cpython-313.pyc:md5,832b069bf682ce8907c19b81edb2c10f", - "tables.cpython-313.pyc:md5,d24f656b9aa718bf746aded25a487a84", - "tooltips.cpython-313.pyc:md5,73d1bc477c66be7d8f2b5230a8d00572", - "top.cpython-313.pyc:md5,37a20595a428164c20090d64314ae0fb", - "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", - "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", - "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", - "genes.cpython-313.pyc:md5,856ff23441233f9bf5eed8e62f75a66d", - "samples.cpython-313.pyc:md5,8a74deeb75fa6768968e8fd7d2ebff37", - "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", - "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", - "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", - "tables.py:md5,84d17ad7f43458412e550f74a24560e5", - "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", - "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.cpython-313.pyc:md5,b83f5941ca31e47a79675883fb0c7c07", - "data_management.cpython-313.pyc:md5,9c035c628e12438445367c1e93677105", - "style.cpython-313.pyc:md5,95e8e3da5520837f0e8e897d676c6e6c", - "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", - "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", - "style.py:md5,b9f1207f06464e43c05e6e3912f12731", - "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "E_GEOD_21945_A_AFFY_2.dataset_stats.csv:md5,5c80252d4ffa083f1c898580e9521d79", - "E_GEOD_52806_rnaseq.dataset_stats.csv:md5,444997cea92417085759f839d946da98", - "E_GEOD_21945_A_AFFY_2.design.csv:md5,3c2cc68528e555a885176d845f04d422", - "E_GEOD_21945_A_AFFY_2.microarray.normalised.counts.csv:md5,647f1f8b46457e201af3903be3326654", - "E_GEOD_52806_rnaseq.design.csv:md5,a0f1f0f86a2655f526d0cc099d5b6adc", - "E_GEOD_52806_rnaseq.rnaseq.raw.counts.csv:md5,da027fc750b0f00dca199122a67feac7", - "excluded_geo_accessions.txt:md5,3d247b02a5dfe0064be4c09ceb270c27", - "candidate_counts.parquet:md5,bba3c2a48acf01c14f31109564197130", - "E_GEOD_21945_A_AFFY_2.microarray.normalised.counts.mapping.csv:md5,e00b6804a4ba7ff25c708a9c6a4770f8", - "E_GEOD_21945_A_AFFY_2.microarray.normalised.counts.metadata.csv:md5,7efa12130b8d5bcdb79d630d07fec5d3", - "E_GEOD_21945_A_AFFY_2.microarray.normalised.counts.renamed.csv:md5,91576c77a0cff0116ef8aad73281b729", - "E_GEOD_52806_rnaseq.rnaseq.raw.counts.mapping.csv:md5,1bbc8cddf18072309e81498806aa73ff", - "E_GEOD_52806_rnaseq.rnaseq.raw.counts.metadata.csv:md5,08861c29159a6a2fed38efe523ed9c56", - "E_GEOD_52806_rnaseq.rnaseq.raw.counts.renamed.csv:md5,b8774e7b2c57ac8763dfd244c2c9b0f9", - "whole_gene_id_mapping.csv:md5,b7d2972efa44bdf1903702e260189b68", - "whole_gene_metadata.csv:md5,a08ab44a98542a8529d2a4b5a47e4408", - "all_counts.parquet:md5,ddf26512efc2ea97257f6f634619491f", - "all_counts.parquet:md5,7bea3b16ef009981cb1d6bdf5ce3c7e6", - "all_counts.parquet:md5,a0b9095a1806043a58cdcab4a0145829", - "whole_design.csv:md5,91d69f908aab581087541383a9cc1025", - "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", - "multiqc_expression_distributions_top_stable_genes.txt:md5,6335dcc35e436e1e0745d384a2aa9ced", - "multiqc_gene_statistics.txt:md5,959a3cd171c7d0d6798a54dcdc651134", - "multiqc_ranked_top_stable_genes_summary.txt:md5,520eef70d6c205719e6b1e173174bcfd", - "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030", - "E_GEOD_52806_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,b3b7bd2d373cd9f2e84c015c4d3c9d67", - "E_GEOD_21945_A_AFFY_2.microarray.normalised.counts.renamed.quant_norm.parquet:md5,3d9a59c5990674e632770aaf9557c733", - "E_GEOD_52806_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,932550ef062957fecc7d8e5da27c935b", - "stability_values.normfinder.csv:md5,86892f38ee9d3dc5e42710a4a873ddad" - ] - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-08T16:56:59.861500569" - }, - "-profile test_dataset_custom_mapping": { - "content": [ - null, - [ - "pipeline_info" - ], - [ - + "E_MTAB_5072_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,760061cd64c5d8b1cfd84a4f14b8bc25", + "stability_values.normfinder.csv:md5,fa28be676b85c7bad2587a4a0c15139f", + "E_MTAB_5072_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,91a6f555922d132276d6897ca8438e88", + "std.0.0.parquet:md5,bec540987111844db9a116058c7b535f", + "std.0.1.parquet:md5,f35662d53a8940682011ef471e91696e", + "std.0.10.parquet:md5,e13339c67b0da3a287a6bd4a8ea3630c", + "std.0.11.parquet:md5,be70d7213ff011d6e878dcf5b3a7e7dc", + "std.0.12.parquet:md5,d2f522a24f281c4c9c8402a7af9e9bc1", + "std.0.13.parquet:md5,169415fcd4731cbe642d62563748b7e0", + "std.0.14.parquet:md5,f84145555e3042c93d107bd42cf2b8c5", + "std.0.15.parquet:md5,45d0ed09f4a705a6c1962d8b57c51a4f", + "std.0.16.parquet:md5,c8b78eb2598c49ec2667d545fa581859", + "std.0.2.parquet:md5,86846e6c7f8c7b90bff7c16e72982bc6", + "std.0.3.parquet:md5,4374a082a646bcb342a90a0a964fc556", + "std.0.4.parquet:md5,bcc1a4e78e05a75b2c7971ee16e012b4", + "std.0.5.parquet:md5,69e7da0d7d4424b53a5a33f576c34026", + "std.0.6.parquet:md5,b00e8e2528472ef0474cb62b8f8ef061", + "std.0.7.parquet:md5,7f910c6ffd1d3ab8859ecb4f25f84c77", + "std.0.8.parquet:md5,ca4ac847e4ed42f7c4f020fd831d49cd", + "std.0.9.parquet:md5,9d8e8be6e8e055d896c5379ea7321cae", + "std.1.1.parquet:md5,f6f35561dce41b629ae2bf1441dba5d9", + "std.1.10.parquet:md5,2047d749abba655de2be7d3466e46db5", + "std.1.11.parquet:md5,7f2206102c9ae1fd5b71388daec380fb", + "std.1.12.parquet:md5,6cb2e9ca15aca5a3784a70539603b3bc", + "std.1.13.parquet:md5,6548f9ff93bc2bd77c337c6109517ba7", + "std.1.14.parquet:md5,a5c122bf0232356b85a631cb54ca5463", + "std.1.15.parquet:md5,61c53a4c539541a9cc481f16391cf184", + "std.1.16.parquet:md5,a654f0f440a72c371453cf4d9a61f9c5", + "std.1.2.parquet:md5,2eeece83a942a00d1716ec6f87275614", + "std.1.3.parquet:md5,44a19c3da4d1fc9a5f4c1d6612ad0e82", + "std.1.4.parquet:md5,3e91adda72bcb80a4949a724929c0855", + "std.1.5.parquet:md5,d94f0751b37c8c23a7c36f375c4def2e", + "std.1.6.parquet:md5,dc79205c0b2ed297d4296517de3a594b", + "std.1.7.parquet:md5,fe677376801f54571482394219c36186", + "std.1.8.parquet:md5,ed8f8b588a6a6ffd510f78f52f62ccf4", + "std.1.9.parquet:md5,6c13d527cee43cb93094e228305e965a", + "std.10.10.parquet:md5,c44b948928556c304cf211634fa96a30", + "std.10.11.parquet:md5,17879c3cdf884be20e0255178b95ac17", + "std.10.12.parquet:md5,3ed5619d7732ecf5aa36450b9e495544", + "std.10.13.parquet:md5,c2bfa8e7bba6234ee977d40088b422a3", + "std.10.14.parquet:md5,739810e7e26d04ed6c0d4e0d1aca861c", + "std.10.15.parquet:md5,88e42f1305fdec5874ca9b688cf8a78c", + "std.10.16.parquet:md5,1a9549a20a672e57a4c7a191ec282192", + "std.10.2.parquet:md5,dfe1915bb8c0a61751c6fda45e9ed408", + "std.10.3.parquet:md5,389ebded30f70efb867e15ef4cf736eb", + "std.10.4.parquet:md5,ea92f9211ee69e909d96b7b223075fe5", + "std.10.5.parquet:md5,198356c3a023d820e78011076c3d25b0", + "std.10.6.parquet:md5,d795250305cecfbaca44cb5d64ae8175", + "std.10.7.parquet:md5,aabd8b45b702eda1e8aa2796bee5fd53", + "std.10.8.parquet:md5,7e6098486c96be7ace982ca496bbe0bf", + "std.10.9.parquet:md5,8ea0f4c10cd271a5e6e3e193da6c541f", + "std.11.11.parquet:md5,7fc49febc643aa2cab8e8fb7763e4d92", + "std.11.12.parquet:md5,65af201da1f9fbadf3e17477e99152c2", + "std.11.13.parquet:md5,a7cb295b895f3f031566efe41f436e76", + "std.11.14.parquet:md5,2f585c9e82a279599eaa5bc2eda0607d", + "std.11.15.parquet:md5,0fa74121a8ac3e4c6e7eb8811cfb693a", + "std.11.16.parquet:md5,0a5beb03eb2e9947becd71b408db3147", + "std.11.2.parquet:md5,7d2e1746a91b3966cc43112fa1531038", + "std.11.3.parquet:md5,583e96eaf2a5d88e554652d8dd68d722", + "std.11.4.parquet:md5,07e7ce47b1fda2eda8539dbea53eda6e", + "std.11.5.parquet:md5,b5eaa57e2617e83d980b98661c05e3ce", + "std.11.6.parquet:md5,4109972649d4b377a57f8ac7d4de62bf", + "std.11.7.parquet:md5,96ea24852caa063d3d79949374113429", + "std.11.8.parquet:md5,3fb8da0084127c063ab1f8884cb4255f", + "std.11.9.parquet:md5,6cd716906739b18d7c6df3ab02195cfc", + "std.12.12.parquet:md5,fc2d81e292e7410cb929864f464a972d", + "std.12.13.parquet:md5,2bfa4d8ef0121ec3258f69554bff9d6b", + "std.12.14.parquet:md5,efbeb9fac34a1ba5d2ed2e37dcf29ab2", + "std.12.15.parquet:md5,5b7ab41c67f83cd958d277b9c2a5d3d8", + "std.12.16.parquet:md5,9f2c2764dd554c88b72e27836d68ccac", + "std.12.2.parquet:md5,ab6c71cbbcc2b0b2984b2e6c135c54e5", + "std.12.3.parquet:md5,d07a101fd175b01024006bb126eec216", + "std.12.4.parquet:md5,c631987fdd819424cc6ce2aa486fcb6a", + "std.12.5.parquet:md5,b1e61754391177b141c442cb5bd11b81", + "std.12.6.parquet:md5,71e691c5a815378713f7b0f3116837d1", + "std.12.7.parquet:md5,d637481baf36e7960f816fc65352d47a", + "std.12.8.parquet:md5,f22c75dbced152d62c46de9b576c0587", + "std.12.9.parquet:md5,b06eaac56ac9c76e6256ebe7d510eaf6", + "std.13.13.parquet:md5,dfdc0c55ac38f6e0df2af915dd658fd9", + "std.13.14.parquet:md5,40f93cdafce15e853560b44007b179e2", + "std.13.15.parquet:md5,b0840ba6b5a20bf4e60067e344b5ddf7", + "std.13.16.parquet:md5,665f9e95b59bea94e22ecd16a84c78c7", + "std.13.2.parquet:md5,191aaedaefa158ed3b86153058ebb1b9", + "std.13.3.parquet:md5,d4622758a37649a38ac05365289fa0f0", + "std.13.4.parquet:md5,8d8f5367a147ecc5c5b5030406ed6f89", + "std.13.5.parquet:md5,2a9165891821553ff89f2f2dff2c6af7", + "std.13.6.parquet:md5,7d76c2f040c6e3343f2830407d42c88a", + "std.13.7.parquet:md5,ff49f2cf17d7e81783783d9619b5647c", + "std.13.8.parquet:md5,2107ae2b43b98b10c98c9c85c322f7e0", + "std.13.9.parquet:md5,ea26155d7bb26435476fb476c8914265", + "std.14.14.parquet:md5,d0a615af385f5ec3578de354654beb4f", + "std.14.15.parquet:md5,52df8ad236c0242ccf9d0db7d0bb6869", + "std.14.16.parquet:md5,e3299d2236b45d18c5b85e28ef4064bd", + "std.14.2.parquet:md5,d4eaa873930291c2aaec25d81f67c9c0", + "std.14.3.parquet:md5,4a32df7e9bec4d69e0f7c0027ffda41b", + "std.14.4.parquet:md5,276ad4c29ab0ab8dedbedecf2124e81d", + "std.14.5.parquet:md5,cc138e58d0f1e647f0c955f305815b97", + "std.14.6.parquet:md5,03af0036a435cc4df2dd86bdc47bc1c9", + "std.14.7.parquet:md5,fbb6e97313f33cbb471699d3aac4de69", + "std.14.8.parquet:md5,6d4161f586f8eeaf90f7a7d3adb5fb83", + "std.14.9.parquet:md5,31b4b040a9873c91e68544a810857242", + "std.15.15.parquet:md5,9a0f78d7b4ac8482ab02cf94e97c8444", + "std.15.16.parquet:md5,88b95112a9c798f597af468dec2fb25a", + "std.15.2.parquet:md5,587409e4900fc42bc9ad268023f82ca6", + "std.15.3.parquet:md5,f0c470e9dec0b4af4f5160094762efa9", + "std.15.4.parquet:md5,23c5db2e48870e2dd8e70a11e2cc9d30", + "std.15.5.parquet:md5,8fb19f88ed5a8cf5e9bef99a2680794b", + "std.15.6.parquet:md5,7951f8b720a559e7f4795008b259fd5e", + "std.15.7.parquet:md5,92df6cd844d5888dc6d99763ddc8bcf4", + "std.15.8.parquet:md5,8327692f86547cfab55bc6ebcce52d99", + "std.15.9.parquet:md5,5145ea5595ce024696c6eda23cef5411", + "std.16.16.parquet:md5,a8043c3e8c9cdb30abc85158499cc554", + "std.16.2.parquet:md5,ad88763ff77d66f9250f7690ba52caf9", + "std.16.3.parquet:md5,9da7a3bf4af2f178deef5e624a27de4b", + "std.16.4.parquet:md5,908040f29bede0105833aef4d6b68093", + "std.16.5.parquet:md5,9ce0418b62220bcf5d02b64b501235aa", + "std.16.6.parquet:md5,1547fabcd56ea9fe1d9b52899331de37", + "std.16.7.parquet:md5,9acdb9e10f1ac984df5f0195ad6d4115", + "std.16.8.parquet:md5,0def299f731b56c6a4b2ff36a5045dcc", + "std.16.9.parquet:md5,f05b890abbdf0de6b24ef6231971478a", + "std.2.2.parquet:md5,ae084d6c926199cbfb6657f5352115b7", + "std.2.3.parquet:md5,192f00fd26a999e41698dae2f90bc260", + "std.2.4.parquet:md5,a5c639a6431e352bfc017277c2c07150", + "std.2.5.parquet:md5,d95a71f8cc3675675c9e05be21bf3120", + "std.2.6.parquet:md5,f8a526a4041fb104c3753970094c676f", + "std.2.7.parquet:md5,74bf61b2fb069e01a31cf3b167c924e4", + "std.2.8.parquet:md5,9092ca2f856be33bd5e95fb1a56aa5e0", + "std.2.9.parquet:md5,80d236ef711348375bdac41ecbb03473", + "std.3.3.parquet:md5,3e03e359e94cc0fcb06a7b258f7435db", + "std.3.4.parquet:md5,348e9f0e2b86dec18d1cb2adda6c2eb0", + "std.3.5.parquet:md5,1be93d5117a62bfb7a71094ef2a535e6", + "std.3.6.parquet:md5,c505077669c8b24b334e3d8e03ca5197", + "std.3.7.parquet:md5,b6fb46887c67da79977d0afd01c56f95", + "std.3.8.parquet:md5,4631a95dc3d5680d383a0fbdaf6df4de", + "std.3.9.parquet:md5,e2c555c89f0ab784c79d62f0738af02c", + "std.4.4.parquet:md5,82674d070f78879acfdea5fa296bc3c3", + "std.4.5.parquet:md5,0d7a772af7a202ad48390dd3f360a474", + "std.4.6.parquet:md5,780ceb469af1e8704e8bd6f4b183d1ff", + "std.4.7.parquet:md5,54be3ff3620c043d969bcd634dbb5553", + "std.4.8.parquet:md5,8f4f99b1a95d774f132967d46ada0d39", + "std.4.9.parquet:md5,dc71240ea39a5892409f21bc201d230d", + "std.5.5.parquet:md5,88cb2cc27066095fe16801ca60abf44b", + "std.5.6.parquet:md5,274bdb489783d44201c05af530afb676", + "std.5.7.parquet:md5,aacb7e15ae56a3df6866f2c0da14ebc3", + "std.5.8.parquet:md5,dcd3f126e1b0703c45948539d4402b2d", + "std.5.9.parquet:md5,d31d77a9cc80d8ead0766c5282c47e4a", + "std.6.6.parquet:md5,2a719245ca611936906b58b67286975c", + "std.6.7.parquet:md5,ec0115c3a4eac59d2932acc8c5e8f704", + "std.6.8.parquet:md5,b8508f2b809990d021c9e1b41ff693c2", + "std.6.9.parquet:md5,2a4a41092af439fb1be0f43eb53c549a", + "std.7.7.parquet:md5,719697dfd59560c06752d3e6795967f0", + "std.7.8.parquet:md5,c6c4d54af8e65e4898e46238f46a50a9", + "std.7.9.parquet:md5,d931b29807bf298dde4253df560c5b7f", + "std.8.8.parquet:md5,2ee5662d4150dcc6530897f8f5fb0e46", + "std.8.9.parquet:md5,2120a2cf36237082518b0a2a13446163", + "std.9.9.parquet:md5,2433f39ba1e0922d27a2f897fe2b4742" ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T17:05:31.497015342" + "timestamp": "2025-11-13T11:31:46.635883176" } } \ No newline at end of file diff --git a/tests/modules/local/aggregate_results/main.nf.test.snap b/tests/modules/local/aggregate_results/main.nf.test.snap new file mode 100644 index 00000000..40c1fbc0 --- /dev/null +++ b/tests/modules/local/aggregate_results/main.nf.test.snap @@ -0,0 +1,100 @@ +{ + "Without microarray": { + "content": [ + { + "0": [ + "all_genes_summary.csv:md5,ba17539a8a7b462f0e455dd4f81c5e62" + ], + "1": [ + "top_stable_genes_summary.csv:md5,3b49b24a0cb36b9e35b37410917de0b1" + ], + "2": [ + "all_counts_filtered.parquet:md5,286e7595b3a7b961e39976585d3c106e" + ], + "3": [ + "top_stable_genes_transposed_counts_filtered.csv:md5,0380b330c64ce9efe9644f84e7a5cddb" + ], + "4": [ + [ + "AGGREGATE_RESULTS", + "python", + "3.12.8" + ] + ], + "5": [ + [ + "AGGREGATE_RESULTS", + "polars", + "1.17.1" + ] + ], + "all_counts_filtered": [ + "all_counts_filtered.parquet:md5,286e7595b3a7b961e39976585d3c106e" + ], + "all_genes_summary": [ + "all_genes_summary.csv:md5,ba17539a8a7b462f0e455dd4f81c5e62" + ], + "top_stable_genes_summary": [ + "top_stable_genes_summary.csv:md5,3b49b24a0cb36b9e35b37410917de0b1" + ], + "top_stable_genes_transposed_counts_filtered": [ + "top_stable_genes_transposed_counts_filtered.csv:md5,0380b330c64ce9efe9644f84e7a5cddb" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-12T10:36:49.026267498" + }, + "With microarray": { + "content": [ + { + "0": [ + "all_genes_summary.csv:md5,da32872124a121e27f3dc1c784c8601b" + ], + "1": [ + "top_stable_genes_summary.csv:md5,ea79d53a1b149fa1622f85cb49aad5fc" + ], + "2": [ + "all_counts_filtered.parquet:md5,286e7595b3a7b961e39976585d3c106e" + ], + "3": [ + "top_stable_genes_transposed_counts_filtered.csv:md5,0380b330c64ce9efe9644f84e7a5cddb" + ], + "4": [ + [ + "AGGREGATE_RESULTS", + "python", + "3.12.8" + ] + ], + "5": [ + [ + "AGGREGATE_RESULTS", + "polars", + "1.17.1" + ] + ], + "all_counts_filtered": [ + "all_counts_filtered.parquet:md5,286e7595b3a7b961e39976585d3c106e" + ], + "all_genes_summary": [ + "all_genes_summary.csv:md5,da32872124a121e27f3dc1c784c8601b" + ], + "top_stable_genes_summary": [ + "top_stable_genes_summary.csv:md5,ea79d53a1b149fa1622f85cb49aad5fc" + ], + "top_stable_genes_transposed_counts_filtered": [ + "top_stable_genes_transposed_counts_filtered.csv:md5,0380b330c64ce9efe9644f84e7a5cddb" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-12T11:08:47.11957525" + } +} \ No newline at end of file diff --git a/tests/modules/local/geo/getaccessions/main.nf.test b/tests/modules/local/geo/getaccessions/main.nf.test index 6bae8a9e..93f87ba8 100644 --- a/tests/modules/local/geo/getaccessions/main.nf.test +++ b/tests/modules/local/geo/getaccessions/main.nf.test @@ -33,7 +33,7 @@ nextflow_process { input[0] = "beta_vulgaris" input[1] = "leaf" input[2] = "microarray" - input[3] = "none" + input[3] = [] input[4] = "none" """ } @@ -53,7 +53,7 @@ nextflow_process { input[0] = "arabidopsis_thaliana" input[1] = "" input[2] = "none" - input[3] = "none" + input[3] = [] input[4] = "GSE18808" """ } diff --git a/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test.snap b/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test.snap index 87e35d9e..581fe10e 100644 --- a/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test.snap +++ b/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test.snap @@ -5,7 +5,6 @@ "0": [ [ { - "accession": "E-GEOD-61690", "dataset": "E_GEOD_61690_rnaseq", "design": "E_GEOD_61690_rnaseq.design.csv:md5,ba807caf74e0b55f5c3fe23810a89560" }, @@ -13,7 +12,6 @@ ], [ { - "accession": "E-MTAB-552", "dataset": "E_MTAB_552_rnaseq", "design": "E_MTAB_552_rnaseq.design.csv:md5,b81490696f638e90c1cf14236bb0c08c" }, @@ -21,7 +19,6 @@ ], [ { - "accession": "E-MTAB-8187", "dataset": "E_MTAB_8187_rnaseq", "design": "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf" }, @@ -41,7 +38,6 @@ "downloaded_datasets": [ [ { - "accession": "E-GEOD-61690", "dataset": "E_GEOD_61690_rnaseq", "design": "E_GEOD_61690_rnaseq.design.csv:md5,ba807caf74e0b55f5c3fe23810a89560" }, @@ -49,7 +45,6 @@ ], [ { - "accession": "E-MTAB-552", "dataset": "E_MTAB_552_rnaseq", "design": "E_MTAB_552_rnaseq.design.csv:md5,b81490696f638e90c1cf14236bb0c08c" }, @@ -57,7 +52,6 @@ ], [ { - "accession": "E-MTAB-8187", "dataset": "E_MTAB_8187_rnaseq", "design": "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf" }, @@ -70,7 +64,7 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T16:41:54.19757792" + "timestamp": "2025-11-12T11:20:51.904507731" }, "Accessions only": { "content": [ @@ -119,7 +113,6 @@ "0": [ [ { - "accession": "E-GEOD-77826", "dataset": "E_GEOD_77826_rnaseq", "design": "E_GEOD_77826_rnaseq.design.csv:md5,5aa61df754aa9c6c107b247c642d2e53" }, @@ -127,7 +120,6 @@ ], [ { - "accession": "E-MTAB-4251", "dataset": "E_MTAB_4251_rnaseq", "design": "E_MTAB_4251_rnaseq.design.csv:md5,4f3ef7b76ca6ed1ec3157295bc4a8d84" }, @@ -135,7 +127,6 @@ ], [ { - "accession": "E-MTAB-4301", "dataset": "E_MTAB_4301_rnaseq", "design": "E_MTAB_4301_rnaseq.design.csv:md5,165eeef7d612c01fd62baae1a3f296ae" }, @@ -143,7 +134,6 @@ ], [ { - "accession": "E-MTAB-5038", "dataset": "E_MTAB_5038_rnaseq", "design": "E_MTAB_5038_rnaseq.design.csv:md5,352ed3163d7deef2be35d899418d5ad4" }, @@ -151,7 +141,6 @@ ], [ { - "accession": "E-MTAB-5215", "dataset": "E_MTAB_5215_rnaseq", "design": "E_MTAB_5215_rnaseq.design.csv:md5,2741dcd5b45bacce865db632f626a273" }, @@ -159,7 +148,6 @@ ], [ { - "accession": "E-MTAB-7711", "dataset": "E_MTAB_7711_rnaseq", "design": "E_MTAB_7711_rnaseq.design.csv:md5,3e7748b54a0c25c008d9bd2ddbf1bf00" }, @@ -193,7 +181,6 @@ "downloaded_datasets": [ [ { - "accession": "E-GEOD-77826", "dataset": "E_GEOD_77826_rnaseq", "design": "E_GEOD_77826_rnaseq.design.csv:md5,5aa61df754aa9c6c107b247c642d2e53" }, @@ -201,7 +188,6 @@ ], [ { - "accession": "E-MTAB-4251", "dataset": "E_MTAB_4251_rnaseq", "design": "E_MTAB_4251_rnaseq.design.csv:md5,4f3ef7b76ca6ed1ec3157295bc4a8d84" }, @@ -209,7 +195,6 @@ ], [ { - "accession": "E-MTAB-4301", "dataset": "E_MTAB_4301_rnaseq", "design": "E_MTAB_4301_rnaseq.design.csv:md5,165eeef7d612c01fd62baae1a3f296ae" }, @@ -217,7 +202,6 @@ ], [ { - "accession": "E-MTAB-5038", "dataset": "E_MTAB_5038_rnaseq", "design": "E_MTAB_5038_rnaseq.design.csv:md5,352ed3163d7deef2be35d899418d5ad4" }, @@ -225,7 +209,6 @@ ], [ { - "accession": "E-MTAB-5215", "dataset": "E_MTAB_5215_rnaseq", "design": "E_MTAB_5215_rnaseq.design.csv:md5,2741dcd5b45bacce865db632f626a273" }, @@ -233,7 +216,6 @@ ], [ { - "accession": "E-MTAB-7711", "dataset": "E_MTAB_7711_rnaseq", "design": "E_MTAB_7711_rnaseq.design.csv:md5,3e7748b54a0c25c008d9bd2ddbf1bf00" }, @@ -246,7 +228,7 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T16:41:16.46799497" + "timestamp": "2025-11-12T11:20:03.290745058" }, "No accesssion + no keywords + multiple dataset species": { "content": [ @@ -254,7 +236,6 @@ "0": [ [ { - "accession": "E-MTAB-8187", "dataset": "E_MTAB_8187_rnaseq", "design": "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf" }, @@ -270,7 +251,6 @@ "downloaded_datasets": [ [ { - "accession": "E-MTAB-8187", "dataset": "E_MTAB_8187_rnaseq", "design": "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf" }, @@ -283,6 +263,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T16:41:39.903564263" + "timestamp": "2025-11-12T11:20:34.963087914" } } \ No newline at end of file diff --git a/tests/test_data/input_datasets/input_big.yaml b/tests/test_data/input_datasets/input_big.yaml deleted file mode 100644 index a8a4764b..00000000 --- a/tests/test_data/input_datasets/input_big.yaml +++ /dev/null @@ -1,4 +0,0 @@ -- counts: https://raw.githubusercontent.com/nf-core/test-datasets/differentialabundance/modules_testdata/SRP254919.salmon.merged.gene_counts.top1000cov.assay.tsv - design: tests/test_data/input_datasets/rnaseq_big.design.csv - platform: rnaseq - normalised: false From 2c07114a3f6b00606652fb0ef576efc915268e07 Mon Sep 17 00:00:00 2001 From: Olivier Date: Fri, 14 Nov 2025 18:13:42 +0100 Subject: [PATCH 157/258] allow and improve downloading of RNAseq data from GEO --- bin/download_eatlas_data.R | 7 +- bin/download_geo_data.R | 281 +++++++++++++++--- bin/get_geo_dataset_accessions.py | 7 +- .../main.nf | 5 +- tests/modules/local/geo/getdata/main.nf.test | 48 +++ 5 files changed, 291 insertions(+), 57 deletions(-) diff --git a/bin/download_eatlas_data.R b/bin/download_eatlas_data.R index 9b738d92..e41ba82a 100755 --- a/bin/download_eatlas_data.R +++ b/bin/download_eatlas_data.R @@ -178,13 +178,14 @@ process_data <- function(atlas_data, accession) { } else if ( startsWith(data_type, 'A-') ) { # typically: A-AFFY- or A-GEOD- result <- get_one_colour_microarray_data(data) } else { - write(paste("UNKNOWN DATA TYPE:", data_type), file = FAILURE_REASON_FILE) - quit(save = "no", status = 0) + warning(paste("Unknown data type:", data_type)) + write(paste("UNKNOWN DATA TYPE:", data_type), file = WARNING_REASON_FILE, append=TRUE) + skip_iteration <<- TRUE } }, error = function(e) { warning(paste("Caught an error: ", e$message)) - write(paste('ERROR: COULD NOT GET ASSAY DATA FOR EXPERIMENT ID', accession, 'AND DATA TYPE', data_type), file = WARNING_REASON_FILE) + write(paste('ERROR: COULD NOT GET ASSAY DATA FOR EXPERIMENT ID', accession, 'AND DATA TYPE', data_type), file = WARNING_REASON_FILE, append=TRUE) skip_iteration <<- TRUE }) diff --git a/bin/download_geo_data.R b/bin/download_geo_data.R index 53054b2a..cb8628e2 100755 --- a/bin/download_geo_data.R +++ b/bin/download_geo_data.R @@ -4,9 +4,11 @@ suppressPackageStartupMessages(library("GEOquery")) suppressPackageStartupMessages(library("dplyr")) +suppressPackageStartupMessages(library("tibble")) library(GEOquery) library(optparse) library(dplyr) +library(tibble) options(error = traceback) @@ -22,7 +24,7 @@ WARNING_REASON_FILE <- "warning_reason.txt" get_args <- function() { option_list <- list( make_option("--accession", type = "character", help = "Accession number of GEO dataset. Example: GSE56413"), - make_option("--species", type = "character", help = "Accession number of GEO dataset. Example: GSE56413") + make_option("--species", type = "character", help = "Species name") ) args <- parse_args(OptionParser( @@ -33,6 +35,24 @@ get_args <- function() { } +get_experiment_type <- function(data) { + e = experimentData(data) + experiment_type <- tolower(attr(e, "other")$type) + if (experiment_type == "expression profiling by high throughput sequencing") { + return("rnaseq") + } else if (experiment_type == "expression profiling by array") { + return("microarray") + } else { + return(gsub("\n", " ; ", experiment_type)) + } +} + +get_platform_id <- function(data) { + platform_id <- as.character(unique(pData(data)$platform_id))[1] + return(platform_id) +} + + format_species_name <- function(x) { x <- tools::toTitleCase(x) x <- gsub("[_-]", " ", x) @@ -40,14 +60,14 @@ format_species_name <- function(x) { } -get_samples_for_species <- function(eset, species) { - pheno <- pData(eset) +get_samples_for_species <- function(data, species) { + pheno <- pData(data) # check if organism_ch2 exists if ("organism_ch2" %in% colnames(pheno)) { - keep <- pheno$organism_ch1 == format_species_name(species) & pheno$organism_ch2 == format_species_name(species) + keep <- pheno$organism_ch1 == species & pheno$organism_ch2 == species } else { - keep <- pheno$organism_ch1 == format_species_name(species) + keep <- pheno$organism_ch1 == species } # return a data.frame with matching samples @@ -77,8 +97,6 @@ get_columns_for_grouping <- function(df) { build_design_dataframe <- function(df, accession) { - message("Build design dataframe") - columns_to_group <- get_columns_for_grouping(df) design_df <- df %>% @@ -96,6 +114,20 @@ build_design_dataframe <- function(df, accession) { return(design_df) } +make_design <- function(data, accession, species) { + metadata_df <- pData(data) + design_df <- build_design_dataframe(metadata_df, accession) + + # get samples corresponding to species + species_samples <- get_samples_for_species(data, species) + + # filter design dataframe + design_df <- design_df %>% + filter(sample %in% species_samples) + + return(design_df) +} + download_geo_data_with_retries <- function(accession, species, max_retries = 3, wait_time = 5) { @@ -130,6 +162,11 @@ download_geo_data_with_retries <- function(accession, species, max_retries = 3, } +write_warning <- function(msg) { + file_conn <- file( WARNING_REASON_FILE, open = "a") + cat(paste0(msg, "; "), file = file_conn, sep = "", fill = FALSE) + close(file_conn) +} check_microarray_normalisation <- function(df) { @@ -143,13 +180,13 @@ check_microarray_normalisation <- function(df) { message("Normalized, log2 scale (e.g. RMA, quantile)") } else if (all_integers) { message("Raw probe intensities (unnormalized CEL-like data)") - write("RAW PROBE INTENSITIES FOUND", file = WARNING_REASON_FILE) + write_warning("RAW PROBE INTENSITIES FOUND") } else if (value_range[2] > 1000) { message("Normalized but not log-transformed (e.g. MAS5, raw intensities)") - write("PARSED INTENSITIES: NORMALIZED BUT NOT LOG-TRANSFORMED", file = WARNING_REASON_FILE) + write_warning("PARSED INTENSITIES: NORMALIZED BUT NOT LOG-TRANSFORMED") } else { message("Unclear data origin, check GEO metadata") - write("UNCLEAR DATA ORIGIN: CHECK GEO METADATA", file = WARNING_REASON_FILE) + write_warning("UNCLEAR DATA ORIGIN: CHECK GEO METADATA") } } @@ -157,72 +194,222 @@ check_microarray_normalisation <- function(df) { clean_count_data <- function(df) { message("Cleaning counts") # removes rows that are all NA - df <- df[rowSums(!is.na(df)) > 0, ] + df <- df[rowSums(!is.na(df)) > 0, , drop = FALSE] return(df) } -process_data <- function(geo_data, accession, species) { +get_microarray_counts <- function(data, design_df) { + # get count data corresponding to samples in the design + count_df <- data.frame(exprs(data)) %>% + select(all_of(list(design_df$sample))) + return(count_df) +} - for (i in 1:length(geo_data)) { - eset <- geo_data[[ i ]] +get_extensions <- function(file){ + extensions <- strsplit(basename(file), split="\\.")[[1]] + return(extensions) +} - #print(exprs(eset)) - # Get metadata table - metadata_df <- pData(eset) - design_df <- build_design_dataframe(metadata_df, accession) - # get samples corresponding to species - species_samples <- get_samples_for_species(eset, species) +get_rnaseq_counts <- function(data, design_df) { + pheno_data <- pData(data) - # filter design dataframe - design_df <- design_df %>% - filter(sample %in% species_samples) + valid_samples <- as.character(design_df$sample) + filtered_pdata <- pheno_data[ rownames(pheno_data) %in% valid_samples, ] + if ( nrow(filtered_pdata) == 0 ) { + return(data.frame()) + } - file <- names(geo_data)[[ i ]] + filtered_samples <- rownames(filtered_pdata) + suppl_data <- list(filtered_pdata$supplementary_file_1)[[1]] - data <- geo_data [[ file ]] + count_df_list <- list() + for (i in 1:length(filtered_samples)) { - # keeping only non empty data - if (nrow(data) == 0) { - message(paste0("No data found for ", file)) + sample <- filtered_samples[[i]] + url <- suppl_data[[i]] + + if ( tolower(url) == "none" || is.na(url) || url == "") { + message(paste("Skipping sample", sample, "because no supplementary file is provided")) + write_warning(paste("NO SUPPLEMENTARY FILE:", sample)) + next + } + + filename <- tolower(basename(url)) + extensions <- get_extensions(filename) + ext <- extensions[length(extensions)] + if (ext == "gz") { + ext <- extensions[length(extensions) - 1] + } + if (!(ext %in% c("txt", "tsv", "csv", "tab"))) { + message(paste("Extension not supported:", filename)) + write_warning(paste("UNSUPPORTED EXTENSION:", ext)) next } - # get count data for samples corresponding to the species of interest - count_df <- data.frame(exprs(data)) %>% - select(all_of(species_samples)) + # skipping if it is obviously TPMs / FPKMs / RPKMs + if (grepl("tpm", filename) | grepl("fpkm", filename) | grepl("rpkm", filename)) { + message(paste("Skipping already normalised file", filename)) + write_warning(paste("ALREADY NORMALIZED:", filename)) + next + } - # keeping only non empty data - if (nrow(count_df) == 0 || ncol(count_df) == 0) { - message(paste0("No data found for ", file)) - next + skip_iteration <<- FALSE + message(paste("Downloading", filename)) + tryCatch({ + download.file(url, filename, method = "wget", quiet = TRUE) + }, error = function(e) { + message(paste("Unhandled error while downloading", filename, " : ", e$message)) + write_warning(paste("ERROR WHILE DOWNLOADING:", filename)) + skip_iteration <<- TRUE + }) + + # If an error occurred, skip to the next iteration + if (skip_iteration) { + next + } + + separator <- NULL + for (sep in c("\t", ",", " ")) { + # parsing the first line to determine the separator and see if there is a header + counts <- read.table(filename, header = FALSE, sep = sep, row.names = 1, nrows = 1) + if (ncol(counts) > 0) { + separator <- sep + if (is.numeric(counts[1, 1])) { + has_header <- FALSE + } else { + has_header <- TRUE + } + break + } + } + + if (is.null(separator)) { + message(paste("Skipping file with no valid separator", filename)) + write_warning(paste("NO VALID SEPARATOR:", filename)) + next } - # checking that data are from RMA pipeline and followed proper normalisation - # raises error otherwise - check_microarray_normalisation(count_df) + counts <- read.table(filename, header = has_header, sep = separator, row.names = 1) + # checking number of columns + if (ncol(counts) == 1) { + colnames(counts) <- c(sample) + } else { + # TODO: see how to handle multiple columns + #colnames(counts) <- paste0(sample, "_", 1:ncol(counts)) + write_warning(paste("MULTIPLE COUNT COLUMNS:", filename)) + next + } + + # checking type of values + is_all_integer <- function(x) all(floor(x) == x) + int_counts <- counts %>% select_if(is_all_integer) + + # if some values were not integers + if (nrow(int_counts) < nrow(counts)) { + message(paste("Skipping non-integer file", filename)) + write_warning(paste("NOT ALL INTEGERS:", filename)) + #next + } + + # resetting the index + counts <- tibble::rownames_to_column(counts, var = "gene_id") + count_df_list[[i]] <- counts + } + + # checking if all files were skipped + if (length(count_df_list) == 0) { + message("No valid files found") + return(data.frame()) + } + + # full outer join + joined_df <- Reduce( + function(df1, df2) merge(df1, df2, by = "gene_id", all = TRUE), + count_df_list + ) + joined_df <- tibble::column_to_rownames(joined_df, var = "gene_id") + + return(joined_df) +} + + +process_data <- function(geo_data, accession, species) { + + for (i in 1:length(geo_data)) { + + data <- geo_data[[ i ]] + file <- names(geo_data)[[ i ]] + + platform_id <- get_platform_id(data) + experiment_type <- get_experiment_type(data) + + if ( experiment_type == "microarray") { + message(paste("Processing microarray data:", file)) + + # keeping only non empty data + if (nrow(data) == 0) { + write_warning(paste("NO DATA:", file)) + next + } + + # make design dataframe + # keep only samples corresponding to the species of interest + design_df <- make_design(data, accession, species) + + count_df <- get_microarray_counts(data, design_df) + + # keeping only non empty data + if (nrow(count_df) == 0 || ncol(count_df) == 0) { + write_warning(paste("NO DATA AFTER FILTERING:", file)) + next + } + + # checking that data are from RMA pipeline and followed proper normalisation + check_microarray_normalisation(count_df) + + } else if (experiment_type == "rnaseq") { + message(paste("Processing RNA-seq data:", file)) + # make design dataframe + # keep only samples corresponding to the species of interest + design_df <- make_design(data, accession, species) + + count_df <- get_rnaseq_counts(data, design_df) + + # keeping only non empty data + if (nrow(count_df) == 0 || ncol(count_df) == 0) { + message(paste("No data found for", file)) + write_warning(paste("NO DATA:", file)) + next + } + + } else { + message(paste("Unsupported platform:", experiment_type)) + write_warning(paste("UNSUPPORTED PLATFORM:", experiment_type)) + next + } # clean counts: # * removes rows that are all NA count_df <- clean_count_data(count_df) # exporting count data to CSV - export_count_data(count_df, accession) + export_count_data(count_df, accession, experiment_type, platform_id) # exporting metadata to CSV - export_metadata(design_df, accession) + export_design(design_df, accession, experiment_type, platform_id) } } -export_count_data <- function(count_df, batch_id) { +export_count_data <- function(count_df, accession, experiment_type, platform_id) { # renaming columns, to make them specific to accession and data type - colnames(count_df) <- paste0(batch_id, '_', colnames(count_df)) + colnames(count_df) <- paste0(accession, '_', colnames(count_df)) - outfilename <- paste0(batch_id, '.microarray.normalised.counts.csv') + outfilename <- paste0(accession, '_', platform_id, '.', experiment_type, '.normalised.counts.csv') # exporting to CSV file # index represents gene names @@ -230,15 +417,15 @@ export_count_data <- function(count_df, batch_id) { write.table(count_df, outfilename, sep = ',', row.names = TRUE, col.names = TRUE, quote = FALSE) } -export_metadata <- function(design_df, batch_id) { +export_design <- function(design_df, accession, experiment_type, platform_id) { - new_sample_names <- paste0(batch_id, '_', design_df$sample) + new_sample_names <- paste0(accession, '_', design_df$sample) df <- design_df %>% mutate(sample = new_sample_names ) %>% select(sample, condition, batch) - outfilename <- paste0(batch_id, '.design.csv') + outfilename <- paste0(accession, '_', platform_id, '.', experiment_type, '.design.csv') message(paste('Exporting design data to file', outfilename)) write.table(df, outfilename, sep = ',', row.names = FALSE, col.names = TRUE, quote = FALSE) } @@ -258,4 +445,4 @@ species <- format_species_name(args$species) # searching and downloading expression atlas data geo_data <- download_geo_data_with_retries(args$accession, species) -process_data(geo_data, args$accession, args$species) +process_data(geo_data, args$accession, species) diff --git a/bin/get_geo_dataset_accessions.py b/bin/get_geo_dataset_accessions.py index b9e39a83..7ee9f0de 100755 --- a/bin/get_geo_dataset_accessions.py +++ b/bin/get_geo_dataset_accessions.py @@ -54,14 +54,11 @@ SUPERSERIES_SUMMARY = "This SuperSeries is composed of the SubSeries listed below." ALLOWED_LIBRARY_SOURCES = ["transcriptomic", "RNA"] -ALLOWED_MOLECULE_TYPES = [ - "RNA", - # "SRA" -] +ALLOWED_MOLECULE_TYPES = ["RNA", "SRA"] GEO_EXPERIMENT_TYPE_TO_PLATFORM = { "Expression profiling by array": "microarray", - # "Expression profiling by high throughput sequencing": "rnaseq", + "Expression profiling by high throughput sequencing": "rnaseq", } MINIML_TMPDIR = "geo_miniml" diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index 6ce914b4..baeb54e7 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -414,8 +414,9 @@ def checkCounts(ch_counts) { // display a warning if no datasets are found def msg = ( "No dataset found. " - + "Please note that for the moment only Microarray count datasets are fetched from NCBI GEO. " - + "\nYou can check at https://www.ncbi.nlm.nih.gov/gds if there are raw RNA-seq count datasets for this species. " + + "\nYou may want to check at https://www.ncbi.nlm.nih.gov/gds if there are datasets for this species that you can prepare yourself. " + + "\nOnce you have prepared your own data, you can relaunch the pipeline with the --datasets parameter." + + "\nFor more information, see the online documentation at https://nf-co.re/stableexpression." ) ch_counts.count().map { n -> if( n == 0 ) { diff --git a/tests/modules/local/geo/getdata/main.nf.test b/tests/modules/local/geo/getdata/main.nf.test index 01f89ffb..98f52dc1 100644 --- a/tests/modules/local/geo/getdata/main.nf.test +++ b/tests/modules/local/geo/getdata/main.nf.test @@ -53,4 +53,52 @@ nextflow_process { } + test("Drosophila simulans - Only one sample among several") { + + when { + + process { + """ + input[0] = [ + [ id: "test" ], + "GSE59707" + ] + input[1] = "drosophila_simulans" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("Drosophila simulans - No data found") { + + when { + + process { + """ + input[0] = [ + [ id: "test" ], + "GSE124142" + ] + input[1] = "drosophila_simulans" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + } From 660214c0dc3d5d7be95cb439a32aec24e8f1fe2a Mon Sep 17 00:00:00 2001 From: Olivier Date: Sun, 16 Nov 2025 12:04:52 +0100 Subject: [PATCH 158/258] force integer count values after mapping to ensembl gene IDs --- bin/download_geo_data.R | 252 ++++++++++--------- bin/map_ids_to_ensembl.py | 13 +- tests/modules/local/geo/getdata/main.nf.test | 72 ++++++ 3 files changed, 215 insertions(+), 122 deletions(-) diff --git a/bin/download_geo_data.R b/bin/download_geo_data.R index cb8628e2..a77dcc3a 100755 --- a/bin/download_geo_data.R +++ b/bin/download_geo_data.R @@ -47,12 +47,19 @@ get_experiment_type <- function(data) { } } -get_platform_id <- function(data) { - platform_id <- as.character(unique(pData(data)$platform_id))[1] + +get_platform_id <- function(pdata) { + platform_id <- as.character(unique(pdata$platform_id))[1] return(platform_id) } +get_rnaseq_samples <- function(pdata, valid_samples) { + filtered_pdata <- pdata[ rownames(pdata) %in% valid_samples & pdata$library_strategy == "RNA-Seq", ] + return(rownames(filtered_pdata)) +} + + format_species_name <- function(x) { x <- tools::toTitleCase(x) x <- gsub("[_-]", " ", x) @@ -60,18 +67,16 @@ format_species_name <- function(x) { } -get_samples_for_species <- function(data, species) { - pheno <- pData(data) - +get_samples_for_species <- function(pdata, species) { # check if organism_ch2 exists - if ("organism_ch2" %in% colnames(pheno)) { - keep <- pheno$organism_ch1 == species & pheno$organism_ch2 == species + if ("organism_ch2" %in% colnames(pdata)) { + keep <- pdata$organism_ch1 == species & pdata$organism_ch2 == species } else { - keep <- pheno$organism_ch1 == species + keep <- pdata$organism_ch1 == species } # return a data.frame with matching samples - return(pheno$geo_accession[keep]) + return(pdata$geo_accession[keep]) } @@ -100,12 +105,12 @@ build_design_dataframe <- function(df, accession) { columns_to_group <- get_columns_for_grouping(df) design_df <- df %>% - mutate(sample = rownames(.)) %>% - group_by(!!!syms(columns_to_group)) %>% - mutate(group_num = cur_group_id()) %>% + mutate(sample = geo_accession) %>% # change column name geo_accession to sample + group_by(!!!syms(columns_to_group)) %>% # group by all columns for grouping found + mutate(group_num = cur_group_id()) %>% # create column made from group id ungroup() %>% mutate( - condition = paste0("G", group_num), + condition = paste0("G", group_num), # create condition column from group number batch = accession ) %>% select(sample, condition, batch) %>% @@ -114,12 +119,12 @@ build_design_dataframe <- function(df, accession) { return(design_df) } -make_design <- function(data, accession, species) { - metadata_df <- pData(data) - design_df <- build_design_dataframe(metadata_df, accession) +make_design <- function(pdata, accession, species) { + + design_df <- build_design_dataframe(pdata, accession) # get samples corresponding to species - species_samples <- get_samples_for_species(data, species) + species_samples <- get_samples_for_species(pdata, species) # filter design dataframe design_df <- design_df %>% @@ -200,9 +205,10 @@ clean_count_data <- function(df) { get_microarray_counts <- function(data, design_df) { + # get count data corresponding to samples in the design count_df <- data.frame(exprs(data)) %>% - select(all_of(list(design_df$sample))) + select(all_of(design_df$sample)) return(count_df) } @@ -213,110 +219,112 @@ get_extensions <- function(file){ } -get_rnaseq_counts <- function(data, design_df) { - pheno_data <- pData(data) +get_rnaseq_counts <- function(pdata, design_df) { - valid_samples <- as.character(design_df$sample) - filtered_pdata <- pheno_data[ rownames(pheno_data) %in% valid_samples, ] - if ( nrow(filtered_pdata) == 0 ) { - return(data.frame()) - } + # getting list of samples + samples <- pdata$geo_accession - filtered_samples <- rownames(filtered_pdata) - suppl_data <- list(filtered_pdata$supplementary_file_1)[[1]] + # getting list of columns corresponding to supp data + supplementary_cols <- grep("^supplementary_file(_\\d+)?$", names(pdata), value = TRUE) count_df_list <- list() - for (i in 1:length(filtered_samples)) { + cpt = 1 + for (i in 1:length(samples)) { - sample <- filtered_samples[[i]] - url <- suppl_data[[i]] + sample <- samples[[i]] - if ( tolower(url) == "none" || is.na(url) || url == "") { - message(paste("Skipping sample", sample, "because no supplementary file is provided")) - write_warning(paste("NO SUPPLEMENTARY FILE:", sample)) - next - } + for (j in 1:length(supplementary_cols)) { - filename <- tolower(basename(url)) - extensions <- get_extensions(filename) - ext <- extensions[length(extensions)] - if (ext == "gz") { - ext <- extensions[length(extensions) - 1] - } - if (!(ext %in% c("txt", "tsv", "csv", "tab"))) { - message(paste("Extension not supported:", filename)) - write_warning(paste("UNSUPPORTED EXTENSION:", ext)) - next - } + url <- pdata[pdata$geo_accession == sample, supplementary_cols[j]] - # skipping if it is obviously TPMs / FPKMs / RPKMs - if (grepl("tpm", filename) | grepl("fpkm", filename) | grepl("rpkm", filename)) { - message(paste("Skipping already normalised file", filename)) - write_warning(paste("ALREADY NORMALIZED:", filename)) - next - } + if ( tolower(url) == "none" || is.na(url) || url == "") { + message(paste("Skipping sample", sample, "because no supplementary file is provided")) + write_warning(paste("NO SUPPLEMENTARY FILE:", sample)) + next + } - skip_iteration <<- FALSE - message(paste("Downloading", filename)) - tryCatch({ - download.file(url, filename, method = "wget", quiet = TRUE) - }, error = function(e) { - message(paste("Unhandled error while downloading", filename, " : ", e$message)) - write_warning(paste("ERROR WHILE DOWNLOADING:", filename)) - skip_iteration <<- TRUE - }) + filename <- tolower(basename(url)) + extensions <- get_extensions(filename) + ext <- extensions[length(extensions)] + if (ext == "gz") { + ext <- extensions[length(extensions) - 1] + } + if (!(ext %in% c("txt", "tsv", "csv", "tab"))) { + message(paste("Extension not supported:", filename)) + write_warning(paste("UNSUPPORTED EXTENSION:", ext)) + next + } - # If an error occurred, skip to the next iteration - if (skip_iteration) { - next - } + # skipping if it is obviously TPMs / FPKMs / RPKMs + if (grepl("tpm", filename) | grepl("fpkm", filename) | grepl("rpkm", filename)) { + message(paste("Skipping already normalised file", filename)) + write_warning(paste("ALREADY NORMALIZED:", filename)) + next + } - separator <- NULL - for (sep in c("\t", ",", " ")) { - # parsing the first line to determine the separator and see if there is a header - counts <- read.table(filename, header = FALSE, sep = sep, row.names = 1, nrows = 1) - if (ncol(counts) > 0) { - separator <- sep - if (is.numeric(counts[1, 1])) { - has_header <- FALSE - } else { - has_header <- TRUE + skip_iteration <<- FALSE + message(paste("Downloading", filename)) + tryCatch({ + download.file(url, filename, method = "wget", quiet = TRUE) + }, error = function(e) { + message(paste("Unhandled error while downloading", filename, " : ", e$message)) + write_warning(paste("ERROR WHILE DOWNLOADING:", filename)) + skip_iteration <<- TRUE + }) + + # If an error occurred, skip to the next iteration + if (skip_iteration) { + next + } + + separator <- NULL + for (sep in c("\t", ",", " ")) { + # parsing the first line to determine the separator and see if there is a header + counts <- read.table(filename, header = FALSE, sep = sep, row.names = 1, nrows = 1) + if (ncol(counts) > 0) { + separator <- sep + if (is.numeric(counts[1, 1])) { + has_header <- FALSE + } else { + has_header <- TRUE + } + break } - break } - } - if (is.null(separator)) { - message(paste("Skipping file with no valid separator", filename)) - write_warning(paste("NO VALID SEPARATOR:", filename)) - next - } + if (is.null(separator)) { + message(paste("Skipping file with no valid separator", filename)) + write_warning(paste("NO VALID SEPARATOR:", filename)) + next + } - counts <- read.table(filename, header = has_header, sep = separator, row.names = 1) - # checking number of columns - if (ncol(counts) == 1) { - colnames(counts) <- c(sample) - } else { - # TODO: see how to handle multiple columns - #colnames(counts) <- paste0(sample, "_", 1:ncol(counts)) - write_warning(paste("MULTIPLE COUNT COLUMNS:", filename)) - next - } + counts <- read.table(filename, header = has_header, sep = separator, row.names = 1) + # checking number of columns + if (ncol(counts) == 1) { + colnames(counts) <- c(sample) + } else { + # TODO: see how to handle multiple columns + #colnames(counts) <- paste0(sample, "_", 1:ncol(counts)) + write_warning(paste("MULTIPLE COUNT COLUMNS:", filename)) + next + } - # checking type of values - is_all_integer <- function(x) all(floor(x) == x) - int_counts <- counts %>% select_if(is_all_integer) + # checking type of values + is_all_integer <- function(x) all(floor(x) == x) + int_counts <- counts %>% select_if(is_all_integer) - # if some values were not integers - if (nrow(int_counts) < nrow(counts)) { - message(paste("Skipping non-integer file", filename)) - write_warning(paste("NOT ALL INTEGERS:", filename)) - #next - } + # if some values were not integers + if (nrow(int_counts) < nrow(counts)) { + message(paste("Skipping non-integer file", filename)) + write_warning(paste("NOT ALL INTEGERS:", filename)) + next + } - # resetting the index - counts <- tibble::rownames_to_column(counts, var = "gene_id") - count_df_list[[i]] <- counts + # resetting the index + counts <- tibble::rownames_to_column(counts, var = "gene_id") + count_df_list[[cpt]] <- counts + cpt = cpt + 1 + } } # checking if all files were skipped @@ -343,21 +351,26 @@ process_data <- function(geo_data, accession, species) { data <- geo_data[[ i ]] file <- names(geo_data)[[ i ]] - platform_id <- get_platform_id(data) - experiment_type <- get_experiment_type(data) + pdata <- pData(data) + #print(pdata) + # make design dataframe + # keep only samples corresponding to the species of interest + design_df <- make_design(pdata, accession, species) + valid_samples <- as.character(design_df$sample) - if ( experiment_type == "microarray") { - message(paste("Processing microarray data:", file)) + if (length(valid_samples) == 0) { + message(paste("No samples for required species:", file)) + next + } - # keeping only non empty data - if (nrow(data) == 0) { - write_warning(paste("NO DATA:", file)) - next - } - # make design dataframe - # keep only samples corresponding to the species of interest - design_df <- make_design(data, accession, species) + platform_id <- get_platform_id(pdata) + rnaseq_samples <- get_rnaseq_samples(pdata, valid_samples) + + experiment_type <- get_experiment_type(data) + + if ( experiment_type == "microarray" ) { + message(paste("Processing microarray data:", file)) count_df <- get_microarray_counts(data, design_df) @@ -370,13 +383,10 @@ process_data <- function(geo_data, accession, species) { # checking that data are from RMA pipeline and followed proper normalisation check_microarray_normalisation(count_df) - } else if (experiment_type == "rnaseq") { + } else if ( experiment_type == "rnaseq" || length(rnaseq_samples) > 0 ) { message(paste("Processing RNA-seq data:", file)) - # make design dataframe - # keep only samples corresponding to the species of interest - design_df <- make_design(data, accession, species) - count_df <- get_rnaseq_counts(data, design_df) + count_df <- get_rnaseq_counts(pdata, design_df) # keeping only non empty data if (nrow(count_df) == 0 || ncol(count_df) == 0) { diff --git a/bin/map_ids_to_ensembl.py b/bin/map_ids_to_ensembl.py index 81e1aae4..e53ed142 100755 --- a/bin/map_ids_to_ensembl.py +++ b/bin/map_ids_to_ensembl.py @@ -80,6 +80,7 @@ def main(): # PARSING FILES ############################################################# + # whatever the name of the first col, rename it to "ensembl_gene_id" df = parse_table(count_file, index_col=0) df.index.rename(config.ENSEMBL_GENE_ID_COLNAME, inplace=True) @@ -150,7 +151,17 @@ def main(): # TODO: check is there is another way to avoid duplicate gene names # sometimes different gene names have the same ensembl ID # for now, we just get the mean of values, but this is not ideal - df = df.groupby(config.ENSEMBL_GENE_ID_COLNAME, as_index=False).mean() + + ############################################################# + # GENE COUNT HANDLING + ############################################################# + + # handling cases where multiple genes have the same ensembl ID + # since subsequent steps in the pipeline require integer values, + # we need to ensure that the resulting DataFrame has integer values + df = df.groupby(config.ENSEMBL_GENE_ID_COLNAME, as_index=False, sort=False).agg( + lambda x: x.mean().astype(int) + ) ############################################################# # WRITING OUTFILES diff --git a/tests/modules/local/geo/getdata/main.nf.test b/tests/modules/local/geo/getdata/main.nf.test index 98f52dc1..b6d19420 100644 --- a/tests/modules/local/geo/getdata/main.nf.test +++ b/tests/modules/local/geo/getdata/main.nf.test @@ -101,4 +101,76 @@ nextflow_process { } + test("Drosophila simulans - Expression profiling by array") { + + when { + + process { + """ + input[0] = [ + [ id: "test" ], + "GSE43665" + ] + input[1] = "drosophila_simulans" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("Drosophila simulans - Expression profiling by high throughput sequencing / Some raw counts found") { + + when { + + process { + """ + input[0] = [ + [ id: "test" ], + "GSE59707" + ] + input[1] = "drosophila_simulans" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("Drosophila simulans - Expression profiling by high throughput sequencing / One raw count found") { + + when { + + process { + """ + input[0] = [ + [ id: "test" ], + "GSE100837" + ] + input[1] = "drosophila_simulans" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + } From 404f85a372ce56c95d863a7590183b3e305b8d07 Mon Sep 17 00:00:00 2001 From: Olivier Date: Sun, 16 Nov 2025 12:06:46 +0100 Subject: [PATCH 159/258] functional download and parsing of rnaseq data from GEO --- bin/download_geo_data.R | 742 ++++++++++++------ modules/local/geo/getdata/main.nf | 3 +- .../main.nf | 7 +- 3 files changed, 503 insertions(+), 249 deletions(-) diff --git a/bin/download_geo_data.R b/bin/download_geo_data.R index a77dcc3a..2c3f9654 100755 --- a/bin/download_geo_data.R +++ b/bin/download_geo_data.R @@ -5,19 +5,27 @@ suppressPackageStartupMessages(library("GEOquery")) suppressPackageStartupMessages(library("dplyr")) suppressPackageStartupMessages(library("tibble")) +suppressPackageStartupMessages(library("stringr")) library(GEOquery) library(optparse) library(dplyr) library(tibble) +library(stringr) options(error = traceback) +COUNT_FILE_EXTENSION <- ".counts.csv" +DESIGN_FILE_EXTENSION <- ".design.csv" +MAPPING_FILE_EXTENSION <- ".sample_name_mapping.csv" +METADATA_FILE_EXTENSION <- ".platform_metadata.csv" +BASE_REJECTED_DIR <- "rejected" + FAILURE_REASON_FILE <- "failure_reason.txt" WARNING_REASON_FILE <- "warning_reason.txt" ##################################################### ##################################################### -# FUNCTIONS +# ARG PARSER ##################################################### ##################################################### @@ -35,9 +43,91 @@ get_args <- function() { } -get_experiment_type <- function(data) { - e = experimentData(data) - experiment_type <- tolower(attr(e, "other")$type) +##################################################### +##################################################### +# UTILS +##################################################### +##################################################### + +format_species_name <- function(x) { + x <- tools::toTitleCase(x) + x <- gsub("[_-]", " ", x) + return(x) +} + +write_warning <- function(msg) { + message(msg) + file_conn <- file( WARNING_REASON_FILE, open = "a") + cat(paste0(msg, "; "), file = file_conn, sep = "", fill = FALSE) + close(file_conn) +} + + +get_extensions <- function(file){ + extensions <- strsplit(basename(file), split="\\.")[[1]] + return(extensions) +} + + +get_rejected_dir <- function(platform, series) { + rejected_dir <- file.path(BASE_REJECTED_DIR, paste0(series$accession, '_', platform$id)) + dir.exists(rejected_dir) || dir.create(rejected_dir, recursive = TRUE) + return(rejected_dir) +} + +##################################################### +##################################################### +# DOWNLOAD +##################################################### +##################################################### + +download_geo_data_with_retries <- function(accession, max_retries = 3, wait_time = 5) { + + success <- FALSE + attempts <- 0 + + while (!success && attempts < max_retries) { + attempts <- attempts + 1 + + tryCatch({ + geo_data <- GEOquery::getGEO( accession ) + success <- TRUE + + }, error = function(e) { + + message("Attempt ", attempts, " Message: ", e$message) + + if (attempts < max_retries) { + warning("Retrying in ", wait_time, " seconds...") + Sys.sleep(wait_time) + + } else { + warning("Unhandled error: ", e$message) + write("EXPERIMENT NOT FOUND", file = FAILURE_REASON_FILE) + quit(save = "no", status = 0) + } + }) + + } + return(geo_data) +} + +##################################################### +##################################################### +# PARSE SERIES / PLATFORM METADATA +##################################################### +##################################################### + +get_experiment_data <- function(geo_data) { + data <- geo_data[[1]] + experiment_data <- experimentData(data) + return(experiment_data) +} + + +get_experiment_type <- function(geo_data) { + experiment_data <- get_experiment_data(geo_data) + experiment_type <- tolower(attr(experiment_data, "other")$type) if (experiment_type == "expression profiling by high throughput sequencing") { return("rnaseq") } else if (experiment_type == "expression profiling by array") { @@ -48,35 +138,98 @@ get_experiment_type <- function(data) { } -get_platform_id <- function(pdata) { - platform_id <- as.character(unique(pdata$platform_id))[1] +get_series_supplementary_data <- function(geo_data) { + experiment_data <- get_experiment_data(geo_data) + suppl_data_str <- attr(experiment_data, "other")$supplementary_file + return(stringr::str_split(suppl_data_str, "\n")[[1]]) +} + + +get_platform_id <- function(metadata) { + platform_id <- as.character(unique(metadata$platform_id))[1] return(platform_id) } -get_rnaseq_samples <- function(pdata, valid_samples) { - filtered_pdata <- pdata[ rownames(pdata) %in% valid_samples & pdata$library_strategy == "RNA-Seq", ] - return(rownames(filtered_pdata)) +##################################################### +##################################################### +# RNASEQ SAMPLES +##################################################### +##################################################### + +get_rnaseq_samples <- function(geo_data, design_df) { + rnaseq_sample_df_list <- list() + for (i in 1:length(geo_data)) { + data <- geo_data[[ i ]] + metadata <- pData(data) + rnaseq_sample_df_list[[i]] <- metadata %>% + filter(library_strategy == "RNA-Seq" & geo_accession %in% design_df$sample) %>% + select(geo_accession) + } + # concatenate rows + rnaseq_sample_df <- Reduce( + function(df1, df2) dplyr::bind_rows(df1, df2), + rnaseq_sample_df_list + ) + return(rnaseq_sample_df$geo_accession) } -format_species_name <- function(x) { - x <- tools::toTitleCase(x) - x <- gsub("[_-]", " ", x) - return(x) + +##################################################### +##################################################### +# SAMPLE NAME MAPPING +##################################################### +##################################################### + + +make_sample_name_mapping <- function(geo_data) { + message("Making sample name mapping") + mapping_df_list <- list() + for (i in 1:length(geo_data)) { + data <- geo_data[[ i ]] + metadata <- pData(data) + mapping_df_list[[i]] <- metadata %>% + mutate( + sample_id = geo_accession, + sample_name = title + ) %>% + select(sample_id, sample_name) + } + # concatenate rows + mapping_df <- Reduce( + function(df1, df2) dplyr::bind_rows(df1, df2), + mapping_df_list + ) + return(mapping_df) } +rename_columns <- function(df, mapping_df) { + id_map <- setNames(mapping_df$sample_id, mapping_df$sample_name) + names(df) <- ifelse( + names(df) %in% names(id_map), + id_map[names(df)], + names(df) + ) + return(df) +} + +##################################################### +##################################################### +# DESIGN +##################################################### +##################################################### -get_samples_for_species <- function(pdata, species) { +get_samples_for_species <- function(metadata, species) { # check if organism_ch2 exists - if ("organism_ch2" %in% colnames(pdata)) { - keep <- pdata$organism_ch1 == species & pdata$organism_ch2 == species + if ("organism_ch2" %in% colnames(metadata)) { + keep <- metadata$organism_ch1 == species & metadata$organism_ch2 == species } else { - keep <- pdata$organism_ch1 == species + keep <- metadata$organism_ch1 == species } # return a data.frame with matching samples - return(pdata$geo_accession[keep]) + return(metadata$geo_accession[keep]) } @@ -119,209 +272,151 @@ build_design_dataframe <- function(df, accession) { return(design_df) } -make_design <- function(pdata, accession, species) { - design_df <- build_design_dataframe(pdata, accession) +get_design_for_platform <- function(design_df, metadata) { + platform_samples <- metadata$geo_accession + platform_design_df <- design_df %>% + filter(sample %in% platform_samples) + return(platform_design_df) +} + +get_design_for_rnaseq <- function(design_df, rnaseq_samples) { + rnaseq_design_df <- design_df %>% + filter(sample %in% rnaseq_samples) + return(rnaseq_design_df) +} - # get samples corresponding to species - species_samples <- get_samples_for_species(pdata, species) +make_design <- function(metadata, series) { + design_df <- build_design_dataframe(metadata, series$accession) + # get samples corresponding to species + species_samples <- get_samples_for_species(metadata, series$species) # filter design dataframe design_df <- design_df %>% filter(sample %in% species_samples) + return(design_df) +} + +make_overall_design <- function(geo_data, series) { + message("Making overall design") + design_df_list <- list() + for (i in 1:length(geo_data)) { + data <- geo_data[[ i ]] + metadata <- pData(data) + # make design dataframe + # keep only samples corresponding to the species of interest + design_df <- make_design(metadata, series) + design_df_list[[i]] <- design_df + } + # full outer join + design_df <- Reduce( + function(df1, df2) dplyr::bind_rows(df1, df2), + design_df_list + ) return(design_df) } -download_geo_data_with_retries <- function(accession, species, max_retries = 3, wait_time = 5) { +##################################################### +##################################################### +# PARSE COUNTS FROM DATA +##################################################### +##################################################### - success <- FALSE - attempts <- 0 - while (!success && attempts < max_retries) { - attempts <- attempts + 1 +get_microarray_counts <- function(platform) { + # get count data corresponding to samples in the design + counts <- data.frame(exprs(platform$data)) %>% + select(all_of(platform$design$sample)) + return(counts) +} - tryCatch({ - geo_data <- GEOquery::getGEO( accession ) - success <- TRUE - }, error = function(e) { +get_raw_counts_from_url <- function(data_url) { - message("Attempt ", attempts, " Message: ", e$message) + if ( tolower(data_url) == "none" || is.na(data_url) || data_url == "") { + write_warning(paste("MISFORMED URL:", data_url)) + return(NULL) + } - if (attempts < max_retries) { - warning("Retrying in ", wait_time, " seconds...") - Sys.sleep(wait_time) + filename <- tolower(basename(data_url)) + extensions <- get_extensions(filename) + ext <- extensions[length(extensions)] + if (ext == "gz") { + ext <- extensions[length(extensions) - 1] + } + if (!(ext %in% c("txt", "tsv", "csv", "tab"))) { + write_warning(paste("UNSUPPORTED EXTENSION:", ext, "for URL:", data_url)) + return(NULL) + } + message(paste("Downloading", filename)) + tryCatch({ + download.file(data_url, filename, method = "wget", quiet = TRUE) + }, error = function(e) { + write_warning(paste("ERROR WHILE DOWNLOADING:", filename)) + return(NULL) + }) + + separator <- NULL + for (sep in c("\t", ",", " ")) { + # parsing the first line to determine the separator and see if there is a header + counts <- read.table(filename, header = FALSE, sep = sep, row.names = 1, nrows = 1) + if (ncol(counts) > 0) { + separator <- sep + if (is.numeric(counts[1, 1])) { + has_header <- FALSE } else { - warning("Unhandled error: ", e$message) - write("EXPERIMENT NOT FOUND", file = FAILURE_REASON_FILE) - quit(save = "no", status = 0) + has_header <- TRUE } - }) - + break + } } - return(geo_data) - -} - -write_warning <- function(msg) { - file_conn <- file( WARNING_REASON_FILE, open = "a") - cat(paste0(msg, "; "), file = file_conn, sep = "", fill = FALSE) - close(file_conn) -} - -check_microarray_normalisation <- function(df) { - - vals <- unlist(df, use.names = FALSE) - vals <- vals[!is.na(vals)] - - all_integers <- all(abs(vals - round(vals)) < 1e-8) - value_range <- range(vals, na.rm = TRUE) - - if (value_range[2] <= 20) { - message("Normalized, log2 scale (e.g. RMA, quantile)") - } else if (all_integers) { - message("Raw probe intensities (unnormalized CEL-like data)") - write_warning("RAW PROBE INTENSITIES FOUND") - } else if (value_range[2] > 1000) { - message("Normalized but not log-transformed (e.g. MAS5, raw intensities)") - write_warning("PARSED INTENSITIES: NORMALIZED BUT NOT LOG-TRANSFORMED") - } else { - message("Unclear data origin, check GEO metadata") - write_warning("UNCLEAR DATA ORIGIN: CHECK GEO METADATA") - } -} + if (is.null(separator)) { + write_warning(paste("NO VALID SEPARATOR:", filename)) + return(NULL) + } + message(paste("Parsing", filename)) + tryCatch({ + counts <- read.table(filename, header = has_header, sep = separator, row.names = 1) + }, error = function(e) { + write_warning(paste("ERROR WHILE PARSING:", filename)) + return(NULL) + }) -clean_count_data <- function(df) { - message("Cleaning counts") # removes rows that are all NA - df <- df[rowSums(!is.na(df)) > 0, , drop = FALSE] - return(df) -} - - -get_microarray_counts <- function(data, design_df) { - - # get count data corresponding to samples in the design - count_df <- data.frame(exprs(data)) %>% - select(all_of(design_df$sample)) - return(count_df) + counts <- counts[rowSums(!is.na(counts)) > 0, , drop = FALSE] + return(counts) } -get_extensions <- function(file){ - extensions <- strsplit(basename(file), split="\\.")[[1]] - return(extensions) -} - - -get_rnaseq_counts <- function(pdata, design_df) { - +get_all_rnaseq_counts <- function(platform) { + pdata <- platform$metadata # getting list of samples samples <- pdata$geo_accession - # getting list of columns corresponding to supp data supplementary_cols <- grep("^supplementary_file(_\\d+)?$", names(pdata), value = TRUE) count_df_list <- list() cpt = 1 for (i in 1:length(samples)) { - sample <- samples[[i]] for (j in 1:length(supplementary_cols)) { - - url <- pdata[pdata$geo_accession == sample, supplementary_cols[j]] - - if ( tolower(url) == "none" || is.na(url) || url == "") { - message(paste("Skipping sample", sample, "because no supplementary file is provided")) - write_warning(paste("NO SUPPLEMENTARY FILE:", sample)) - next - } - - filename <- tolower(basename(url)) - extensions <- get_extensions(filename) - ext <- extensions[length(extensions)] - if (ext == "gz") { - ext <- extensions[length(extensions) - 1] - } - if (!(ext %in% c("txt", "tsv", "csv", "tab"))) { - message(paste("Extension not supported:", filename)) - write_warning(paste("UNSUPPORTED EXTENSION:", ext)) + data_url <- pdata[pdata$geo_accession == sample, supplementary_cols[j]] + counts <- get_raw_counts_from_url(data_url) + if (is.null(counts)) { next } - - # skipping if it is obviously TPMs / FPKMs / RPKMs - if (grepl("tpm", filename) | grepl("fpkm", filename) | grepl("rpkm", filename)) { - message(paste("Skipping already normalised file", filename)) - write_warning(paste("ALREADY NORMALIZED:", filename)) - next - } - - skip_iteration <<- FALSE - message(paste("Downloading", filename)) - tryCatch({ - download.file(url, filename, method = "wget", quiet = TRUE) - }, error = function(e) { - message(paste("Unhandled error while downloading", filename, " : ", e$message)) - write_warning(paste("ERROR WHILE DOWNLOADING:", filename)) - skip_iteration <<- TRUE - }) - - # If an error occurred, skip to the next iteration - if (skip_iteration) { - next - } - - separator <- NULL - for (sep in c("\t", ",", " ")) { - # parsing the first line to determine the separator and see if there is a header - counts <- read.table(filename, header = FALSE, sep = sep, row.names = 1, nrows = 1) - if (ncol(counts) > 0) { - separator <- sep - if (is.numeric(counts[1, 1])) { - has_header <- FALSE - } else { - has_header <- TRUE - } - break - } - } - - if (is.null(separator)) { - message(paste("Skipping file with no valid separator", filename)) - write_warning(paste("NO VALID SEPARATOR:", filename)) - next - } - - counts <- read.table(filename, header = has_header, sep = separator, row.names = 1) - # checking number of columns + # if only one column if (ncol(counts) == 1) { colnames(counts) <- c(sample) - } else { - # TODO: see how to handle multiple columns - #colnames(counts) <- paste0(sample, "_", 1:ncol(counts)) - write_warning(paste("MULTIPLE COUNT COLUMNS:", filename)) - next - } - - # checking type of values - is_all_integer <- function(x) all(floor(x) == x) - int_counts <- counts %>% select_if(is_all_integer) - - # if some values were not integers - if (nrow(int_counts) < nrow(counts)) { - message(paste("Skipping non-integer file", filename)) - write_warning(paste("NOT ALL INTEGERS:", filename)) - next } - - # resetting the index counts <- tibble::rownames_to_column(counts, var = "gene_id") + # adding to list count_df_list[[cpt]] <- counts cpt = cpt + 1 } @@ -344,100 +439,174 @@ get_rnaseq_counts <- function(pdata, design_df) { } -process_data <- function(geo_data, accession, species) { +##################################################### +##################################################### +# DATA QUALITY CONTROL +##################################################### +##################################################### - for (i in 1:length(geo_data)) { +is_valid_microarray <- function(platform) { - data <- geo_data[[ i ]] - file <- names(geo_data)[[ i ]] + if (!all(colnames(platform$counts) %in% platform$design$sample)) { + message("Column names do not match samples in design") + return(FALSE) + } - pdata <- pData(data) - #print(pdata) - # make design dataframe - # keep only samples corresponding to the species of interest - design_df <- make_design(pdata, accession, species) - valid_samples <- as.character(design_df$sample) + vals <- unlist(platform$counts, use.names = FALSE) + vals <- vals[!is.na(vals)] - if (length(valid_samples) == 0) { - message(paste("No samples for required species:", file)) - next - } + all_integers <- all(abs(vals - round(vals)) < 1e-8) + value_range <- range(vals, na.rm = TRUE) + if (value_range[2] <= 20) { + message(paste(platform$id, ": normalized, log2 scale (e.g. RMA, quantile)")) + return(TRUE) + } else if (all_integers) { + write_warning(paste(platform$id, ": RAW PROBE INTENSITIES FOUND")) + return(FALSE) + } else if (value_range[2] > 1000) { + write_warning(paste(platform$id, ": PARSED INTENSITIES: NORMALIZED BUT NOT LOG-TRANSFORMED")) + return(FALSE) + } else { + write_warning(paste(platform$id, ": UNCLEAR DATA ORIGIN: CHECK GEO METADATA")) + return(FALSE) + } +} - platform_id <- get_platform_id(pdata) - rnaseq_samples <- get_rnaseq_samples(pdata, valid_samples) +is_valid_rnaseq <- function(platform) { - experiment_type <- get_experiment_type(data) + if (!all(colnames(platform$counts) %in% platform$design$sample)) { + message(paste(platform$id, ": column names do not match samples in design")) + return(FALSE) + } - if ( experiment_type == "microarray" ) { - message(paste("Processing microarray data:", file)) + # checking if all values are integers + tryCatch({ + is_all_integer <- function(x) all(floor(x) == x) + int_counts <- platform$counts %>% select_if(is_all_integer) + # if some values were not integers + if (nrow(int_counts) < nrow(platform$counts)) { + write_warning(paste(platform$id, ": NOT ALL INTEGERS")) + return(FALSE) + } + }, error = function(e) { + write_warning(paste(platform$id, ": COULD NOT COMPUTE FLOOR")) + return(FALSE) + }) - count_df <- get_microarray_counts(data, design_df) + return(TRUE) +} - # keeping only non empty data - if (nrow(count_df) == 0 || ncol(count_df) == 0) { - write_warning(paste("NO DATA AFTER FILTERING:", file)) - next - } - # checking that data are from RMA pipeline and followed proper normalisation - check_microarray_normalisation(count_df) +##################################################### +##################################################### +# EXPORT +##################################################### +##################################################### - } else if ( experiment_type == "rnaseq" || length(rnaseq_samples) > 0 ) { - message(paste("Processing RNA-seq data:", file)) +export_count_data <- function(platform, series) { + # renaming columns, to make them specific to accession and data type + colnames(platform$counts) <- paste0(series$accession, '_', colnames(platform$counts)) + + # if nothing is left after cleaning, we still return the original data + # so that we can have a look at it afterwards + if (platform$type == "microarray") { + extension <- paste0(".normalised", COUNT_FILE_EXTENSION) + } else { + extension <- paste0(".raw", COUNT_FILE_EXTENSION) + } - count_df <- get_rnaseq_counts(pdata, design_df) + outfilename <- paste0(series$accession, '_', platform$id, '.', platform$type, extension) + if (!platform$is_valid) { + outfilename <- file.path(get_rejected_dir(platform, series), outfilename) + } - # keeping only non empty data - if (nrow(count_df) == 0 || ncol(count_df) == 0) { - message(paste("No data found for", file)) - write_warning(paste("NO DATA:", file)) - next - } + # exporting to CSV file + # index represents gene names + message(paste(platform$id, ': exporting count data to file', outfilename)) + write.table(platform$counts, outfilename, sep = ',', row.names = TRUE, col.names = TRUE, quote = FALSE) +} - } else { - message(paste("Unsupported platform:", experiment_type)) - write_warning(paste("UNSUPPORTED PLATFORM:", experiment_type)) - next - } - # clean counts: - # * removes rows that are all NA - count_df <- clean_count_data(count_df) +export_design <- function(platform, series) { + new_sample_names <- paste0(series$accession, '_', series$design$sample) + design_df <- series$design %>% + mutate(sample = new_sample_names ) %>% + select(sample, condition, batch) + + outfilename <- paste0(series$accession, '_', platform$id, '.', platform$type, DESIGN_FILE_EXTENSION) + if (!platform$is_valid) { + outfilename <- file.path(get_rejected_dir(platform, series), outfilename) + } + + message(paste(platform$id, ': exporting design data to file', outfilename)) + write.table(design_df, outfilename, sep = ',', row.names = FALSE, col.names = TRUE, quote = FALSE) +} - # exporting count data to CSV - export_count_data(count_df, accession, experiment_type, platform_id) - # exporting metadata to CSV - export_design(design_df, accession, experiment_type, platform_id) +export_name_mapping <- function(platform, series) { + outfilename <- paste0(series$accession, '_', platform$id, '.', platform$type, MAPPING_FILE_EXTENSION) + if (!platform$is_valid) { + outfilename <- file.path(get_rejected_dir(platform, series), outfilename) } + message(paste(platform$id, ': exporting design data to file', outfilename)) + write.table(series$mapping, outfilename, sep = ',', row.names = FALSE, col.names = TRUE, quote = FALSE) } +export_metadata <- function(platform, series) { + outfilename <- paste0(series$accession, '_', platform$id, '.', platform$type, METADATA_FILE_EXTENSION) + if (!platform$is_valid) { + outfilename <- file.path(get_rejected_dir(platform, series), outfilename) + } + message(paste(platform$id, ': exporting metadata to file', outfilename)) + write.table(platform$metadata, outfilename, sep = ',', row.names = FALSE, col.names = TRUE, quote = FALSE) +} -export_count_data <- function(count_df, accession, experiment_type, platform_id) { - # renaming columns, to make them specific to accession and data type - colnames(count_df) <- paste0(accession, '_', colnames(count_df)) +##################################################### +##################################################### +# PROCESS DATA +##################################################### +##################################################### - outfilename <- paste0(accession, '_', platform_id, '.', experiment_type, '.normalised.counts.csv') +post_process_and_export <- function(platform, series) { + # keeping only non empty data + if (nrow(platform$counts) == 0 || ncol(platform$counts) == 0) { + message(paste(platform$id, ': no data found')) + write_warning(paste(platform$id, ": NO DATA")) + return(NULL) + } + # rename columns when needed + platform$counts <- rename_columns(platform$counts, series$mapping) - # exporting to CSV file - # index represents gene names - message(paste('Exporting count data to file', outfilename)) - write.table(count_df, outfilename, sep = ',', row.names = TRUE, col.names = TRUE, quote = FALSE) + export_count_data(platform, series) + export_design(platform, series) + export_name_mapping(platform, series) + export_metadata(platform, series) } -export_design <- function(design_df, accession, experiment_type, platform_id) { - new_sample_names <- paste0(accession, '_', design_df$sample) +process_platform_data <- function(platform, series) { - df <- design_df %>% - mutate(sample = new_sample_names ) %>% - select(sample, condition, batch) + platform$metadata <- pData(platform$data) + platform$design <- get_design_for_platform(series$design, platform$metadata) + valid_samples <- as.character(platform$design$sample) + platform$id <- get_platform_id(platform$metadata) + + if (length(valid_samples) == 0) { + message(paste(platform$id, ": no sample corresponding to species", series$species)) + return(NULL) + } - outfilename <- paste0(accession, '_', platform_id, '.', experiment_type, '.design.csv') - message(paste('Exporting design data to file', outfilename)) - write.table(df, outfilename, sep = ',', row.names = FALSE, col.names = TRUE, quote = FALSE) + if (platform$type == "microarray") { + platform$counts <- get_microarray_counts(platform) + platform$is_valid <- is_valid_microarray(platform) + } else { + platform$counts <- get_all_rnaseq_counts(platform) + platform$is_valid <- is_valid_rnaseq(platform) + } + + post_process_and_export(platform, series) } @@ -447,12 +616,93 @@ export_design <- function(design_df, accession, experiment_type, platform_id) { ##################################################### ##################################################### -args <- get_args() -cat(paste("Getting data for accession", args$accession, "\n")) +main <- function() { + + args <- get_args() + + series <- list() -species <- format_species_name(args$species) -# searching and downloading expression atlas data -geo_data <- download_geo_data_with_retries(args$accession, species) + series$accession <- args$accession + series$species <- format_species_name(args$species) -process_data(geo_data, args$accession, species) + message(paste("Getting data for accession", series$accession)) + # searching and downloading expression atlas data + geo_data <- download_geo_data_with_retries(series$accession) + + # make a single design dataframe for all samples in the series + series$design <- make_overall_design(geo_data, series) + if ( length(series$design) == 0 ) { + message("No sample corresponding to species", series$species) + write(paste("NO SAMPLES FOR SPECIES", series$species), file = FAILURE_REASON_FILE) + quit(save = "no", status = 0) + } + + # make a map associating sample names to sample IDs + series$mapping <- make_sample_name_mapping(geo_data) + + series$experiment_type <- get_experiment_type(geo_data) + + suppl_data_urls <- get_series_supplementary_data(geo_data) + # for now, considering suppl data as raw rnaseq data + # TODO: check if these are always raw rnaseq data + if (length(suppl_data_urls) > 0) { + + message("Processing supplementary data") + for (supp_data_url in suppl_data_urls) { + counts <- get_raw_counts_from_url(supp_data_url) + if (is.null(counts)) { + next + } + platform <- list( + type = "rnaseq", + id = "suppl", + counts = counts, + design = series$design + ) + platform$is_valid <- is_valid_rnaseq(platform) + post_process_and_export(platform, series) + } + + } + + # NOTE: we consider that a series is either a microarray series OR contains RNA-seq data + # mixed types should be found only in SuperSeries, and it is not handled for now + if ( series$experiment_type == "microarray" ) { + + message("Processing microarray data") + for (i in 1:length(geo_data)) { + platform <- list( + type = "microarray", + data = geo_data[[ i ]] + ) + process_platform_data(platform, series) + } + + } else { + + rnaseq_samples <- get_rnaseq_samples(geo_data, series$design) + if ( series$experiment_type == "rnaseq" || length(rnaseq_samples) > 0 ) { + + message("Processing RNA-seq data") + # taking a subset of the design corresponding to bona-fide RNA-seq samples + rnaseq_design_df <- get_design_for_rnaseq(series$design, rnaseq_samples) + for (i in 1:length(geo_data)) { + platform <- list( + type = "rnaseq", + data = geo_data[[ i ]] + ) + process_platform_data(platform, series) + } + + } else { + write_warning(paste("UNSUPPORTED PLATFORM:", series$experiment_type)) + } + } +} + + +##################################################### +# ENTRYPOINT +##################################################### +main() diff --git a/modules/local/geo/getdata/main.nf b/modules/local/geo/getdata/main.nf index c7369e53..0a85f845 100644 --- a/modules/local/geo/getdata/main.nf +++ b/modules/local/geo/getdata/main.nf @@ -4,7 +4,7 @@ process GEO_GETDATA { tag "$accession" - maxForks 8 // limiting to 8 threads at a time to avoid 429 errors with the Expression Atlas API server + maxForks 8 // limiting to 8 threads at a time to avoid 429 errors with the NCBI server conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? @@ -18,6 +18,7 @@ process GEO_GETDATA { output: path("*.counts.csv"), optional: true, emit: counts path("*.design.csv"), optional: true, emit: design + path("rejected/**"), optional: true tuple val(accession), path("failure_reason.txt"), optional: true, topic: geo_failure_reason tuple val(accession), path("warning_reason.txt"), optional: true, topic: geo_warning_reason tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //"'), topic: versions diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index baeb54e7..f97dcd9e 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -363,10 +363,13 @@ def augmentMetadata( ch_files ) { return ch_files .map { meta, file -> - if ( getNthPartFromEnd(file.name, 3) == 'raw' ) { + def norm_state = getNthPartFromEnd(file.name, 3) + if ( norm_state == 'raw' ) { meta.normalised = false - } else { + } else if ( norm_state == 'normalised' ) { meta.normalised = true + } else { + error("Invalid normalisation state: ${norm_state}") } meta.platform = getNthPartFromEnd(file.name, 4) [meta, file] From 5aa1acc97c78faff8678c26ae5fabd6af92baf34 Mon Sep 17 00:00:00 2001 From: Olivier Date: Sun, 16 Nov 2025 12:07:30 +0100 Subject: [PATCH 160/258] set ks threshold pvalue as negative by default --- bin/clean_count_data.py | 12 ++---------- nextflow.config | 2 +- nextflow_schema.json | 2 +- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/bin/clean_count_data.py b/bin/clean_count_data.py index b31c47ed..35f0ccfa 100755 --- a/bin/clean_count_data.py +++ b/bin/clean_count_data.py @@ -42,7 +42,7 @@ def parse_args(): ) parser.add_argument( "--ks-pvalue-threshold", - type=str, + type=float, dest="ks_pvalue_threshold", required=True, help="KS p-value threshold", @@ -68,20 +68,12 @@ def get_counts( def remove_samples_with_low_ks_pvalue( - count_lf: pl.LazyFrame, ks_stats_file: Path, ks_pvalue_threshold: str + count_lf: pl.LazyFrame, ks_stats_file: Path, ks_pvalue_threshold: float ) -> pl.LazyFrame: ks_stats_df = pl.read_csv(ks_stats_file, has_header=True).select( [config.SAMPLE_COLNAME, config.KS_TEST_COLNAME] ) - # parsing threshold - try: - ks_pvalue_threshold = float(ks_pvalue_threshold) - except ValueError: - raise ValueError( - f"KS p-value threshold {ks_pvalue_threshold} could not be cast to float" - ) - # logging number of samples excluded from analysis not_valid_samples = ks_stats_df.filter( ks_stats_df[config.KS_TEST_COLNAME] <= ks_pvalue_threshold diff --git a/nextflow.config b/nextflow.config index 83ddf867..3ccf9196 100644 --- a/nextflow.config +++ b/nextflow.config @@ -44,7 +44,7 @@ params { normalisation_method = 'deseq2' quantile_norm_target_distrib = 'uniform' nb_top_gene_candidates = 5000 - ks_pvalue_threshold = 0 + ks_pvalue_threshold = -1 min_expr_threshold = 0.2 // stability scoring diff --git a/nextflow_schema.json b/nextflow_schema.json index 2005be49..973f2d01 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -227,7 +227,7 @@ "description": "Threshold for KS p-value for considering samples counts as a uniform distribution", "fa_icon": "fas fa-battery-three-quarters", "maximum": 1, - "default": 0, + "default": -1, "help_text": "P-value threshold for the Kolmogorov-Smirnov test of samples counts against a uniform distribution. Samples showing a p-value equal or below this threshold are considered not uniform and will therefore not be considered for computation of the stability score. Examples: `0`, `'0.05'`, `'1E-27'`. Provide a negative value to disable this filter. By default, all samples showing a pvalue of 0 will be discarded." }, "min_expr_threshold": { From 459d726d2a4d9eafe099d80af3be172ab97c6464 Mon Sep 17 00:00:00 2001 From: Olivier Date: Sun, 16 Nov 2025 13:07:42 +0100 Subject: [PATCH 161/258] fix issue with deseq2 and edger in case of only one sample; improve logging --- bin/download_geo_data.R | 20 +++----- bin/normalise_with_deseq2.R | 47 ++++++++++++++----- bin/normalise_with_edger.R | 40 +++++++++++++--- modules/local/normalisation/deseq2/main.nf | 9 ++-- modules/local/normalisation/edger/main.nf | 9 ++-- .../local/expression_normalisation/main.nf | 9 ++-- 6 files changed, 92 insertions(+), 42 deletions(-) diff --git a/bin/download_geo_data.R b/bin/download_geo_data.R index 2c3f9654..5e921512 100755 --- a/bin/download_geo_data.R +++ b/bin/download_geo_data.R @@ -507,16 +507,7 @@ is_valid_rnaseq <- function(platform) { export_count_data <- function(platform, series) { # renaming columns, to make them specific to accession and data type colnames(platform$counts) <- paste0(series$accession, '_', colnames(platform$counts)) - - # if nothing is left after cleaning, we still return the original data - # so that we can have a look at it afterwards - if (platform$type == "microarray") { - extension <- paste0(".normalised", COUNT_FILE_EXTENSION) - } else { - extension <- paste0(".raw", COUNT_FILE_EXTENSION) - } - - outfilename <- paste0(series$accession, '_', platform$id, '.', platform$type, extension) + outfilename <- paste0(series$accession, '_', platform$id, '.', platform$type, '.', platform$count_type, COUNT_FILE_EXTENSION) if (!platform$is_valid) { outfilename <- file.path(get_rejected_dir(platform, series), outfilename) } @@ -534,7 +525,7 @@ export_design <- function(platform, series) { mutate(sample = new_sample_names ) %>% select(sample, condition, batch) - outfilename <- paste0(series$accession, '_', platform$id, '.', platform$type, DESIGN_FILE_EXTENSION) + outfilename <- paste0(series$accession, '_', platform$id, '.', platform$type,'.', platform$count_type, DESIGN_FILE_EXTENSION) if (!platform$is_valid) { outfilename <- file.path(get_rejected_dir(platform, series), outfilename) } @@ -545,7 +536,7 @@ export_design <- function(platform, series) { export_name_mapping <- function(platform, series) { - outfilename <- paste0(series$accession, '_', platform$id, '.', platform$type, MAPPING_FILE_EXTENSION) + outfilename <- paste0(series$accession, '_', platform$id, '.', platform$type, '.', platform$count_type, MAPPING_FILE_EXTENSION) if (!platform$is_valid) { outfilename <- file.path(get_rejected_dir(platform, series), outfilename) } @@ -554,7 +545,7 @@ export_name_mapping <- function(platform, series) { } export_metadata <- function(platform, series) { - outfilename <- paste0(series$accession, '_', platform$id, '.', platform$type, METADATA_FILE_EXTENSION) + outfilename <- paste0(series$accession, '_', platform$id, '.', platform$type, '.', platform$count_type, METADATA_FILE_EXTENSION) if (!platform$is_valid) { outfilename <- file.path(get_rejected_dir(platform, series), outfilename) } @@ -657,6 +648,7 @@ main <- function() { platform <- list( type = "rnaseq", id = "suppl", + count_type = "raw", counts = counts, design = series$design ) @@ -674,6 +666,7 @@ main <- function() { for (i in 1:length(geo_data)) { platform <- list( type = "microarray", + count_type = "normalised", data = geo_data[[ i ]] ) process_platform_data(platform, series) @@ -690,6 +683,7 @@ main <- function() { for (i in 1:length(geo_data)) { platform <- list( type = "rnaseq", + count_type = "raw", data = geo_data[[ i ]] ) process_platform_data(platform, series) diff --git a/bin/normalise_with_deseq2.R b/bin/normalise_with_deseq2.R index 0120bb1b..40b12a30 100755 --- a/bin/normalise_with_deseq2.R +++ b/bin/normalise_with_deseq2.R @@ -1,7 +1,7 @@ #!/usr/bin/env Rscript # Written by Olivier Coen. Released under the MIT license. - +options(error = traceback) suppressPackageStartupMessages(library("DESeq2")) library(DESeq2) library(optparse) @@ -60,15 +60,15 @@ check_samples <- function(count_matrix, design_data) { } prefilter_counts <- function(count_matrix, design_data) { - if (is.null(design_data)) { - keep <- rowSums(count_matrix >= 10) >= 1 + if (ncol(count_matrix) == 1) { + keep <- count_matrix[, 1] >= 1 } else { # see https://bioconductor.org/packages/devel/bioc/vignettes/DESeq2/inst/doc/DESeq2.html # getting size of smallest group group_sizes <- table(design_data$condition) smallest_group_size <- min(group_sizes) # keep genes with at least 10 counts over a certain number of samples - keep <- rowSums(count_matrix >= 10) >= smallest_group_size + keep <- rowSums(count_matrix >= 1) >= smallest_group_size } filtered_count_matrix <- count_matrix[keep, , drop = FALSE] # drop = FALSE: keep dataframe structure even if only one column remains return(filtered_count_matrix) @@ -76,7 +76,7 @@ prefilter_counts <- function(count_matrix, design_data) { remove_all_zero_columns <- function(df) { # remove columns which contains only zeros - df <- df[, colSums(df) != 0] + df <- df[, colSums(df) != 0, drop = FALSE] return(df) } @@ -107,6 +107,7 @@ get_cpm_counts <- function(normalised_counts, filtered_count_matrix) { get_normalised_cpm_counts <- function(count_file, design_file) { + message("Parsing count file") count_data <- parse_dataframe(count_file, row.names = 1) # data should all be integers but sometimes they are integers converted to floats (1234 -> 1234.0) @@ -114,29 +115,43 @@ get_normalised_cpm_counts <- function(count_file, design_file) { count_data[] <- lapply(count_data, as.integer) count_matrix <- as.matrix(count_data) + # in some rare datasets, columns can contain only zeros # we do not consider these columns + message("Removing columns with all zeros") count_matrix <- remove_all_zero_columns(count_matrix) + if (ncol(count_matrix) == 0) { + message("All columns were full of zeros.") + write("ALL COLUMNS WERE FULL OF ZEROS", file = FAILURE_REASON_FILE) + quit(save = "no", status = 0) + } + # getting design data + message("Parsing design file") design_data <- parse_dataframe(design_file) + # removing extra samples in design table - design_data <- design_data[design_data$sample %in% colnames(count_matrix), ] + message("Removing extra samples in design table") + design_data <- design_data[design_data$sample %in% colnames(count_matrix), , drop = FALSE] + + if (nrow(design_data) == 0) { + message("Design and sample names do not match.") + write("DESIGN AND SAMPLE NAMES DO NOT MATCH", file = FAILURE_REASON_FILE) + quit(save = "no", status = 0) + } # check if the column names of count_matrix match the sample names + message("Checking sample names") check_samples(count_matrix, design_data) - col_data <- data.frame( - row.names = design_data$sample, - condition = factor(design_data$condition) - ) - # reorder count matrix columns to match design row order # this is absolutely mandatory # see https://bioconductor.org/packages/devel/bioc/vignettes/DESeq2/inst/doc/DESeq2.html at part "Count matrix input" - count_matrix <- count_matrix[, design_data$sample ] + count_matrix <- count_matrix[, as.character(design_data$sample), drop = FALSE] # pre-filter genes with low counts + message("Pre-filtering genes") filtered_count_matrix <- prefilter_counts(count_matrix, design_data) # if the dataframe is now empty, stop the process @@ -147,9 +162,15 @@ get_normalised_cpm_counts <- function(count_file, design_file) { } # add a small pseudocount to avoid zero counts + message("Replacing zero counts with pseudocounts") filtered_count_matrix <- replace_zero_counts_with_pseudocounts(filtered_count_matrix) # if the number of distinct conditions is only 1, DESeq2 returns an error + message("Creating DESeqDataSet") + col_data <- data.frame( + row.names = design_data$sample, + condition = factor(design_data$condition) + ) num_unique_conditions <- length(unique(design_data$condition)) if (num_unique_conditions == 1) { dds <- DESeqDataSetFromMatrix(countData = filtered_count_matrix, colData = col_data, design = ~ 1) @@ -157,8 +178,10 @@ get_normalised_cpm_counts <- function(count_file, design_file) { dds <- DESeqDataSetFromMatrix(countData = filtered_count_matrix, colData = col_data, design = ~ condition) } + message("Normalising counts") normalised_counts <- get_normalised_counts(dds) + message("Calculating CPM counts") cpm_counts <- get_cpm_counts(normalised_counts, filtered_count_matrix) return(cpm_counts) diff --git a/bin/normalise_with_edger.R b/bin/normalise_with_edger.R index 89b4a35c..f12d7e74 100755 --- a/bin/normalise_with_edger.R +++ b/bin/normalise_with_edger.R @@ -43,7 +43,7 @@ parse_dataframe <- function(file_path, ...) { remove_all_zero_columns <- function(df) { # remove columns which contain only zeros - df <- df[, colSums(df) != 0] + df <- df[, colSums(df) != 0, drop = FALSE] return(df) } @@ -66,8 +66,8 @@ check_samples <- function(count_matrix, design_data) { prefilter_counts <- function(count_matrix) { # remove genes having zeros for all counts # it is advised to remove them analysis - non_zero_rows <- rownames(count_matrix[apply(count_matrix!=0, 1, any),]) - filtered_count_matrix <- count_matrix[rownames(count_matrix) %in% non_zero_rows, ] + non_zero_rows <- rownames(count_matrix[apply(count_matrix!=0, 1, any), , drop = FALSE]) + filtered_count_matrix <- count_matrix[rownames(count_matrix) %in% non_zero_rows, , drop = FALSE] return(filtered_count_matrix) } @@ -95,45 +95,73 @@ get_cpm_counts <- function(dge) { get_normalised_cpm_counts <- function(count_file, design_file) { message(paste('Normalizing counts in:', count_file)) - + message("Parsing count file") count_data <- parse_dataframe(count_file, row.names = 1) count_matrix <- as.matrix(count_data) # in some rare datasets, columns can contain only zeros # we do not consider these columns + message("Removing columns with all zeros") count_matrix <- remove_all_zero_columns(count_matrix) + if (ncol(count_matrix) == 0) { + message("All columns were full of zeros.") + write("ALL COLUMNS WERE FULL OF ZEROS", file = FAILURE_REASON_FILE) + quit(save = "no", status = 0) + } + # getting design data + message("Parsing design file") design_data <- parse_dataframe(design_file) # removing extra samples in design table + message("Removing extra samples in design table") design_data <- design_data[design_data$sample %in% colnames(count_matrix), ] + if (nrow(design_data) == 0) { + message("Design and sample names do not match.") + write("DESIGN AND SAMPLE NAMES DO NOT MATCH", file = FAILURE_REASON_FILE) + quit(save = "no", status = 0) + } + # check if the column names of count_matrix match the sample names + message("Checking sample names") check_samples(count_matrix, design_data) # pre-filter genes with low counts + message("Pre-filtering genes") count_matrix <- prefilter_counts(count_matrix) + # if the dataframe is now empty, stop the process + if (nrow(count_matrix) == 0) { + message("No genes left after pre-filtering.") + write("NO GENES LEFT AFTER PRE-FILTERING", file = FAILURE_REASON_FILE) + quit(save = "no", status = 0) + } # Add a small pseudocount to avoid zero counts + message("Replacing zero counts with pseudocounts") count_matrix_pseudocount <- replace_zero_counts_with_pseudocounts(count_matrix) + message("Normalising data") group <- factor(design_data$condition) dge <- DGEList(counts = count_matrix_pseudocount, group = group) rownames(dge) <- rownames(count_matrix) colnames(dge) <- colnames(count_matrix) + message("Filtering out lowly expressed genes") dge <- filter_out_lowly_expressed_genes(dge) # if the dataframe is now empty, stop the process if (nrow(dge) == 0) { - message("No genes left after pre-filtering.") - write("NO GENES LEFT AFTER PRE-FILTERING", file = FAILURE_REASON_FILE) + message("No genes left after filtering lowly expressed genes.") + write("NO GENES LEFT AFTER FILTERING LOWLY EXPRESSED GENES", file = FAILURE_REASON_FILE) quit(save = "no", status = 0) } # normalisation + message("Calculating normalisation factors") dge <- calcNormFactors(dge, method="TMM") + message("Calculating CPM counts") cpm_counts <- get_cpm_counts(dge) return(cpm_counts) diff --git a/modules/local/normalisation/deseq2/main.nf b/modules/local/normalisation/deseq2/main.nf index e7b497d1..7a517561 100644 --- a/modules/local/normalisation/deseq2/main.nf +++ b/modules/local/normalisation/deseq2/main.nf @@ -10,19 +10,20 @@ process NORMALISATION_DESEQ2 { 'community.wave.seqera.io/library/bioconductor-deseq2_r-base_r-optparse:c84cd7ffdb298fa7' }" input: - tuple val(meta), path(count_file) + tuple val(meta), path(count_file), path(design_file) output: - tuple val(meta), path('*.cpm.csv'), emit: cpm + tuple val(meta), path('*.cpm.csv'), optional: true, emit: cpm tuple val(meta.dataset), path("failure_reason.txt"), optional: true, topic: normalisation_failure_reason tuple val(meta.dataset), path("warning_reason.txt"), optional: true, topic: normalisation_warning_reason tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //"'), topic: versions tuple val("${task.process}"), val('DESeq2'), eval('Rscript -e "cat(as.character(packageVersion(\'DESeq2\')))"'), topic: versions script: - def design_arg = meta.design ? "--design ${meta.design}" : "" """ - normalise_with_deseq2.R --counts $count_file $design_arg + normalise_with_deseq2.R \\ + --counts $count_file \\ + --design $design_file """ diff --git a/modules/local/normalisation/edger/main.nf b/modules/local/normalisation/edger/main.nf index 91cf1dc7..5ff95b51 100644 --- a/modules/local/normalisation/edger/main.nf +++ b/modules/local/normalisation/edger/main.nf @@ -10,19 +10,20 @@ process NORMALISATION_EDGER { 'community.wave.seqera.io/library/bioconductor-edger_r-base_r-optparse:400aaabddeea1574' }" input: - tuple val(meta), path(count_file) + tuple val(meta), path(count_file), path(design_file) output: - tuple val(meta), path('*.cpm.csv'), emit: cpm + tuple val(meta), path('*.cpm.csv'), optional: true, emit: cpm tuple val(meta.dataset), path("failure_reason.txt"), optional: true, topic: normalisation_failure_reason tuple val(meta.dataset), path("warning_reason.txt"), optional: true, topic: normalisation_warning_reason tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //"'), topic: versions tuple val("${task.process}"), val('edgeR'), eval('Rscript -e "cat(as.character(packageVersion(\'edgeR\')))"'), topic: versions script: - def design_arg = meta.design ? "--design ${meta.design}" : "" """ - normalise_with_edger.R --counts $count_file $design_arg + normalise_with_edger.R \\ + --counts $count_file \\ + --design $design_file """ } diff --git a/subworkflows/local/expression_normalisation/main.nf b/subworkflows/local/expression_normalisation/main.nf index 144a4db9..67dca582 100644 --- a/subworkflows/local/expression_normalisation/main.nf +++ b/subworkflows/local/expression_normalisation/main.nf @@ -28,14 +28,17 @@ workflow EXPRESSION_NORMALISATION { normalised: meta.normalised == true } - ch_raw_rnaseq_datasets = ch_datasets.raw.filter { meta, file -> meta.platform == 'rnaseq' } + ch_datasets + .raw.filter { meta, file -> meta.platform == 'rnaseq' } + .map { meta, file -> [ meta, file, meta.design ] } + .set { ch_raw_rnaseq_datasets_to_normalise } if ( normalisation_method == 'deseq2' ) { - NORMALISATION_DESEQ2( ch_raw_rnaseq_datasets ) + NORMALISATION_DESEQ2( ch_raw_rnaseq_datasets_to_normalise ) ch_raw_rnaseq_datasets_normalised = NORMALISATION_DESEQ2.out.cpm } else { // 'edger' - NORMALISATION_EDGER( ch_raw_rnaseq_datasets ) + NORMALISATION_EDGER( ch_raw_rnaseq_datasets_to_normalise ) ch_raw_rnaseq_datasets_normalised = NORMALISATION_EDGER.out.cpm } From 2e380b1595b22d3397734452d6d6b09a2a9b2b94 Mon Sep 17 00:00:00 2001 From: Olivier Date: Sun, 16 Nov 2025 19:04:42 +0100 Subject: [PATCH 162/258] fix merge_counts environment --- modules/local/merge_counts/spec-file.txt | 75 ++++++++++++------------ 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/modules/local/merge_counts/spec-file.txt b/modules/local/merge_counts/spec-file.txt index 79747f60..26484b2c 100644 --- a/modules/local/merge_counts/spec-file.txt +++ b/modules/local/merge_counts/spec-file.txt @@ -2,41 +2,44 @@ # $ conda create --name --file # platform: linux-64 @EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda#3cd1a7238a0dd3d0860fdefc496cc854 -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62ee74e96c5ebb0af99386de58cf9553 -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.6.15-hbd8a1cb_0.conda#72525f07d72806e3b639ad4504c30ce5 -https://conda.anaconda.org/conda-forge/linux-64/python-3.12.8-h9e4cc4f_1_cpython.conda#7fd2fd79436d9b473812f14e86746844 +https://repo.anaconda.com/pkgs/main/linux-64/_libgcc_mutex-0.1-main.conda#c3473ff8bdb3d124ed5ff11ec380d6f9 +https://repo.anaconda.com/pkgs/main/linux-64/_openmp_mutex-5.1-1_gnu.conda#71d281e9c2192cb3fa425655a8defb85 +https://repo.anaconda.com/pkgs/main/linux-64/libgcc-15.2.0-h69a1729_7.conda#01fb1b8725fc7f66312b9d409758917a +https://repo.anaconda.com/pkgs/main/linux-64/libgcc-ng-15.2.0-h166f726_7.conda#2783efb2502b9caa7f08e25fd54df899 +https://repo.anaconda.com/pkgs/main/linux-64/bzip2-1.0.8-h5eee18b_6.conda#f21a3ff51c1b271977f53ce956a69297 +https://repo.anaconda.com/pkgs/main/linux-64/libstdcxx-15.2.0-h39759b7_7.conda#7dc7ec61ceea5de17f3e2c4c5f442fc6 +https://repo.anaconda.com/pkgs/main/linux-64/libstdcxx-ng-15.2.0-hc03a8fd_7.conda#cf200522c0b13d64bf81035358d05f5b +https://repo.anaconda.com/pkgs/main/linux-64/expat-2.7.3-h3385a95_0.conda#105822d24b4de9055e705a7d76549416 +https://repo.anaconda.com/pkgs/main/linux-64/ld_impl_linux-64-2.44-h153f514_2.conda#dffdc9a0e09d04051d4bd758e104f4b3 +https://repo.anaconda.com/pkgs/main/linux-64/libffi-3.4.4-h6a678d5_1.conda#70646cc713f0c43926cfdcfe9b695fe0 +https://repo.anaconda.com/pkgs/main/linux-64/libmpdec-4.0.0-h5eee18b_0.conda#feb10f42b1a7b523acbf85461be41a3e +https://repo.anaconda.com/pkgs/main/linux-64/libuuid-1.41.5-h5eee18b_0.conda#4a6a2354414c9080327274aa514e5299 +https://repo.anaconda.com/pkgs/main/linux-64/ncurses-6.5-h7934f7d_0.conda#0abfc090299da4bb031b84c64309757b +https://repo.anaconda.com/pkgs/main/linux-64/ca-certificates-2025.11.4-h06a4308_0.conda#f04cd5aa67216b77e8f664bb4c7098a4 +https://repo.anaconda.com/pkgs/main/linux-64/openssl-3.0.18-hd6dcaed_0.conda#3762b8999909b69745881cf4b8dd2816 +https://repo.anaconda.com/pkgs/main/linux-64/python_abi-3.13-1_cp313.conda#bea705c35663f9394ec82e87dc692c85 +https://repo.anaconda.com/pkgs/main/linux-64/readline-8.3-hc2a1206_0.conda#8578e006d4ef5cb98a6cda232b3490f6 +https://repo.anaconda.com/pkgs/main/linux-64/libzlib-1.3.1-hb25bd0a_0.conda#338ee51e19ee211b7fc994d4ba88c631 +https://repo.anaconda.com/pkgs/main/linux-64/zlib-1.3.1-hb25bd0a_0.conda#9f3a877e5e0fa0fb39253a59ff824861 +https://repo.anaconda.com/pkgs/main/linux-64/sqlite-3.51.0-h2a70700_0.conda#99a4278be9c6901ee6989b24fd213240 +https://repo.anaconda.com/pkgs/main/linux-64/pthread-stubs-0.3-h0ce48e5_1.conda#973a642312d2a28927aaf5b477c67250 +https://repo.anaconda.com/pkgs/main/linux-64/xorg-libxau-1.0.12-h9b100fa_0.conda#a8005a9f6eb903e113cd5363e8a11459 +https://repo.anaconda.com/pkgs/main/linux-64/xorg-libxdmcp-1.1.5-h9b100fa_0.conda#c284a09ddfba81d9c4e740110f09ea06 +https://repo.anaconda.com/pkgs/main/linux-64/libxcb-1.17.0-h9b100fa_0.conda#fdf0d380fa3809a301e2dbc0d5183883 +https://repo.anaconda.com/pkgs/main/linux-64/xorg-xorgproto-2024.1-h5eee18b_1.conda#412a0d97a7a51d23326e57226189da92 +https://repo.anaconda.com/pkgs/main/linux-64/xorg-libx11-1.8.12-h9b100fa_1.conda#6298b27afae6f49f03765b2a03df2fcb +https://repo.anaconda.com/pkgs/main/linux-64/tk-8.6.15-h54e0aa7_0.conda#1fa91e0c4fc9c9435eda3f1a25a676fd +https://repo.anaconda.com/pkgs/main/noarch/tzdata-2025b-h04d1e81_0.conda#1d027393db3427ab22a02aa44a56f143 +https://repo.anaconda.com/pkgs/main/linux-64/xz-5.6.4-h5eee18b_1.conda#3581505fa450962d631bd82b8616350e +https://repo.anaconda.com/pkgs/main/linux-64/python-3.13.9-h7e8bc2b_100_cp313.conda#9ea34b30a1bdb8f7c9d62c072697e681 +https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.9-py313hd8ed1ab_101.conda#367133808e89325690562099851529c8 +https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.9-h4df99d1_101.conda#f41e3c1125e292e6bfcea8392a3de3d8 +https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda#aaa2a381ccc56eac91d63b6c1240312f https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda#962b9857ee8e7018c22f2776ffa0b2d7 -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_0.conda#e31316a586cac398b1fcdb10ace786b9 -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-32_h59b9bed_openblas.conda#2af9f3d5c2e39f417ce040f5a35c40c6 -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-32_he106b2a_openblas.conda#3d3f9355e52f269cd8bc2c440d8a5263 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda#db0bfbe7dd197b68ad5f30333bae6ce0 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda#9e60c55e725c20d23125a5f0dd69af5d -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda#e66f2b8ad787e7beb0f846e4bd7e8493 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda#bfbca721fd33188ef923dfe9ba172f29 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda#530566b68c3b8ce7eec4cd047eae19fe -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-32_h7ac8fdf_openblas.conda#6c3f04ccb6c578138e9f9899da0bd714 -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda#d864d34357c3b65a4b731f78c0801dc4 -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda#323dc8f259224d13078aaf7ce96c3efe -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.2-h6cd9bfd_0.conda#b04c7eda6d7dab1e6503135e7fad4d25 -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda#6d11a5edae89fe413c0569f16d308f5a -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b -https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.1-py312h6cf2f7f_0.conda#7e086a30150af2536a1059885368dcf0 -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda#c87df2ab1448ba69169652ab9547082d -https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda#58335b26c38bf4a20f399384c33cbcf9 -https://conda.anaconda.org/conda-forge/noarch/pip-25.1.1-pyh8b19718_0.conda#32d0781ace05105cc99af55d36cbec7c -https://conda.anaconda.org/conda-forge/linux-64/polars-1.17.1-py312hda0fa55_1.conda#d9d77bfc286b6044dc045d1696c6acdc -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda#0dfcdc155cf23812a0c9deada86fb723 -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 +https://repo.anaconda.com/pkgs/main/linux-64/libgomp-15.2.0-h4751f2c_7.conda#82025ed6da944bd419d42d9b1ff116aa +https://repo.anaconda.com/pkgs/main/linux-64/setuptools-80.9.0-py313h06a4308_0.conda#42ffd8d5a0c04d5e55431e3d4f6e8408 +https://repo.anaconda.com/pkgs/main/linux-64/wheel-0.45.1-py313h06a4308_0.conda#29057e876eedce0e37c2388c138a19f9 +https://repo.anaconda.com/pkgs/main/noarch/pip-25.3-pyhc872135_0.conda#f713912a259ec613b3832c3bc842e9d4 +https://conda.anaconda.org/conda-forge/linux-64/polars-runtime-32-1.35.1-py310hffdcd12_0.conda#093d1242f534e7c383b4d67ab48c7c3d +https://conda.anaconda.org/conda-forge/noarch/polars-1.35.1-pyh6a1acc5_0.conda#dcb4da1773fc1e8c9e2321a648f34382 https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda#9efbfdc37242619130ea42b1cc4ed861 -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/noarch/wheel-0.45.1-pyhd8ed1ab_1.conda#75cb7132eb58d97896e173ef12ac9986 From 1976981de7045d5ce544ca0c9642d65041dbdd20 Mon Sep 17 00:00:00 2001 From: Olivier Date: Sun, 16 Nov 2025 20:22:53 +0100 Subject: [PATCH 163/258] fix issue with file count file parsing when first column name is empty --- .../utils_nfcore_stableexpression_pipeline/main.nf | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index f97dcd9e..91621e54 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -387,9 +387,17 @@ def storeDatasetSize( ch_counts, nb_genes_key, nb_samples_key ) { // adding nb genes and nb samples in the meta map under keys provided as parameters return ch_counts .map { meta, count_file -> - def content = count_file.splitCsv( header: true ) + def header = count_file.withReader { reader -> reader.readLine() } + if ( header.contains('\t') ) { + columns = header.split('\t') + } else if ( header.contains(',') ) { + columns = header.split(',') + } else { + error("Invalid separator in file ${count_file.name}") + } + def content = count_file.splitCsv( header: false, skip: 1 ) meta[nb_genes_key] = content.size() - meta[nb_samples_key] = content[0].findAll {it.key != 'ensembl_gene_id'}.size() + meta[nb_samples_key] = columns.size() - 1 // removing index column [ meta, count_file ] } } From 81f52a472b4c912eb9d0534060b9df6cdba5d5e6 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 17 Nov 2025 12:32:48 +0100 Subject: [PATCH 164/258] fix synthax issue --- .../main.nf | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index 91621e54..4cb979d6 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -388,13 +388,9 @@ def storeDatasetSize( ch_counts, nb_genes_key, nb_samples_key ) { return ch_counts .map { meta, count_file -> def header = count_file.withReader { reader -> reader.readLine() } - if ( header.contains('\t') ) { - columns = header.split('\t') - } else if ( header.contains(',') ) { - columns = header.split(',') - } else { - error("Invalid separator in file ${count_file.name}") - } + def columns = header.contains(',') ? header.split(',') : + header.contains('\t') ? header.split('\t') : + [header] def content = count_file.splitCsv( header: false, skip: 1 ) meta[nb_genes_key] = content.size() meta[nb_samples_key] = columns.size() - 1 // removing index column @@ -423,12 +419,13 @@ def getWholeDatasetSize( ch_counts ) { def checkCounts(ch_counts) { // display a warning if no datasets are found - def msg = ( - "No dataset found. " - + "\nYou may want to check at https://www.ncbi.nlm.nih.gov/gds if there are datasets for this species that you can prepare yourself. " - + "\nOnce you have prepared your own data, you can relaunch the pipeline with the --datasets parameter." - + "\nFor more information, see the online documentation at https://nf-co.re/stableexpression." - ) + def msg = [ + "No dataset found. ", + "You may want to check at https://www.ncbi.nlm.nih.gov/gds if there are datasets for this species that you can prepare yourself. ", + "Once you have prepared your own data, you can relaunch the pipeline with the --datasets parameter. ", + "For more information, see the online documentation at https://nf-co.re/stableexpression." + ].join("\n").trim() + ch_counts.count().map { n -> if( n == 0 ) { log.warn(msg) From 11bb0babbc103a0851cef62bc6a1474fdae59606 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 17 Nov 2025 14:39:31 +0100 Subject: [PATCH 165/258] update doc --- README.md | 6 +----- docs/troubleshooting.md | 3 --- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/README.md b/README.md index 3cf6abce..7a7a9558 100644 --- a/README.md +++ b/README.md @@ -31,14 +31,10 @@ It takes as main inputs : **Use cases**: * **find the most suitable genes as RT-qPCR reference genes for a specific species (and optionally specific conditions)** - * download all Expression Atlas and NCBI GEO datasets (microarray only!) for a species + * download all Expression Atlas and NCBI GEO datasets for a species -

    - -

    - ## Basic usage > [!NOTE] diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index ffbc613f..e3963de8 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -2,9 +2,6 @@ ## Ǹo dataset found ->[!IMPORTANT] -> For the time being, only Microarray count datasets are fetched from NCBI GEO. - For species that are not on Expression Atlas and that do not have microarray data on NCBI data, the pipeline will not be able to find suitable datasets and will log the following message: ``` From 39c390368d21aa732d1d9ebe51469c05a96e237a Mon Sep 17 00:00:00 2001 From: Olivier Date: Tue, 18 Nov 2025 10:52:15 +0100 Subject: [PATCH 166/258] fix issue with data aggregation when gene id mapping or metadata are absent --- modules/local/aggregate_results/main.nf | 6 ++++-- subworkflows/local/merge_data/main.nf | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/local/aggregate_results/main.nf b/modules/local/aggregate_results/main.nf index 6f89040f..c10bacd2 100644 --- a/modules/local/aggregate_results/main.nf +++ b/modules/local/aggregate_results/main.nf @@ -24,14 +24,16 @@ process AGGREGATE_RESULTS { tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions script: + def mapping_files_arg = mapping_files ? "--mappings " + "$mapping_files" : "" + def metadata_files_arg = metadata_files ? "--metadata " + "$metadata_files" : "" def rnaseq_dataset_stat_file_arg = rnaseq_dataset_stat_file ? "--rnaseq $rnaseq_dataset_stat_file" : "" def microarray_dataset_stat_file_arg = microarray_dataset_stat_file ? "--microarray $microarray_dataset_stat_file" : "" """ aggregate_results.py \\ --counts $count_file \\ --stats $stat_file \\ - --metadata "$metadata_files" \\ - --mappings "$mapping_files" \\ + $mapping_files_arg \\ + $metadata_files_arg \\ $rnaseq_dataset_stat_file_arg \\ $microarray_dataset_stat_file_arg \\ """ diff --git a/subworkflows/local/merge_data/main.nf b/subworkflows/local/merge_data/main.nf index e70aaf1e..325612fa 100644 --- a/subworkflows/local/merge_data/main.nf +++ b/subworkflows/local/merge_data/main.nf @@ -101,6 +101,7 @@ workflow MERGE_DATA { // ----------------------------------------------------------------- ch_gene_id_mapping + .filter { it != [] } // handle case where there are no mappings .splitCsv( header: true ) .unique() .collectFile( @@ -112,6 +113,7 @@ workflow MERGE_DATA { ) { item -> "${item.original_gene_id},${item.ensembl_gene_id}" } + .ifEmpty([]) // handle case where there are no mappings .set { ch_whole_gene_id_mapping } // ----------------------------------------------------------------- @@ -119,6 +121,7 @@ workflow MERGE_DATA { // ----------------------------------------------------------------- ch_gene_metadata + .filter { it != [] } // handle case where there are no mappings .splitCsv( header: true ) .unique() .collectFile( @@ -130,6 +133,7 @@ workflow MERGE_DATA { ) { item -> "${item.ensembl_gene_id},${item.name},${item.description}" } + .ifEmpty([]) // handle case where there are no mappings .set { ch_whole_gene_metadata } emit: From 7b59db3959f3d6b5a7f56b8d6aa6db8f8b97747b Mon Sep 17 00:00:00 2001 From: Olivier Date: Tue, 18 Nov 2025 16:47:15 +0100 Subject: [PATCH 167/258] removed subsampling in quantile normalisations to allow full reproducibility --- bin/compute_stability_scores.py | 14 ++++++++------ bin/quantile_normalise.py | 8 ++++---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/bin/compute_stability_scores.py b/bin/compute_stability_scores.py index 23d8d4a0..2da538fe 100755 --- a/bin/compute_stability_scores.py +++ b/bin/compute_stability_scores.py @@ -3,14 +3,14 @@ # Written by Olivier Coen. Released under the MIT license. import argparse -import polars as pl -from pathlib import Path -from sklearn.preprocessing import QuantileTransformer +import logging from dataclasses import dataclass, field +from pathlib import Path from typing import ClassVar -import logging import config +import polars as pl +from sklearn.preprocessing import QuantileTransformer logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -52,7 +52,7 @@ def quantile_normalise(data: pl.Series, new_name: str) -> pl.Series: Quantile normalize a series """ array = data.to_numpy().reshape(-1, 1) - transformer = QuantileTransformer(output_distribution="uniform") + transformer = QuantileTransformer(output_distribution="uniform", subsample=None) normalised_array = transformer.fit_transform(array) return pl.Series(new_name, normalised_array.ravel()) @@ -212,7 +212,9 @@ def get_statistics(stat_files: list[Path]) -> pl.LazyFrame: def export_data(scored_df: pl.DataFrame): """Export gene expression data to CSV files.""" logger.info(f"Exporting stability scores to: {STATISTICS_WITH_SCORES_OUTFILENAME}") - scored_df.write_csv(STATISTICS_WITH_SCORES_OUTFILENAME) + scored_df.write_csv( + STATISTICS_WITH_SCORES_OUTFILENAME, float_precision=config.CSV_FLOAT_PRECISION + ) logger.info("Done") diff --git a/bin/quantile_normalise.py b/bin/quantile_normalise.py index 713cc91d..88d9f66c 100755 --- a/bin/quantile_normalise.py +++ b/bin/quantile_normalise.py @@ -3,12 +3,12 @@ # Written by Olivier Coen. Released under the MIT license. import argparse -from pathlib import Path -import pandas as pd -from sklearn.preprocessing import QuantileTransformer import logging +from pathlib import Path import config +import pandas as pd +from sklearn.preprocessing import QuantileTransformer logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -50,7 +50,7 @@ def quantile_normalise(data: pd.DataFrame, target_distribution: str): Quantile normalize a data matrix based on a target distribution. """ transformer = QuantileTransformer( - n_quantiles=N_QUANTILES, output_distribution=target_distribution + n_quantiles=N_QUANTILES, output_distribution=target_distribution, subsample=None ) normalised_data = pd.DataFrame(index=data.index, columns=data.columns) From 997b4937a08923e964658db6512f32b1d412dc1d Mon Sep 17 00:00:00 2001 From: Olivier Date: Wed, 19 Nov 2025 14:04:36 +0100 Subject: [PATCH 168/258] fix issue with design schema not taken into account --- assets/schema_datasets.json | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/schema_datasets.json b/assets/schema_datasets.json index b9416629..fa320e10 100644 --- a/assets/schema_datasets.json +++ b/assets/schema_datasets.json @@ -17,6 +17,7 @@ "design": { "type": "string", "format": "file-path", + "schema": "assets/schema_design.json", "exists": true, "pattern": "^\\S+\\.(csv|tsv|dat)$", "errorMessage": "You must provide a design file", From d2f8b58c834425a91800ecb106e197f31f0cdb55 Mon Sep 17 00:00:00 2001 From: Olivier Date: Wed, 19 Nov 2025 15:00:45 +0100 Subject: [PATCH 169/258] replace lazyframes by dataframes in clean_count_data.py --- bin/clean_count_data.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bin/clean_count_data.py b/bin/clean_count_data.py index 35f0ccfa..901debfe 100755 --- a/bin/clean_count_data.py +++ b/bin/clean_count_data.py @@ -62,14 +62,14 @@ def get_count_columns(lf: pl.LazyFrame) -> list[str]: def get_counts( file: Path, -) -> pl.LazyFrame: +) -> pl.DataFrame: # sorting dataframe (necessary to get consistent output) - return pl.scan_parquet(file).sort(config.ENSEMBL_GENE_ID_COLNAME, descending=False) + return pl.read_parquet(file).sort(config.ENSEMBL_GENE_ID_COLNAME, descending=False) def remove_samples_with_low_ks_pvalue( - count_lf: pl.LazyFrame, ks_stats_file: Path, ks_pvalue_threshold: float -) -> pl.LazyFrame: + count_lf: pl.DataFrame, ks_stats_file: Path, ks_pvalue_threshold: float +) -> pl.DataFrame: ks_stats_df = pl.read_csv(ks_stats_file, has_header=True).select( [config.SAMPLE_COLNAME, config.KS_TEST_COLNAME] ) @@ -103,8 +103,8 @@ def remove_samples_with_low_ks_pvalue( return count_lf.select([config.ENSEMBL_GENE_ID_COLNAME] + valid_samples) -def export_data(all_counts_lf: pl.LazyFrame): - all_counts_lf.collect().write_parquet(ALL_COUNTS_FILTERED_PARQUET_OUTFILENAME) +def export_data(all_counts_lf: pl.DataFrame): + all_counts_lf.write_parquet(ALL_COUNTS_FILTERED_PARQUET_OUTFILENAME) logger.info("Done") From cccac73e89e788938e91f1bcfec044480719ec96 Mon Sep 17 00:00:00 2001 From: Olivier Date: Wed, 19 Nov 2025 18:07:03 +0100 Subject: [PATCH 170/258] fix most test cases --- bin/aggregate_results.py | 46 +- bin/compute_base_statistics.py | 2 +- bin/compute_m_measures.py | 24 +- bin/config.py | 2 + bin/get_eatlas_accessions.py | 35 +- bin/get_gene_lengths.py | 131 ++ bin/get_geo_dataset_accessions.py | 21 +- bin/gprofiler_utils.py | 1 - bin/normalise_to_tpm.py | 110 ++ bin/normfinder.py | 18 +- bin/old/get_annotation_accession.py | 176 ++ bin/old/get_array_express_accessions.py | 415 +++++ modules/local/dash_app/main.nf | 1 + .../old/download_genome_annotation/main.nf | 27 + .../download_genome_annotation/spec-file.txt | 12 + .../old/get_annotation_accession/main.nf | 32 + .../get_annotation_accession/spec-file.txt | 53 + modules/local/quantile_normalisation/main.nf | 4 +- tests/.nftignore | 10 +- tests/default.nf.test | 65 +- tests/default.nf.test.snap | 1587 ++++++++--------- .../local/aggregate_results/main.nf.test.snap | 28 +- .../compute_base_statistics/main.nf.test.snap | 12 +- .../main.nf.test.snap | 12 +- .../expressionatlas/getdata/main.nf.test.snap | 90 +- .../local/geo/getdata/main.nf.test.snap | 283 ++- .../idmapping/gprofiler/main.nf.test.snap | 24 +- .../local/normalisation/deseq2/main.nf.test | 42 +- .../normalisation/deseq2/main.nf.test.snap | 24 +- .../local/normalisation/edger/main.nf.test | 46 +- .../normalisation/edger/main.nf.test.snap | 26 +- .../local/normfinder/main.nf.test.snap | 12 +- .../local/genorm/main.nf.test.snap | 6 +- tests/test_data/input_datasets/input.csv | 4 +- tests/test_data/input_datasets/input_big.yaml | 4 + 35 files changed, 2210 insertions(+), 1175 deletions(-) create mode 100755 bin/get_gene_lengths.py create mode 100755 bin/normalise_to_tpm.py create mode 100755 bin/old/get_annotation_accession.py create mode 100755 bin/old/get_array_express_accessions.py create mode 100644 modules/local/old/download_genome_annotation/main.nf create mode 100644 modules/local/old/download_genome_annotation/spec-file.txt create mode 100644 modules/local/old/get_annotation_accession/main.nf create mode 100644 modules/local/old/get_annotation_accession/spec-file.txt create mode 100644 tests/test_data/input_datasets/input_big.yaml diff --git a/bin/aggregate_results.py b/bin/aggregate_results.py index d1d6a009..ef22fc10 100755 --- a/bin/aggregate_results.py +++ b/bin/aggregate_results.py @@ -3,11 +3,11 @@ # Written by Olivier Coen. Released under the MIT license. import argparse -import polars as pl -from pathlib import Path import logging +from pathlib import Path import config +import polars as pl logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -61,11 +61,10 @@ def parse_args(): "--metadata", type=str, dest="metadata_files", - required=True, help="Metadata file", ) parser.add_argument( - "--mappings", type=str, dest="mapping_files", required=True, help="Mapping file" + "--mappings", type=str, dest="mapping_files", help="Mapping file" ) return parser.parse_args() @@ -147,16 +146,18 @@ def get_counts(file: Path) -> pl.LazyFrame: return pl.scan_parquet(file).sort(config.ENSEMBL_GENE_ID_COLNAME, descending=False) -def get_metadata(metadata_files: list[Path]) -> pl.LazyFrame: +def get_metadata(metadata_files: list[Path]) -> pl.LazyFrame | None: """Retrieve and concatenate metadata from a list of metadata files.""" + if not metadata_files: + return None return concat_cast_to_string_and_drop_duplicates(metadata_files) -def get_mappings(mapping_files: list[Path]) -> pl.LazyFrame: +def get_mappings(mapping_files: list[Path]) -> pl.LazyFrame | None: + if not mapping_files: + return None concat_lf = concat_cast_to_string_and_drop_duplicates(mapping_files) # group by new gene IDs and gets the lis - """Group by new gene IDs, get the list of distinct original gene IDs and convert to a string representation.""" - # t of distinct original gene IDs for each group # convert the list column to a string representation # separate the original gene IDs with a semicolon return concat_lf.group_by(config.ENSEMBL_GENE_ID_COLNAME).agg( @@ -230,7 +231,6 @@ def get_top_stable_genes_counts( .to_series() .to_list() ) - return sorted_transposed_counts_df.drop( ["sort_order", config.ENSEMBL_GENE_ID_COLNAME] ).transpose(column_names=actual_gene_names) @@ -244,12 +244,16 @@ def export_data( ): """Export gene expression data to CSV files.""" logger.info(f"Exporting statistics of all genes to: {ALL_GENE_SUMMARY_OUTFILENAME}") - all_genes_summary_lf.collect().write_csv(ALL_GENE_SUMMARY_OUTFILENAME) + all_genes_summary_lf.collect().write_csv( + ALL_GENE_SUMMARY_OUTFILENAME, float_precision=config.CSV_FLOAT_PRECISION + ) logger.info( f"Exporting statistics of the top stable genes to: {TOP_STABLE_GENE_SUMMARY_OUTFILENAME}" ) - top_stable_genes_summary_lf.collect().write_csv(TOP_STABLE_GENE_SUMMARY_OUTFILENAME) + top_stable_genes_summary_lf.collect().write_csv( + TOP_STABLE_GENE_SUMMARY_OUTFILENAME, float_precision=config.CSV_FLOAT_PRECISION + ) logger.info(f"Exporting all counts to: {ALL_COUNTS_FILTERED_PARQUET_OUTFILENAME}") all_counts_lf.collect().write_parquet(ALL_COUNTS_FILTERED_PARQUET_OUTFILENAME) @@ -257,7 +261,9 @@ def export_data( logger.info( f"Exporting counts of the top stable genes to: {TOP_STABLE_GENES_COUNTS_OUTFILENAME}" ) - top_stable_genes_counts_df.write_csv(TOP_STABLE_GENES_COUNTS_OUTFILENAME) + top_stable_genes_counts_df.write_csv( + TOP_STABLE_GENES_COUNTS_OUTFILENAME, float_precision=config.CSV_FLOAT_PRECISION + ) logger.info("Done") @@ -272,8 +278,16 @@ def export_data( def main(): args = parse_args() - metadata_files = [Path(file) for file in args.metadata_files.split(" ")] - mapping_files = [Path(file) for file in args.mapping_files.split(" ")] + metadata_files = ( + [Path(file) for file in args.metadata_files.split(" ")] + if args.metadata_files is not None + else [] + ) + mapping_files = ( + [Path(file) for file in args.mapping_files.split(" ")] + if args.mapping_files is not None + else [] + ) count_lf = get_counts(args.count_file) @@ -287,8 +301,9 @@ def main(): ] metadata_lf = get_metadata(metadata_files) mapping_lf = get_mappings(mapping_files) + optional_lfs = [lf for lf in [metadata_lf, mapping_lf] if lf is not None] - additional_data_lfs = [metadata_lf, mapping_lf] + platform_datasets_stat_lfs + additional_data_lfs = optional_lfs + platform_datasets_stat_lfs all_genes_summary_lf = get_all_genes_summary( all_genes_stat_summary_lf, *additional_data_lfs ) @@ -300,6 +315,7 @@ def main(): top_stable_genes_counts_df = get_top_stable_genes_counts( count_lf, top_stable_stat_summary_lf ) + # exporting computed data export_data( all_genes_summary_lf, diff --git a/bin/compute_base_statistics.py b/bin/compute_base_statistics.py index 45892497..ba8d9e91 100755 --- a/bin/compute_base_statistics.py +++ b/bin/compute_base_statistics.py @@ -251,7 +251,7 @@ def export_data(stat_lf: pl.LazyFrame, platform: str | None): else ALL_GENES_RESULT_OUTFILE_SUFFIX ) logger.info(f"Exporting statistics for all genes to: {outfile}") - stat_lf.collect().write_csv(outfile) + stat_lf.collect().write_csv(outfile, float_precision=config.CSV_FLOAT_PRECISION) logger.info("Done") diff --git a/bin/compute_m_measures.py b/bin/compute_m_measures.py index 4be8559f..35a2ce44 100755 --- a/bin/compute_m_measures.py +++ b/bin/compute_m_measures.py @@ -2,12 +2,12 @@ # Written by Olivier Coen. Released under the MIT license. -import polars as pl -from pathlib import Path import argparse import logging +from pathlib import Path import config +import polars as pl logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -100,7 +100,9 @@ def main(): ############################################################################# # MAKING A FOLDER FOR EACH CHUNK OF GENE IDS ############################################################################# - gene_ids = count_lf.select(config.ENSEMBL_GENE_ID_COLNAME).collect().to_series().to_list() + gene_ids = ( + count_lf.select(config.ENSEMBL_GENE_ID_COLNAME).collect().to_series().to_list() + ) gene_ids = sorted(gene_ids) chunksize = max( @@ -166,7 +168,9 @@ def main(): raise ValueError("Duplicate values found for gene IDs!") process_gene_ids = sorted( - m_measure_df.select(config.ENSEMBL_GENE_ID_COLNAME).to_series().to_list() + m_measure_df.select(config.ENSEMBL_GENE_ID_COLNAME) + .to_series() + .to_list() ) if process_gene_ids != gene_id_list_chunks[i]: raise ValueError("Incorrect gene IDs found!") @@ -190,9 +194,17 @@ def main(): # appending to output file if i == 0: - m_measure_df.write_csv(fout, include_header=True) + m_measure_df.write_csv( + fout, + include_header=True, + float_precision=config.CSV_FLOAT_PRECISION, + ) else: - m_measure_df.write_csv(fout, include_header=False) + m_measure_df.write_csv( + fout, + include_header=False, + float_precision=config.CSV_FLOAT_PRECISION, + ) logger.info(f"Number of gene IDs: {len(gene_ids)}") logger.info(f"Number of computed genes: {computed_genes}") diff --git a/bin/config.py b/bin/config.py index 0b51be6e..81ab465f 100644 --- a/bin/config.py +++ b/bin/config.py @@ -41,3 +41,5 @@ "cv": VARIATION_COEFFICIENT_COLNAME, "rcvm": ROBUST_COEFFICIENT_OF_VARIATION_MEDIAN_COLNAME, } + +CSV_FLOAT_PRECISION = 6 diff --git a/bin/get_eatlas_accessions.py b/bin/get_eatlas_accessions.py index 48926676..8f3ec985 100755 --- a/bin/get_eatlas_accessions.py +++ b/bin/get_eatlas_accessions.py @@ -3,21 +3,20 @@ # Written by Olivier Coen. Released under the MIT license. import argparse -import requests +import logging +from functools import partial +from multiprocessing import Pool + import pandas as pd +import requests +import yaml +from natural_language_utils import keywords_in_fields from tenacity import ( + before_sleep_log, retry, - retry_if_exception_type, stop_after_delay, wait_exponential, - before_sleep_log, ) -import yaml -from functools import partial -from multiprocessing import Pool -import logging - -from natural_language_utils import keywords_in_fields logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -30,17 +29,6 @@ FILTERED_EXPERIMENTS_WITH_KEYWORDS_OUTFILE_NAME = "filtered_experiments.keywords.yaml" -################################################################## -################################################################## -# EXCEPTIONS -################################################################## -################################################################## - - -class ExpressionAtlasNothingFoundError(Exception): - pass - - ################################################################## ################################################################## # FUNCTIONS @@ -67,7 +55,6 @@ def parse_args(): @retry( - retry=retry_if_exception_type(ExpressionAtlasNothingFoundError), stop=stop_after_delay(600), wait=wait_exponential(multiplier=1, min=1, max=30), before_sleep=before_sleep_log(logger, logging.WARNING), @@ -94,10 +81,10 @@ def get_data(url: str) -> dict: response = requests.get(url) if response.status_code == 200: return response.json() - elif response.status_code == 500: - raise ExpressionAtlasNothingFoundError else: - raise RuntimeError(f"Failed to retrieve data: {response.status_code}") + raise RuntimeError( + f"Failed to retrieve data: encountered error {response.status_code}" + ) def get_experiment_description(exp_dict: dict): diff --git a/bin/get_gene_lengths.py b/bin/get_gene_lengths.py new file mode 100755 index 00000000..d3fd1d87 --- /dev/null +++ b/bin/get_gene_lengths.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 + +# Written by Olivier Coen. Released under the MIT license. + +import argparse +import json +import logging +from pathlib import Path + +import pandas as pd +import requests +from tenacity import ( + before_sleep_log, + retry, + stop_after_delay, + wait_exponential, +) +from tqdm.contrib.concurrent import process_map + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +GENE_IDS_CHUNKSIZE = 50 # max allowed by Ensembl REST API + +ENSEMBL_REST_SERVER = "https://rest.ensembl.org" +SEQUENCE_INFO_EXT = "/sequence/id" +HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json", +} +STOP_RETRY_AFTER_DELAY = 600 + +OUTFILE = "gene_ids_lengths.csv" + + +################################################################## +################################################################## +# FUNCTIONS +################################################################## +################################################################## + + +def parse_args(): + parser = argparse.ArgumentParser("Get GEO Datasets accessions") + parser.add_argument( + "--genes", + type=Path, + dest="gene_file", + required=True, + help="File containing gene IDs", + ) + return parser.parse_args() + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# QUERIES TO ENSEMBL +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +@retry( + stop=stop_after_delay(STOP_RETRY_AFTER_DELAY), + wait=wait_exponential(multiplier=1, min=1, max=30), + before_sleep=before_sleep_log(logger, logging.WARNING), + retry_error_callback=(lambda _: {}), +) +def send_post_request_to_ensembl(gene_ids: list[str]) -> list[dict]: + data = {"ids": gene_ids, "type": "cdna"} + url = ENSEMBL_REST_SERVER + SEQUENCE_INFO_EXT + response = requests.post(url, headers=HEADERS, data=json.dumps(data)) + try: + response.raise_for_status() + except: + logger.error(f"Could not get info for genes {gene_ids}") + return [] + return response.json() + + +def get_gene_lengths(gene_ids: list[str]) -> list[dict]: + records = send_post_request_to_ensembl(gene_ids) + return [ + { + "gene_id": record["query"], + "transcript_id": record["id"], + "length": len(record["seq"]), + } + for record in records + if record.get("query") is not None and record.get("seq") is not None + ] + + +def chunk_list(lst: list, chunksize: int) -> list: + """Splits a list into chunks of a given size. + + Args: + lst (list): The list to split. + chunksize (int): The size of each chunk. + + Returns: + list: A list of chunks, where each chunk is a list of len(chunksize). + """ + return [lst[i : i + chunksize] for i in range(0, len(lst), chunksize)] + + +################################################################## +################################################################## +# MAIN +################################################################## +################################################################## + + +def main(): + args = parse_args() + + with open(args.gene_file, "r") as fin: + gene_ids = [line.strip() for line in fin] + + gene_id_chunks = chunk_list(gene_ids, GENE_IDS_CHUNKSIZE) + # getting gene lengths chunk by chunk + records_list = process_map(get_gene_lengths, gene_id_chunks, max_workers=12) + # flattening list of lists into a single list + records = [record for sublist in records_list for record in sublist] + + df = pd.DataFrame.from_dict(records) + # taking the length of the longest transcript for each gene + df = df.groupby("gene_id", as_index=False).agg({"length": "max"}) + + df.to_csv(OUTFILE, index=False, header=True) + + +if __name__ == "__main__": + main() diff --git a/bin/get_geo_dataset_accessions.py b/bin/get_geo_dataset_accessions.py index 7ee9f0de..ee04e57b 100755 --- a/bin/get_geo_dataset_accessions.py +++ b/bin/get_geo_dataset_accessions.py @@ -45,7 +45,9 @@ ENTREZ_EMAIL = "stableexpression@nfcore.com" PLATFORM_METADATA_CHUNKSIZE = 2000 -# NCBI_API_BASE_URL = "https://www.ncbi.nlm.nih.gov/geo/query/acc.cgi?view=data&acc={accession}" +NCBI_API_BASE_URL = ( + "https://www.ncbi.nlm.nih.gov/geo/query/acc.cgi?view=data&acc={accession}" +) STOP_RETRY_AFTER_DELAY = 600 NB_PROBE_IDS_TO_PARSE = 1000 @@ -652,11 +654,23 @@ def check_dataset_platforms( # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +def sort_if_list(x): + if isinstance(x, list): + return sorted(x) + else: + return x + + def export_dataset_metadatas( datasets: list[dict], output_file: str, clean_columns: bool = True ): if datasets: df = pd.DataFrame.from_dict(datasets) + # all dataframe contain the column "accession" + # sorting by accessions to ensure that outputs are reproducible + df.sort_values(by="accession", inplace=True) + for col in df.columns: + df[col] = df[col].apply(sort_if_list) # cleaning columns so that MultiQC can parse them if clean_columns: for col in df.columns: @@ -793,7 +807,10 @@ def main(): logger.info(f"Kept {len(selected_datasets)} datasets") # getting accessions of selected experiments - selected_accessions = [dataset["accession"] for dataset in selected_datasets] + # sorting accessions to ensure that outputs are reproducible + selected_accessions = sorted( + [dataset["accession"] for dataset in selected_datasets] + ) with open(ACCESSION_OUTFILE_NAME, "w") as fout: fout.write("\n".join(selected_accessions)) diff --git a/bin/gprofiler_utils.py b/bin/gprofiler_utils.py index 769abbf1..cb860832 100755 --- a/bin/gprofiler_utils.py +++ b/bin/gprofiler_utils.py @@ -3,7 +3,6 @@ # Written by Olivier Coen. Released under the MIT license. import logging -import sys import config import pandas as pd diff --git a/bin/normalise_to_tpm.py b/bin/normalise_to_tpm.py new file mode 100755 index 00000000..a81fe6fc --- /dev/null +++ b/bin/normalise_to_tpm.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 + +# Written by Olivier Coen. Released under the MIT license. + +import argparse +import logging +from pathlib import Path + +import config +import pandas as pd + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +TPM_NORM_SUFFIX = ".tpm.csv" + + +##################################################### +##################################################### +# FUNCTIONS +##################################################### +##################################################### + + +def parse_args(): + parser = argparse.ArgumentParser(description="Normalise data to TPM") + parser.add_argument( + "--counts", type=Path, dest="count_file", required=True, help="Count file" + ) + parser.add_argument( + "--annotation", + type=Path, + dest="annotation_file", + required=True, + help="File containing gene annotations (and gene lengths in particular)", + ) + return parser.parse_args() + + +def is_raw_counts(df: pd.DataFrame): + """Check if the data are raw counts (integers).""" + return all(df.dtypes.apply(lambda x: pd.api.types.is_integer_dtype(x))) + + +def is_tpm(df: pd.DataFrame): + """Check if the data are TPM (sum to 1e6 per sample).""" + sample_sums = df.sum(axis=0) + return all((sample_sums - 1e6).abs() < 1e-3) # Allow for floating-point precision + + +def is_fpkm_or_rpkm(df: pd.DataFrame): + """Check if the data are FPKM or RPKM (not raw, not TPM).""" + return not is_raw_counts(df) and not is_tpm(df) + + +def process_to_tpm(df: pd.DataFrame, gene_lengths: list): + """ + Process raw counts, FPKM, or RPKM to TPM. + - For raw counts: Calculate RPKM, then TPM. + - For FPKM/RPKM: Convert directly to TPM. + """ + if is_raw_counts(df): + # Calculate RPKM + total_reads = df.sum(axis=0) + rpkm = df.div(gene_lengths, axis=0) / total_reads * 1e9 + # Convert RPKM to TPM + tpm = rpkm.div(rpkm.sum(axis=0), axis=1) * 1e6 + return tpm + elif is_fpkm_or_rpkm(df): + # Convert FPKM/RPKM to TPM + tpm = df.div(df.sum(axis=0), axis=1) * 1e6 + return tpm + elif is_tpm(df): + print("Data are already TPM. No conversion needed.") + return df + else: + raise ValueError("Could not determine data type.") + + +def export_count_data(quantile_normalized_counts: pd.DataFrame, count_file: Path): + """Export gene expression data to CSV files.""" + # replace .csv / .tsv by .tpm.csv + outfilename = ".".join(count_file.name.split(".")[:-1]) + TPM_NORM_SUFFIX + logger.info(f"Exporting quantile normalised counts to: {outfilename}") + quantile_normalized_counts.reset_index().to_parquet(outfilename) + + +##################################################### +##################################################### +# MAIN +##################################################### +##################################################### + + +def main(): + args = parse_args() + count_file = args.count_file + + logger.info(f"Normalising {count_file.name}") + count_df = pd.read_csv(count_file, index_col=0) + count_df.index.name = config.ENSEMBL_GENE_ID_COLNAME + + quantile_normalized_counts = quantile_normalise(count_df, args.target_distribution) + + export_count_data(quantile_normalized_counts, count_file) + + +if __name__ == "__main__": + main() diff --git a/bin/normfinder.py b/bin/normfinder.py index faf4a64a..52a0fa57 100755 --- a/bin/normfinder.py +++ b/bin/normfinder.py @@ -2,18 +2,18 @@ # Written by Olivier Coen. Released under the MIT license. -import polars as pl -import sys import argparse -from pathlib import Path -from tqdm import tqdm +import logging +import sys from dataclasses import dataclass, field +from pathlib import Path from statistics import mean -import numpy as np -from numba import njit, prange -import logging import config +import numpy as np +import polars as pl +from numba import njit, prange +from tqdm import tqdm logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -493,7 +493,9 @@ def parse_args(): def export_stability(stabilities: pl.DataFrame): """Export stability values to CSV file.""" logger.info(f"Exporting stability values to: {STABILITY_OUTFILENAME}") - stabilities.write_csv(STABILITY_OUTFILENAME) + stabilities.write_csv( + STABILITY_OUTFILENAME, float_precision=config.CSV_FLOAT_PRECISION + ) def main(): diff --git a/bin/old/get_annotation_accession.py b/bin/old/get_annotation_accession.py new file mode 100755 index 00000000..d48d9d16 --- /dev/null +++ b/bin/old/get_annotation_accession.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 + +# Written by Olivier Coen. Released under the MIT license. + +import argparse +import logging +import sys + +import requests +from tenacity import ( + before_sleep_log, + retry, + stop_after_delay, + wait_exponential, +) + +logging.basicConfig( + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO +) +logger = logging.getLogger(__name__) + +# Modern NCBI API +NCBI_TAXONOMY_API_URL = "https://api.ncbi.nlm.nih.gov/datasets/v2/taxonomy" +NCBI_GENOME_DATASET_REPORT_API_URL = ( + "https://api.ncbi.nlm.nih.gov/datasets/v2/genome/taxon/{taxid}/dataset_report" +) +NCBI_GENOME_DATASET_REPORT_API_PARAMS = "filters.has_annotation=true&page_size=1000" +NCBI_API_HEADERS = {"accept": "application/json", "content-type": "application/json"} + +ACCESSION_FILE = "accession.txt" + + +##################################################### +##################################################### +# PARSER +##################################################### +##################################################### + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Get best assembly for a specific taxon ID" + ) + parser.add_argument("--species", type=str, required=True, help="Species name") + return parser.parse_args() + + +##################################################### +##################################################### +# REQUESTS +##################################################### +##################################################### + + +@retry( + stop=stop_after_delay(600), + wait=wait_exponential(multiplier=1, min=1, max=30), + before_sleep=before_sleep_log(logger, logging.WARNING), +) +def send_request_to_ncbi_taxonomy(taxid: str | int): + taxons = [str(taxid)] + data = {"taxons": taxons} + response = requests.post(NCBI_TAXONOMY_API_URL, headers=NCBI_API_HEADERS, json=data) + response.raise_for_status() + return response.json() + + +@retry( + stop=stop_after_delay(600), + wait=wait_exponential(multiplier=1, min=1, max=30), + before_sleep=before_sleep_log(logger, logging.WARNING), +) +def send_request_to_ncbi_genome_dataset_report_api(taxid: int): + url = NCBI_GENOME_DATASET_REPORT_API_URL.format(taxid=taxid) + url += f"?{NCBI_GENOME_DATASET_REPORT_API_PARAMS}" + response = requests.get(url, headers=NCBI_API_HEADERS) + response.raise_for_status() + return response.json() + + +##################################################### +##################################################### +# DATA HANDLING +##################################################### +##################################################### + + +def get_species_taxid(species: str) -> int: + result = send_request_to_ncbi_taxonomy(species) + + if len(result["taxonomy_nodes"]) > 1: + raise ValueError(f"Multiple taxids for species {species}") + metadata = result["taxonomy_nodes"][0] + + if "taxonomy" not in metadata: + logger.info(f"Could not find taxonomy results for species {species}") + if "errors" in metadata: + for error in metadata["errors"]: + logger.error(f"Error: {error['reason']}\n") + sys.exit(100) + return int(metadata["taxonomy"]["tax_id"]) + + +def get_assembly_with_best_stats(reports: list[dict]): + sorted_reports = sorted( + reports, + key=lambda x: ( + int(x.get("assembly_stats").get("total_sequence_length", 0)), + -int(x.get("assembly_stats", {}).get("total_number_of_chromosomes", 1e9)), + ), + reverse=True, + ) + return sorted_reports[0] + + +def get_current_assemblies(reports: list[dict]) -> dict | None: + current_assembly_reports = [ + report + for report in reports + if report.get("assembly_info", {}).get("refseq_category") == "reference genome" + ] + if not current_assembly_reports: + return None + + refseq_reports = [ + report + for report in current_assembly_reports + if report.get("source_database") == "SOURCE_DATABASE_REFSEQ" + ] + + if refseq_reports: + return refseq_reports[0] + else: + return None + + +def get_reference_assembly(reports: list[dict]) -> dict: + best_assembly_report = get_current_assemblies(reports) + if best_assembly_report is not None: + return best_assembly_report + else: + return get_assembly_with_best_stats(reports) + + +def format_species_name(species: str): + return species.replace("_", " ").lower() + + +##################################################### +##################################################### +# MAIN +##################################################### +##################################################### + +if __name__ == "__main__": + args = parse_args() + species = format_species_name(args.species) + + species_taxid = get_species_taxid(species) + logger.info(f"Species taxid: {species_taxid}") + + logger.info(f"Getting best NCBI assembly for taxid: {species_taxid}") + result = send_request_to_ncbi_genome_dataset_report_api(species_taxid) + + try: + reports = result["reports"] + best_assembly_report = get_reference_assembly(reports) + logger.info(f"Best assembly: {best_assembly_report['accession']}") + except Exception as e: + logger.error(f"Could not get any assembly for taxid {species_taxid}: {e}") + sys.exit(100) + + with open(ACCESSION_FILE, "w") as fout: + fout.write(best_assembly_report["accession"]) + + logger.info("Done") diff --git a/bin/old/get_array_express_accessions.py b/bin/old/get_array_express_accessions.py new file mode 100755 index 00000000..beb0ddd5 --- /dev/null +++ b/bin/old/get_array_express_accessions.py @@ -0,0 +1,415 @@ +#!/usr/bin/env python3 + +# Written by Olivier Coen. Released under the MIT license. + +import argparse +import logging +import math +import urllib.parse +from functools import partial +from multiprocessing import Pool + +import pandas as pd +import requests +import yaml +from natural_language_utils import keywords_in_fields +from tenacity import ( + before_sleep_log, + retry, + retry_if_exception_type, + stop_after_delay, + wait_exponential, +) +from tqdm import tqdm + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +SEARCH_URL = "https://www.ebi.ac.uk/biostudies/api/v1/arrayexpress/search?" +SEARCH_MAX_PAGE_SIZE = 100 +SEARCH_BASE_PARAMS = {"pageSize": SEARCH_MAX_PAGE_SIZE} + +STUDY_SEARCH_URL = ( + "https://www.ebi.ac.uk/biostudies/api/v1/arrayexpress/study/{accession}" +) +ACCESSION_OUTFILE_NAME = "accessions.txt" +# ALL_EXPERIMENTS_METADATA_OUTFILE_NAME = "all_experiments.metadata.tsv" +SPECIES_EXPERIMENTS_METADATA_OUTFILE_NAME = "species_experiments.metadata.tsv" +SELECTED_EXPERIMENTS_METADATA_OUTFILE_NAME = "selected_experiments.metadata.tsv" +FILTERED_EXPERIMENTS_WITH_KEYWORDS_OUTFILE_NAME = "filtered_experiments.keywords.yaml" + + +################################################################## +################################################################## +# FUNCTIONS +################################################################## +################################################################## + + +def parse_args(): + parser = argparse.ArgumentParser("Get expression atlas accessions") + parser.add_argument( + "--species", + type=str, + required=True, + help="Search Expression Atlas for this specific species", + ) + parser.add_argument( + "--keywords", + type=str, + nargs="*", + help="Keywords to search for in experiment description", + ) + parser.add_argument("--platform", type=str, help="Platform type") + return parser.parse_args() + + +@retry( + stop=stop_after_delay(600), + wait=wait_exponential(multiplier=1, min=1, max=30), + before_sleep=before_sleep_log(logger, logging.WARNING), +) +def get_data(url: str) -> dict: + """ + Queries a URL and returns the data as a JSON object + + Parameters + ---------- + url : str + The URL to query + + Returns + ------- + data : dict + The JSON object returned by the query + + Raises + ------ + RuntimeError + If the query fails + """ + response = requests.get(url) + if response.status_code == 200: + return response.json() + else: + raise RuntimeError( + f"Failed to retrieve data: encountered error {response.status_code}" + ) + + +def get_array_express_studies(species: str): + """ + Gets all experiments from Array Express + + Parameters + ---------- + species : str + Name of species. Example: "human" + + Returns + ------- + experiments : list + A list of experiment dictionaries + """ + nb_hits = None + page_number = 0 + results = [] + while not nb_hits or page_number * SEARCH_MAX_PAGE_SIZE < nb_hits: + params = {"organism": species, "page": page_number} + all_formatted_params = [ + f"{key}={value}" for key, value in (params | SEARCH_BASE_PARAMS).items() + ] + all_params_str = " AND ".join(all_formatted_params) + print(all_params_str) + query_url = SEARCH_URL + urllib.parse.quote(all_params_str) + logger.info(f"Sending request {query_url}") + result = get_data(query_url) + + if not result: + logger.warning(f"Failed to query Entrey Esearch with query: {query_url}") + continue + print() + # getting total nb of entries + if not nb_hits: + nb_hits = int(result["totalHits"]) + nb_iters = math.ceil(nb_hits / SEARCH_MAX_PAGE_SIZE) + pbar = tqdm(total=nb_iters) + + # if there is no entry for this species + if nb_hits == 0: + logger.info(f"No entries found for query: {query_url}") + return [] + + results += result + # setting next cursor to the next group + page_number += 1 + pbar.update(page_number) + + pbar.close() + return results + + +def get_experiment_description(exp_dict: dict): + """ + Gets the description from an experiment dictionary + + Parameters + ---------- + exp_dict : dict + The experiment dictionary + + Returns + ------- + description : str + The experiment description + + Raises + ------ + KeyError + If the description field is not found in the experiment dictionary + """ + if "experiment" in exp_dict: + if "description" in exp_dict["experiment"]: + return exp_dict["experiment"]["description"] + else: + raise KeyError(f"Could not find description field in {exp_dict}") + elif "experimentDescription" in exp_dict: + return exp_dict["experimentDescription"] + else: + raise KeyError(f"Could not find description field in {exp_dict}") + + +def get_experiment_accession(exp_dict: dict): + """ + Gets the accession from an experiment dictionary + + Parameters + ---------- + exp_dict : dict + The experiment dictionary + + Returns + ------- + accession : str + The experiment accession + + Raises + ------ + KeyError + If the accession field is not found in the experiment dictionary + """ + if "experiment" in exp_dict: + if "accession" in exp_dict["experiment"]: + return exp_dict["experiment"]["accession"] + else: + raise KeyError(f"Could not find accession field in {exp_dict}") + elif "experimentAccession" in exp_dict: + return exp_dict["experimentAccession"] + else: + raise KeyError(f"Could not find accession field in {exp_dict}") + + +def get_properties_values(exp_dict: dict): + """ + Gets all values from properties from an experiment dictionary + + Parameters + ---------- + exp_dict : dict + The experiment dictionary + + Returns + ------- + values : list + A list of all values from properties + """ + values = [] + for column_header_dict in exp_dict["columnHeaders"]: + key_found = False + for key in ["assayGroupSummary", "contrastSummary"]: + if key in column_header_dict: + for property_dict in column_header_dict[key]["properties"]: + values.append(property_dict["testValue"]) + key_found = True + break + if not key_found: + raise KeyError(f"Could not find property value in {column_header_dict}") + # removing empty strings + values = [value for value in values if value != ""] + # removing duplicates + return list(set(values)) + + +def get_platform_specific_studies(experiments: list[dict], platform: str): + """ + Gets all experiments for a given platform from Expression Atlas + Possible platforms in Expression Atlas are 'rnaseq', 'microarray', 'proteomics' + + Parameters + ---------- + experiments: list[str] + platform : str + Name of platform. Example: "rnaseq" + + Returns + ------- + experiments : list + A list of experiment dictionaries + """ + platform_experiments = [] + for exp_dict in experiments: + if technology_type := exp_dict.get("technologyType"): + parsed_technology_type = ( + technology_type[0] + if isinstance(technology_type, list) + else technology_type + ) + parsed_platform = ( + parsed_technology_type.lower().split(" ")[0].replace("-", "") + ) + if platform == parsed_platform: + platform_experiments.append(exp_dict) + return platform_experiments + + +def get_study_details(study: dict): + """ + Get details of a study + + Parameters + ---------- + study : dict + A dictionary containing study details + + Returns + ------- + study_details : dict + A dictionary containing study details + """ + url = STUDY_SEARCH_URL.format(accession=study["accession"]) + logger.info(f"Sending request {url}") + data = get_data(url) + return data["hits"] + + +def get_experiment_data(exp_dict: dict): + """ + Gets the full data for an experiment given its dictionary + + Parameters + ---------- + exp_dict : dict + The experiment dictionary + + Returns + ------- + exp_data : dict + The full experiment data + """ + exp_url = ALL_EXP_URL + exp_dict["experimentAccession"] + return get_data(exp_url) + + +def parse_experiment(exp_dict: dict): + # getting accession and description + accession = get_experiment_accession(exp_dict) + description = get_experiment_description(exp_dict) + # getting properties of this experiment + exp_data = get_experiment_data(exp_dict) + properties_values = get_properties_values(exp_data) + + return { + "accession": accession, + "description": description, + "properties": properties_values, + } + + +def filter_experiment_with_keywords(exp_dict: dict, keywords: list[str]) -> dict | None: + all_searchable_fields = [exp_dict["description"]] + exp_dict["properties"] + found_keywords = keywords_in_fields(all_searchable_fields, keywords) + # only returning experiments if found keywords + if found_keywords: + exp_dict["found_keywords"] = list(set(found_keywords)) + return exp_dict + else: + return None + + +def get_metadata_for_selected_experiments( + experiments: list[dict], results: list[dict] +) -> list[dict]: + filtered_accessions = [result_dict["accession"] for result_dict in results] + return [ + exp_dict + for exp_dict in experiments + if get_experiment_accession(exp_dict) in filtered_accessions + ] + + +def format_species_name(species: str) -> str: + return species.replace("_", " ").strip() + + +################################################################## +################################################################## +# MAIN +################################################################## +################################################################## + + +def main(): + args = parse_args() + + results = None + selected_accessions = [] + selected_experiments = [] + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # PARSING EXPRESSION ATLAS + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + # Getting arguments + species_name = format_species_name(args.species) + keywords = args.keywords + + logger.info(f"Getting experiments corresponding to species {species_name}") + all_studies = get_array_express_studies(species_name) + + if args.platform: + logger.info(f"Getting experiments corresponding to platform {args.platform}") + all_studies = get_platform_specific_studies(all_studies, args.platform) + + detailed_studies = [get_study_details(study) for study in all_studies] + print(detailed_studies[0]) + logger.info( + f"Found {len(species_experiments)} experiments for species {species_name}" + ) + + logger.info("Parsing experiments") + with Pool() as pool: + results = pool.map(parse_experiment, species_experiments) + + if keywords: + logger.info(f"Filtering experiments with keywords {keywords}") + func = partial(filter_experiment_with_keywords, keywords=keywords) + with Pool() as pool: + results = [res for res in pool.map(func, results) if res is not None] + + if results: + logger.info(f"Kept {len(results)} experiments") + # getting accessions of selected experiments + selected_accessions = [exp_dict["accession"] for exp_dict in results] + # keeping metadata only for selected experiments + selected_experiments = get_metadata_for_selected_experiments( + species_experiments, results + ) + + else: + logger.warning( + f"Could not find experiments for species {species_name} and keywords {keywords}" + ) + + +if __name__ == "__main__": + main() diff --git a/modules/local/dash_app/main.nf b/modules/local/dash_app/main.nf index 34fac912..a00ae851 100644 --- a/modules/local/dash_app/main.nf +++ b/modules/local/dash_app/main.nf @@ -49,6 +49,7 @@ process DASH_APP { # trying to launch the app # if the resulting exit code is not 124 (exit code of timeout) then there is an error + export PYTHONDONTWRITEBYTECODE=1 timeout 10 python app.py || exit_code=\$?; [ "\$exit_code" -eq 124 ] && exit 0 || exit 100 """ diff --git a/modules/local/old/download_genome_annotation/main.nf b/modules/local/old/download_genome_annotation/main.nf new file mode 100644 index 00000000..f28e7e33 --- /dev/null +++ b/modules/local/old/download_genome_annotation/main.nf @@ -0,0 +1,27 @@ +process DOWNLOAD_GENOME_ANNOTATION { + + label 'process_single' + + tag "$accession" + + conda "${moduleDir}/spec-file.txt" + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/a6/a6b13690259900baef6865722cb3a319103acc83b5bcab67504c88bde1e3a9f6/data': + 'community.wave.seqera.io/library/ncbi-datasets-cli_unzip:785aabe86637bae4' }" + + input: + val(accession) + + output: + path('genomic.gff'), emit: annotation + tuple val("${task.process}"), val('ncbi-datasets-cli'), eval("datasets --version | sed 's/datasets version: //g'"), topic: versions + + script: + """ + datasets download genome accession $accession --include gff3 + + unzip -o ncbi_dataset.zip + mv ncbi_dataset/data/${accession}/* . + """ + +} diff --git a/modules/local/old/download_genome_annotation/spec-file.txt b/modules/local/old/download_genome_annotation/spec-file.txt new file mode 100644 index 00000000..f979bd9f --- /dev/null +++ b/modules/local/old/download_genome_annotation/spec-file.txt @@ -0,0 +1,12 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda#3cd1a7238a0dd3d0860fdefc496cc854 +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.7.14-hbd8a1cb_0.conda#d16c90324aef024877d8713c0b7fea5b +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda#9e60c55e725c20d23125a5f0dd69af5d +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda#e66f2b8ad787e7beb0f846e4bd7e8493 +https://conda.anaconda.org/conda-forge/linux-64/ncbi-datasets-cli-18.5.0-ha770c72_0.conda#28b6b83d9152d8af1bebacdcc070c13a +https://conda.anaconda.org/conda-forge/linux-64/unzip-6.0-h7f98852_3.tar.bz2#7cb7109505433a5abbf68bb34b31edac diff --git a/modules/local/old/get_annotation_accession/main.nf b/modules/local/old/get_annotation_accession/main.nf new file mode 100644 index 00000000..ab2733da --- /dev/null +++ b/modules/local/old/get_annotation_accession/main.nf @@ -0,0 +1,32 @@ +process GET_ANNOTATION_ACCESSION { + + label 'process_single' + + tag "$species" + + conda "${moduleDir}/spec-file.txt" + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/b4/b4d686ef63e22bc4d461178fc241cefddd2aa3436e189d3787c8e019448f056e/data': + 'community.wave.seqera.io/library/requests_tenacity_tqdm:126dbed8ef3ff96f' }" + + input: + val(species) + + output: + env("ACCESSION"), emit: accession + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions + tuple val("${task.process}"), val('requests'), eval('python3 -c "import requests; print(requests.__version__)"'), topic: versions + tuple val("${task.process}"), val('tenacity'), eval('python3 -c "from importlib.metadata import version; print(version(\'tenacity\'))"'), topic: versions + + script: + """ + get_annotation_accession.py --species $species + ACCESSION=\$(cat accession.txt) + """ + + stub: + """ + touch accession.txt + """ + +} diff --git a/modules/local/old/get_annotation_accession/spec-file.txt b/modules/local/old/get_annotation_accession/spec-file.txt new file mode 100644 index 00000000..9c4d257a --- /dev/null +++ b/modules/local/old/get_annotation_accession/spec-file.txt @@ -0,0 +1,53 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://repo.anaconda.com/pkgs/main/linux-64/_libgcc_mutex-0.1-main.conda#c3473ff8bdb3d124ed5ff11ec380d6f9 +https://repo.anaconda.com/pkgs/main/linux-64/_openmp_mutex-5.1-1_gnu.conda#71d281e9c2192cb3fa425655a8defb85 +https://repo.anaconda.com/pkgs/main/linux-64/libgcc-15.2.0-h69a1729_7.conda#01fb1b8725fc7f66312b9d409758917a +https://repo.anaconda.com/pkgs/main/linux-64/libstdcxx-15.2.0-h39759b7_7.conda#7dc7ec61ceea5de17f3e2c4c5f442fc6 +https://repo.anaconda.com/pkgs/main/linux-64/libgcc-ng-15.2.0-h166f726_7.conda#2783efb2502b9caa7f08e25fd54df899 +https://repo.anaconda.com/pkgs/main/linux-64/bzip2-1.0.8-h5eee18b_6.conda#f21a3ff51c1b271977f53ce956a69297 +https://repo.anaconda.com/pkgs/main/linux-64/libstdcxx-ng-15.2.0-hc03a8fd_7.conda#cf200522c0b13d64bf81035358d05f5b +https://repo.anaconda.com/pkgs/main/linux-64/expat-2.7.3-h3385a95_0.conda#105822d24b4de9055e705a7d76549416 +https://repo.anaconda.com/pkgs/main/linux-64/ld_impl_linux-64-2.44-h153f514_2.conda#dffdc9a0e09d04051d4bd758e104f4b3 +https://repo.anaconda.com/pkgs/main/linux-64/libffi-3.4.4-h6a678d5_1.conda#70646cc713f0c43926cfdcfe9b695fe0 +https://repo.anaconda.com/pkgs/main/linux-64/libmpdec-4.0.0-h5eee18b_0.conda#feb10f42b1a7b523acbf85461be41a3e +https://repo.anaconda.com/pkgs/main/linux-64/libuuid-1.41.5-h5eee18b_0.conda#4a6a2354414c9080327274aa514e5299 +https://repo.anaconda.com/pkgs/main/linux-64/ncurses-6.5-h7934f7d_0.conda#0abfc090299da4bb031b84c64309757b +https://repo.anaconda.com/pkgs/main/linux-64/ca-certificates-2025.11.4-h06a4308_0.conda#f04cd5aa67216b77e8f664bb4c7098a4 +https://repo.anaconda.com/pkgs/main/linux-64/openssl-3.0.18-hd6dcaed_0.conda#3762b8999909b69745881cf4b8dd2816 +https://repo.anaconda.com/pkgs/main/linux-64/python_abi-3.13-1_cp313.conda#bea705c35663f9394ec82e87dc692c85 +https://repo.anaconda.com/pkgs/main/linux-64/readline-8.3-hc2a1206_0.conda#8578e006d4ef5cb98a6cda232b3490f6 +https://repo.anaconda.com/pkgs/main/linux-64/libzlib-1.3.1-hb25bd0a_0.conda#338ee51e19ee211b7fc994d4ba88c631 +https://repo.anaconda.com/pkgs/main/linux-64/zlib-1.3.1-hb25bd0a_0.conda#9f3a877e5e0fa0fb39253a59ff824861 +https://repo.anaconda.com/pkgs/main/linux-64/sqlite-3.51.0-h2a70700_0.conda#99a4278be9c6901ee6989b24fd213240 +https://repo.anaconda.com/pkgs/main/linux-64/pthread-stubs-0.3-h0ce48e5_1.conda#973a642312d2a28927aaf5b477c67250 +https://repo.anaconda.com/pkgs/main/linux-64/xorg-libxau-1.0.12-h9b100fa_0.conda#a8005a9f6eb903e113cd5363e8a11459 +https://repo.anaconda.com/pkgs/main/linux-64/xorg-libxdmcp-1.1.5-h9b100fa_0.conda#c284a09ddfba81d9c4e740110f09ea06 +https://repo.anaconda.com/pkgs/main/linux-64/libxcb-1.17.0-h9b100fa_0.conda#fdf0d380fa3809a301e2dbc0d5183883 +https://repo.anaconda.com/pkgs/main/linux-64/xorg-xorgproto-2024.1-h5eee18b_1.conda#412a0d97a7a51d23326e57226189da92 +https://repo.anaconda.com/pkgs/main/linux-64/xorg-libx11-1.8.12-h9b100fa_1.conda#6298b27afae6f49f03765b2a03df2fcb +https://repo.anaconda.com/pkgs/main/linux-64/tk-8.6.15-h54e0aa7_0.conda#1fa91e0c4fc9c9435eda3f1a25a676fd +https://repo.anaconda.com/pkgs/main/noarch/tzdata-2025b-h04d1e81_0.conda#1d027393db3427ab22a02aa44a56f143 +https://repo.anaconda.com/pkgs/main/linux-64/xz-5.6.4-h5eee18b_1.conda#3581505fa450962d631bd82b8616350e +https://repo.anaconda.com/pkgs/main/linux-64/python-3.13.9-h7e8bc2b_100_cp313.conda#9ea34b30a1bdb8f7c9d62c072697e681 +https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py313h09d1b84_0.conda#dfd94363b679c74937b3926731ee861a +https://conda.anaconda.org/conda-forge/noarch/certifi-2025.11.12-pyhd8ed1ab_0.conda#96a02a5c1a65470a7e4eedb644c872fd +https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda#12c566707c80111f9799308d9e265aef +https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py313hfab6e84_0.conda#ce6386a5892ef686d6d680c345c40ad1 +https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.4-pyhd8ed1ab_0.conda#a22d1fd9bf98827e280a02875d9a007a +https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda#0a802cb9888dd14eeefc611f05c40b6e +https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda#8e6923fc12f1fe8f8c4e5c9f343256ac +https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda#164fc43f0b53b6e3a7bc7dce5e4f1dc9 +https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda#53abe63df7e10a6ba605dc5f9f961d36 +https://repo.anaconda.com/pkgs/main/linux-64/libgomp-15.2.0-h4751f2c_7.conda#82025ed6da944bd419d42d9b1ff116aa +https://repo.anaconda.com/pkgs/main/linux-64/setuptools-80.9.0-py313h06a4308_0.conda#42ffd8d5a0c04d5e55431e3d4f6e8408 +https://repo.anaconda.com/pkgs/main/linux-64/wheel-0.45.1-py313h06a4308_0.conda#29057e876eedce0e37c2388c138a19f9 +https://repo.anaconda.com/pkgs/main/noarch/pip-25.3-pyhc872135_0.conda#f713912a259ec613b3832c3bc842e9d4 +https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda#461219d1a5bd61342293efa2c0c90eac +https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda#6432cb5d4ac0046c3ac0a8a0f95842f9 +https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.25.0-py313h54dd161_1.conda#710d4663806d0f72b2fb414e936223b5 +https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda#436c165519e140cb08d246a4472a9d6a +https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda#db0c6b99149880c8ba515cf4abe93ee4 +https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.2-pyhd8ed1ab_0.conda#5d99943f2ae3cc69e1ada12ce9d4d701 diff --git a/modules/local/quantile_normalisation/main.nf b/modules/local/quantile_normalisation/main.nf index c28a21fd..6a062f3f 100644 --- a/modules/local/quantile_normalisation/main.nf +++ b/modules/local/quantile_normalisation/main.nf @@ -22,8 +22,8 @@ process QUANTILE_NORMALISATION { script: """ - quantile_normalise.py \ - --counts $count_file \ + quantile_normalise.py \\ + --counts $count_file \\ --target-distrib $target_distribution """ diff --git a/tests/.nftignore b/tests/.nftignore index 83f7a0a5..fd424e3f 100644 --- a/tests/.nftignore +++ b/tests/.nftignore @@ -1,10 +1,4 @@ .DS_Store -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 pipeline_info/*.{html,json,txt,yml} +multiqc/** +**.parquet diff --git a/tests/default.nf.test b/tests/default.nf.test index ecf1f039..51729294 100644 --- a/tests/default.nf.test +++ b/tests/default.nf.test @@ -12,23 +12,45 @@ nextflow_pipeline { params { species = 'beta vulgaris' keywords = "leaf" + datasets = "https://raw.githubusercontent.com/nf-core/test-datasets/stableexpression/test_data/input_datasets/input_beta_vulgaris.csv" + outdir = "$outputDir" + } + } + + then { + def stable_name = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}']) + def stable_path = getAllFilesFromDir(params.outdir, ignoreFile: 'tests/.nftignore') + assertAll( + { assert workflow.success}, + { assert snapshot( + stable_name, + stable_path + ).match() } + ) + } + } + + test("-profile test_dataset_only") { + tag "test_dataset_only" + + when { + params { + species = 'mus musculus' + datasets = "https://raw.githubusercontent.com/nf-core/test-datasets/stableexpression/test_data/input_datasets/input_big.yaml" + skip_fetch_eatlas_accessions = true + skip_fetch_geo_accessions = true outdir = "$outputDir" } } then { - // stable_name: All files + folders in ${params.outdir}/ with a stable name def stable_name = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}']) - // stable_path: All files in ${params.outdir}/ with stable content def stable_path = getAllFilesFromDir(params.outdir, ignoreFile: 'tests/.nftignore') assertAll( { assert workflow.success}, { 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_stableexpression_software_mqc_versions.yml"), - // All stable path name, with a relative path stable_name, - // All files with stable contents stable_path ).match() } ) @@ -151,7 +173,34 @@ nextflow_pipeline { } } - /* + test("-profile test_skip_id_mapping") { + tag "test_skip_id_mapping" + + when { + params { + species = 'solanum tuberosum' + datasets = "${projectDir}/tests/test_data/input_datasets/input.csv" + skip_id_mapping = true + skip_fetch_eatlas_accessions = true + skip_fetch_geo_accessions = true + outdir = "$outputDir" + } + } + + then { + def stable_name = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}']) + def stable_path = getAllFilesFromDir(params.outdir, ignoreFile: 'tests/.nftignore') + assertAll( + { assert workflow.success}, + { assert snapshot( + stable_name, + stable_path + ).match() } + ) + } + } + + test("-profile test_dataset_custom_mapping") { tag "test_dataset_custom_mapping" @@ -160,6 +209,8 @@ nextflow_pipeline { species = 'solanum tuberosum' datasets = "${projectDir}/tests/test_data/input_datasets/input.csv" skip_id_mapping = true + skip_fetch_eatlas_accessions = true + skip_fetch_geo_accessions = true gene_id_mapping = "${projectDir}/tests/test_data/input_datasets/mapping.csv" gene_metadata = "${projectDir}/tests/test_data/input_datasets/metadata.csv" outdir = "$outputDir" @@ -179,7 +230,7 @@ nextflow_pipeline { ) } } - */ + test("-profile test_no_dataset_found") { tag "test_no_dataset_found" diff --git a/tests/default.nf.test.snap b/tests/default.nf.test.snap index 1e3bbbc5..bb9c291d 100644 --- a/tests/default.nf.test.snap +++ b/tests/default.nf.test.snap @@ -1,5 +1,5 @@ { - "-profile test_one_accession_low_gene_count": { + "-profile test_dataset_only": { "content": [ null, [ @@ -29,28 +29,14 @@ "dash_app/spec-file.txt", "dash_app/src", "dash_app/src/callbacks", - "dash_app/src/callbacks/__pycache__", - "dash_app/src/callbacks/__pycache__/common.cpython-313.pyc", - "dash_app/src/callbacks/__pycache__/genes.cpython-313.pyc", - "dash_app/src/callbacks/__pycache__/samples.cpython-313.pyc", "dash_app/src/callbacks/common.py", "dash_app/src/callbacks/genes.py", "dash_app/src/callbacks/samples.py", "dash_app/src/components", - "dash_app/src/components/__pycache__", - "dash_app/src/components/__pycache__/graphs.cpython-313.pyc", - "dash_app/src/components/__pycache__/right_sidebar.cpython-313.pyc", - "dash_app/src/components/__pycache__/stores.cpython-313.pyc", - "dash_app/src/components/__pycache__/tables.cpython-313.pyc", - "dash_app/src/components/__pycache__/tooltips.cpython-313.pyc", - "dash_app/src/components/__pycache__/top.cpython-313.pyc", "dash_app/src/components/graphs.py", "dash_app/src/components/icons.py", "dash_app/src/components/right_sidebar.py", "dash_app/src/components/settings", - "dash_app/src/components/settings/__pycache__", - "dash_app/src/components/settings/__pycache__/genes.cpython-313.pyc", - "dash_app/src/components/settings/__pycache__/samples.cpython-313.pyc", "dash_app/src/components/settings/genes.py", "dash_app/src/components/settings/samples.py", "dash_app/src/components/stores.py", @@ -58,29 +44,20 @@ "dash_app/src/components/tooltips.py", "dash_app/src/components/top.py", "dash_app/src/utils", - "dash_app/src/utils/__pycache__", - "dash_app/src/utils/__pycache__/config.cpython-313.pyc", - "dash_app/src/utils/__pycache__/data_management.cpython-313.pyc", - "dash_app/src/utils/__pycache__/style.cpython-313.pyc", "dash_app/src/utils/config.py", "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", "dash_app/versions.yml", "dataset_statistics", - "dataset_statistics/E_GEOD_51720_rnaseq.dataset_stats.csv", + "dataset_statistics/SRP254919.salmon.merged.gene_counts.top1000cov.assay.dataset_stats.csv", "errors", - "expression_atlas", - "expression_atlas/datasets", - "expression_atlas/datasets/E_GEOD_51720_rnaseq.design.csv", - "expression_atlas/datasets/E_GEOD_51720_rnaseq.rnaseq.raw.counts.csv", "geo", - "geo/excluded_geo_accessions.txt", "get_candidate_genes", "get_candidate_genes/candidate_counts.parquet", "idmapping", - "idmapping/E_GEOD_51720_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/E_GEOD_51720_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/SRP254919.salmon.merged.gene_counts.top1000cov.assay.mapping.csv", + "idmapping/SRP254919.salmon.merged.gene_counts.top1000cov.assay.metadata.csv", + "idmapping/SRP254919.salmon.merged.gene_counts.top1000cov.assay.renamed.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", "merge_all_counts", @@ -117,99 +94,116 @@ "multiqc/multiqc_report.html", "multiqc/versions.yml", "normalised", - "normalised/E_GEOD_51720_rnaseq", - "normalised/E_GEOD_51720_rnaseq/normalisation_deseq2", - "normalised/E_GEOD_51720_rnaseq/normalisation_deseq2/E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay", + "normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay/normalisation_deseq2", + "normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay/normalisation_deseq2/SRP254919.salmon.merged.gene_counts.top1000cov.assay.renamed.cpm.csv", "normfinder", "normfinder/stability_values.normfinder.csv", "pipeline_info", "pipeline_info/software_mqc_versions.yml", "quantile_normalised", - "quantile_normalised/E_GEOD_51720_rnaseq", - "quantile_normalised/E_GEOD_51720_rnaseq/E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", + "quantile_normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay", + "quantile_normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay/SRP254919.salmon.merged.gene_counts.top1000cov.assay.renamed.cpm.quant_norm.parquet", "warnings" ], [ - "all_counts_filtered.parquet:md5,0eed740e271b9838212fddbd91700198", - "all_genes_summary.csv:md5,b2858858c2db1fc583d2aa91b82fcfb6", - "top_stable_genes_summary.csv:md5,e462975f7e21414420b17e6fc98be70d", - "top_stable_genes_transposed_counts_filtered.csv:md5,34befd9532a2055c22cd82d594b33efd", - "cleaned_counts_filtered.parquet:md5,5cb41caa6a4f0a2bbd2c2bc44c0bd4a7", - "stats_all_genes.csv:md5,28d5526c41e39a26836c862b1d1d96b6", - "rnaseq.stats_all_genes.csv:md5,2c01fc90ce64a89b9c508dc6ef916501", - "stats_with_scores.csv:md5,5ae77ecc6e76e74bde5fc58698816e60", + "all_genes_summary.csv:md5,5f51fb8a0383a9cc6e5f68f038b2824b", + "top_stable_genes_summary.csv:md5,5f51fb8a0383a9cc6e5f68f038b2824b", + "top_stable_genes_transposed_counts_filtered.csv:md5,981651a618f221898767331191517a0b", + "stats_all_genes.csv:md5,7c8675eea31265f9ef3e9c807e4ade42", + "rnaseq.stats_all_genes.csv:md5,d3a3334d0a2fd7312f2576a6240997f2", + "stats_with_scores.csv:md5,be0e4235981e652d9ddf84fccb879267", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_counts.parquet:md5,c2c4a4eea8c2091bd4969d34fd7ff0e1", - "all_genes_summary.csv:md5,b2858858c2db1fc583d2aa91b82fcfb6", - "whole_design.csv:md5,d3aa542c4ad07d0051a84482fe6cd81c", + "all_genes_summary.csv:md5,5f51fb8a0383a9cc6e5f68f038b2824b", + "whole_design.csv:md5,f29515bc2c783e593fb9028127342593", "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", - "common.cpython-313.pyc:md5,5679a63ef45c9734d7a97b3e9d725cdf", - "genes.cpython-313.pyc:md5,d4724081f39b670c7456493e9eeaba4e", - "samples.cpython-313.pyc:md5,7f9a6de869a2dcc79d93a812ff4b3c9e", "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", - "graphs.cpython-313.pyc:md5,1a6ca6f62c9057aa0b53318a4638f292", - "right_sidebar.cpython-313.pyc:md5,741fac5305a328b9f42f9325915a51e6", - "stores.cpython-313.pyc:md5,732530c11da62620189f7f1e66cf3f75", - "tables.cpython-313.pyc:md5,a3007cd8919e0ccfa3a2c15eff11dea5", - "tooltips.cpython-313.pyc:md5,0393a3e8c1a9c17f3537489be39d1dbf", - "top.cpython-313.pyc:md5,adc6059ed209a47a058734447ac35a42", "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", - "genes.cpython-313.pyc:md5,93ec2bbc17fcb4d21eec837b9a86f9c7", - "samples.cpython-313.pyc:md5,bf034337885b1172d48aeae1dbc91cfc", "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", "tables.py:md5,84d17ad7f43458412e550f74a24560e5", "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.cpython-313.pyc:md5,9a4884625b79f671334af231867b37af", - "data_management.cpython-313.pyc:md5,d6ce46aaea7c100bf4f89882cdc1b37c", - "style.cpython-313.pyc:md5,b3a815c7ae0123dd33d1a3bfd553a24d", "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", "style.py:md5,b9f1207f06464e43c05e6e3912f12731", "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "E_GEOD_51720_rnaseq.dataset_stats.csv:md5,428c31aba1b6ba8af014d7a80e21bd97", - "E_GEOD_51720_rnaseq.design.csv:md5,80805afb29837b6fbb73a6aa6f3a461b", - "E_GEOD_51720_rnaseq.rnaseq.raw.counts.csv:md5,07cd448196fc2fea4663bd9705da2b98", - "excluded_geo_accessions.txt:md5,cdbec776e8b1d9dc7d0aa44aaf52aa50", - "candidate_counts.parquet:md5,1d25a87d5e815b78c0cf2aa018130319", - "E_GEOD_51720_rnaseq.rnaseq.raw.counts.mapping.csv:md5,1bbc8cddf18072309e81498806aa73ff", - "E_GEOD_51720_rnaseq.rnaseq.raw.counts.metadata.csv:md5,08861c29159a6a2fed38efe523ed9c56", - "E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.csv:md5,ed0d6193d4f39e5e4000f1ddbee521bf", - "whole_gene_id_mapping.csv:md5,1bbc8cddf18072309e81498806aa73ff", - "whole_gene_metadata.csv:md5,a08ab44a98542a8529d2a4b5a47e4408", - "all_counts.parquet:md5,c2c4a4eea8c2091bd4969d34fd7ff0e1", - "all_counts.parquet:md5,c2c4a4eea8c2091bd4969d34fd7ff0e1", - "whole_design.csv:md5,d3aa542c4ad07d0051a84482fe6cd81c", - "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", - "multiqc_expression_distributions_top_stable_genes.txt:md5,66a44969a7b827943a31406bb6e1eca7", - "multiqc_gene_statistics.txt:md5,2a7dfaf41ff7ea555808d01be43616fb", - "multiqc_ranked_top_stable_genes_summary.txt:md5,f00ef628aa055283744c1653fcb9e5b7", - "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030", - "E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,7b76036297abbe80891c05f2e0af9647", - "stability_values.normfinder.csv:md5,c32298f5ba2ab39ea954925fe86d2d64", - "E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,3a76d77541b2fe103ed56b7e7b54de8f" + "SRP254919.salmon.merged.gene_counts.top1000cov.assay.dataset_stats.csv:md5,b1eade259446b4134a7e357aee92911a", + "SRP254919.salmon.merged.gene_counts.top1000cov.assay.mapping.csv:md5,7858e0e480ead97ecdfceea91be68b0f", + "SRP254919.salmon.merged.gene_counts.top1000cov.assay.metadata.csv:md5,7477308e2615ced44692cacb4ace178e", + "SRP254919.salmon.merged.gene_counts.top1000cov.assay.renamed.csv:md5,797b5e4f3a3f83f6c8d9aa7b35ddc3c6", + "whole_gene_id_mapping.csv:md5,9923ab8dfbe4bdbee956684dd7a1ec92", + "whole_gene_metadata.csv:md5,e1c71f34a81565a15aaf6db420d56c4a", + "whole_design.csv:md5,f29515bc2c783e593fb9028127342593", + "SRP254919.salmon.merged.gene_counts.top1000cov.assay.renamed.cpm.csv:md5,45db1f53c79ff459ad5689bbb4e0a1ca", + "stability_values.normfinder.csv:md5,68f8cb62b8ba78ff6ea2dd11e9e988e4" ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-13T11:24:15.307429751" + "timestamp": "2025-11-19T15:09:38.05737019" }, - "-profile test_download_only": { + "-profile test_eatlas_only_with_keywords": { "content": [ null, [ + "aggregate_results", + "aggregate_results/all_counts_filtered.parquet", + "aggregate_results/all_genes_summary.csv", + "aggregate_results/top_stable_genes_summary.csv", + "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", + "clean_count_data", + "clean_count_data/cleaned_counts_filtered.parquet", + "compute_base_statistics", + "compute_base_statistics/stats_all_genes.csv", + "compute_base_statistics_for_rnaseq", + "compute_base_statistics_for_rnaseq/rnaseq.stats_all_genes.csv", + "compute_stability_scores", + "compute_stability_scores/stats_with_scores.csv", + "dash_app", + "dash_app/app.py", + "dash_app/assets", + "dash_app/assets/style.css", + "dash_app/data", + "dash_app/data/all_counts.parquet", + "dash_app/data/all_genes_summary.csv", + "dash_app/data/whole_design.csv", + "dash_app/file_system_backend", + "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", + "dash_app/spec-file.txt", + "dash_app/src", + "dash_app/src/callbacks", + "dash_app/src/callbacks/common.py", + "dash_app/src/callbacks/genes.py", + "dash_app/src/callbacks/samples.py", + "dash_app/src/components", + "dash_app/src/components/graphs.py", + "dash_app/src/components/icons.py", + "dash_app/src/components/right_sidebar.py", + "dash_app/src/components/settings", + "dash_app/src/components/settings/genes.py", + "dash_app/src/components/settings/samples.py", + "dash_app/src/components/stores.py", + "dash_app/src/components/tables.py", + "dash_app/src/components/tooltips.py", + "dash_app/src/components/top.py", + "dash_app/src/utils", + "dash_app/src/utils/config.py", + "dash_app/src/utils/data_management.py", + "dash_app/src/utils/style.py", + "dash_app/versions.yml", + "dataset_statistics", + "dataset_statistics/E_MTAB_8187_rnaseq.dataset_stats.csv", "errors", - "errors/geo_failure_reasons.csv", "expression_atlas", "expression_atlas/accessions", "expression_atlas/accessions/accessions.txt", @@ -219,12 +213,20 @@ "expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", "expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", "geo", - "geo/accessions", - "geo/accessions/accessions.txt", - "geo/accessions/geo_all_datasets.metadata.tsv", - "geo/accessions/geo_selected_datasets.metadata.tsv", - "geo/datasets", - "geo/datasets/failure_reason.txt", + "get_candidate_genes", + "get_candidate_genes/candidate_counts.parquet", + "idmapping", + "idmapping/E_MTAB_8187_rnaseq.rnaseq.raw.counts.mapping.csv", + "idmapping/E_MTAB_8187_rnaseq.rnaseq.raw.counts.metadata.csv", + "idmapping/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/whole_gene_id_mapping.csv", + "idmapping/whole_gene_metadata.csv", + "merge_all_counts", + "merge_all_counts/all_counts.parquet", + "merge_rnaseq_counts", + "merge_rnaseq_counts/all_counts.parquet", + "merged_datasets", + "merged_datasets/whole_design.csv", "multiqc", "multiqc/multiqc_data", "multiqc/multiqc_data/llms-full.txt", @@ -234,65 +236,249 @@ "multiqc/multiqc_data/multiqc_data.json", "multiqc/multiqc_data/multiqc_eatlas_all_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_failure_reasons.txt", - "multiqc/multiqc_data/multiqc_geo_selected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", + "multiqc/multiqc_data/multiqc_gene_statistics.txt", + "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", "multiqc/multiqc_plots", "multiqc/multiqc_plots/pdf", "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_failure_reasons.pdf", - "multiqc/multiqc_plots/pdf/geo_selected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", + "multiqc/multiqc_plots/pdf/gene_statistics.pdf", + "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", "multiqc/multiqc_plots/png", "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_failure_reasons.png", - "multiqc/multiqc_plots/png/geo_selected_experiments_metadata.png", + "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", + "multiqc/multiqc_plots/png/gene_statistics.png", + "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", "multiqc/multiqc_plots/svg", "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_failure_reasons.svg", - "multiqc/multiqc_plots/svg/geo_selected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", + "multiqc/multiqc_plots/svg/gene_statistics.svg", + "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", + "normalised", + "normalised/E_MTAB_8187_rnaseq", + "normalised/E_MTAB_8187_rnaseq/normalisation_deseq2", + "normalised/E_MTAB_8187_rnaseq/normalisation_deseq2/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normfinder", + "normfinder/stability_values.normfinder.csv", "pipeline_info", "pipeline_info/software_mqc_versions.yml", + "quantile_normalised", + "quantile_normalised/E_MTAB_8187_rnaseq", + "quantile_normalised/E_MTAB_8187_rnaseq/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", "warnings" ], [ - "geo_failure_reasons.csv:md5,17976bf17f7f0a5f0deca2cbaf28fac6", + "all_genes_summary.csv:md5,b20bf9df3ba0d7d82510fa632f5531df", + "top_stable_genes_summary.csv:md5,3f6ad9a7f2f184b7a4a4651935b1da42", + "top_stable_genes_transposed_counts_filtered.csv:md5,b9e7e88055bd0ff844bce2787c795683", + "stats_all_genes.csv:md5,0262d1ed114708648080d917cf6d735d", + "rnaseq.stats_all_genes.csv:md5,f898b1e455ab27d1c4b3642ec0680b98", + "stats_with_scores.csv:md5,b2e3634ccae3e031d32feff0bd3dfd1f", + "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", + "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", + "all_genes_summary.csv:md5,b20bf9df3ba0d7d82510fa632f5531df", + "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", + "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", + "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", + "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", + "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", + "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", + "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", + "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", + "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", + "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", + "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", + "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", + "tables.py:md5,84d17ad7f43458412e550f74a24560e5", + "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", + "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", + "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", + "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", + "style.py:md5,b9f1207f06464e43c05e6e3912f12731", + "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", + "E_MTAB_8187_rnaseq.dataset_stats.csv:md5,cfe732af026c75716331b4c043f0828c", "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", - "accessions.txt:md5,530c21fd138405dc6bcdd275633c07ef", - "geo_all_datasets.metadata.tsv:md5,b810eae9a69f6a1b1e1db9444fa593fa", - "geo_selected_datasets.metadata.tsv:md5,b810eae9a69f6a1b1e1db9444fa593fa", - "failure_reason.txt:md5,2631bb8c3ae982a1b869107f8dbfa107", - "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", - "multiqc_eatlas_all_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", - "multiqc_eatlas_selected_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", - "multiqc_geo_all_experiments_metadata.txt:md5,faf1630bcea828f358a5dbe934a0d7b6", - "multiqc_geo_failure_reasons.txt:md5,8eb73de29967d7d43b28d7edf167dcac", - "multiqc_geo_selected_experiments_metadata.txt:md5,faf1630bcea828f358a5dbe934a0d7b6", - "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030" + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.mapping.csv:md5,d95581c2b10beed3ba4a4556c1afc44f", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.metadata.csv:md5,faf9789296e5578fd31efee8d0c40ca9", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv:md5,92d1acd8e9d5e5c8f44b25e1ffce66c2", + "whole_gene_id_mapping.csv:md5,d95581c2b10beed3ba4a4556c1afc44f", + "whole_gene_metadata.csv:md5,faf9789296e5578fd31efee8d0c40ca9", + "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,e8dfabbc566f7c096c1a850084d4768b", + "stability_values.normfinder.csv:md5,64082412b0d0c0903b3677ebfc4131af" ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-13T11:22:05.641306836" + "timestamp": "2025-11-19T15:15:16.561840879" }, - "-profile test_eatlas_only_with_keywords": { + "-profile test_skip_id_mapping": { + "content": [ + [ + "aggregate_results", + "aggregate_results/all_counts_filtered.parquet", + "aggregate_results/all_genes_summary.csv", + "aggregate_results/top_stable_genes_summary.csv", + "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", + "clean_count_data", + "clean_count_data/cleaned_counts_filtered.parquet", + "compute_base_statistics", + "compute_base_statistics/stats_all_genes.csv", + "compute_base_statistics_for_microarray", + "compute_base_statistics_for_microarray/microarray.stats_all_genes.csv", + "compute_base_statistics_for_rnaseq", + "compute_base_statistics_for_rnaseq/rnaseq.stats_all_genes.csv", + "compute_stability_scores", + "compute_stability_scores/stats_with_scores.csv", + "dash_app", + "dash_app/app.py", + "dash_app/assets", + "dash_app/assets/style.css", + "dash_app/data", + "dash_app/data/all_counts.parquet", + "dash_app/data/all_genes_summary.csv", + "dash_app/data/whole_design.csv", + "dash_app/file_system_backend", + "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", + "dash_app/spec-file.txt", + "dash_app/src", + "dash_app/src/callbacks", + "dash_app/src/callbacks/common.py", + "dash_app/src/callbacks/genes.py", + "dash_app/src/callbacks/samples.py", + "dash_app/src/components", + "dash_app/src/components/graphs.py", + "dash_app/src/components/icons.py", + "dash_app/src/components/right_sidebar.py", + "dash_app/src/components/settings", + "dash_app/src/components/settings/genes.py", + "dash_app/src/components/settings/samples.py", + "dash_app/src/components/stores.py", + "dash_app/src/components/tables.py", + "dash_app/src/components/tooltips.py", + "dash_app/src/components/top.py", + "dash_app/src/utils", + "dash_app/src/utils/config.py", + "dash_app/src/utils/data_management.py", + "dash_app/src/utils/style.py", + "dash_app/versions.yml", + "dataset_statistics", + "dataset_statistics/microarray.normalised.dataset_stats.csv", + "dataset_statistics/rnaseq.raw.dataset_stats.csv", + "errors", + "geo", + "get_candidate_genes", + "get_candidate_genes/candidate_counts.parquet", + "idmapping", + "merge_all_counts", + "merge_all_counts/all_counts.parquet", + "merge_microarray_counts", + "merge_microarray_counts/all_counts.parquet", + "merge_rnaseq_counts", + "merge_rnaseq_counts/all_counts.parquet", + "merged_datasets", + "merged_datasets/whole_design.csv", + "multiqc", + "multiqc/multiqc_data", + "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_expression_distributions_top_stable_genes.txt", + "multiqc/multiqc_data/multiqc_gene_statistics.txt", + "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", + "multiqc/multiqc_data/multiqc_software_versions.txt", + "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_plots", + "multiqc/multiqc_plots/pdf", + "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", + "multiqc/multiqc_plots/pdf/gene_statistics.pdf", + "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", + "multiqc/multiqc_plots/png", + "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", + "multiqc/multiqc_plots/png/gene_statistics.png", + "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", + "multiqc/multiqc_plots/svg", + "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", + "multiqc/multiqc_plots/svg/gene_statistics.svg", + "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", + "multiqc/multiqc_report.html", + "multiqc/versions.yml", + "normalised", + "normalised/rnaseq.raw", + "normalised/rnaseq.raw/normalisation_deseq2", + "normalised/rnaseq.raw/normalisation_deseq2/rnaseq.raw.cpm.csv", + "normfinder", + "normfinder/stability_values.normfinder.csv", + "pipeline_info", + "pipeline_info/software_mqc_versions.yml", + "quantile_normalised", + "quantile_normalised/microarray.normalised", + "quantile_normalised/microarray.normalised/microarray.normalised.quant_norm.parquet", + "quantile_normalised/rnaseq.raw", + "quantile_normalised/rnaseq.raw/rnaseq.raw.cpm.quant_norm.parquet", + "warnings" + ], + [ + "all_genes_summary.csv:md5,14fb1614a449f7162987cd9195d0cb1c", + "top_stable_genes_summary.csv:md5,14fb1614a449f7162987cd9195d0cb1c", + "top_stable_genes_transposed_counts_filtered.csv:md5,20d963b92dcf1b4d85c6e878cff17253", + "stats_all_genes.csv:md5,08b6baca5dd63d9e57b9b1c080a901c6", + "microarray.stats_all_genes.csv:md5,69c6a3908fa708e8f3670a9e96321f7d", + "rnaseq.stats_all_genes.csv:md5,427f59e5c2b5920f83bab591268e1fcb", + "stats_with_scores.csv:md5,f38a32ae4520ec843b076502b2d80074", + "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", + "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", + "all_genes_summary.csv:md5,14fb1614a449f7162987cd9195d0cb1c", + "whole_design.csv:md5,70d6c2673e619ca52d2774fb3e368382", + "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", + "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", + "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", + "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", + "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", + "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", + "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", + "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", + "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", + "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", + "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", + "tables.py:md5,84d17ad7f43458412e550f74a24560e5", + "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", + "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", + "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", + "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", + "style.py:md5,b9f1207f06464e43c05e6e3912f12731", + "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", + "microarray.normalised.dataset_stats.csv:md5,4993a895b6561b9a4f939906bae582b4", + "rnaseq.raw.dataset_stats.csv:md5,c407ed714414e689c760fe1b450d5f58", + "whole_design.csv:md5,70d6c2673e619ca52d2774fb3e368382", + "rnaseq.raw.cpm.csv:md5,59f604e6266fbc46948287f384c3639b", + "stability_values.normfinder.csv:md5,4e37152d1f0a8a4376b86cbf64b9f73c" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-19T14:43:26.609074576" + }, + "-profile test": { "content": [ - null, [ "aggregate_results", "aggregate_results/all_counts_filtered.parquet", @@ -320,28 +506,14 @@ "dash_app/spec-file.txt", "dash_app/src", "dash_app/src/callbacks", - "dash_app/src/callbacks/__pycache__", - "dash_app/src/callbacks/__pycache__/common.cpython-313.pyc", - "dash_app/src/callbacks/__pycache__/genes.cpython-313.pyc", - "dash_app/src/callbacks/__pycache__/samples.cpython-313.pyc", "dash_app/src/callbacks/common.py", "dash_app/src/callbacks/genes.py", "dash_app/src/callbacks/samples.py", "dash_app/src/components", - "dash_app/src/components/__pycache__", - "dash_app/src/components/__pycache__/graphs.cpython-313.pyc", - "dash_app/src/components/__pycache__/right_sidebar.cpython-313.pyc", - "dash_app/src/components/__pycache__/stores.cpython-313.pyc", - "dash_app/src/components/__pycache__/tables.cpython-313.pyc", - "dash_app/src/components/__pycache__/tooltips.cpython-313.pyc", - "dash_app/src/components/__pycache__/top.cpython-313.pyc", "dash_app/src/components/graphs.py", "dash_app/src/components/icons.py", "dash_app/src/components/right_sidebar.py", "dash_app/src/components/settings", - "dash_app/src/components/settings/__pycache__", - "dash_app/src/components/settings/__pycache__/genes.cpython-313.pyc", - "dash_app/src/components/settings/__pycache__/samples.cpython-313.pyc", "dash_app/src/components/settings/genes.py", "dash_app/src/components/settings/samples.py", "dash_app/src/components/stores.py", @@ -349,10 +521,6 @@ "dash_app/src/components/tooltips.py", "dash_app/src/components/top.py", "dash_app/src/utils", - "dash_app/src/utils/__pycache__", - "dash_app/src/utils/__pycache__/config.cpython-313.pyc", - "dash_app/src/utils/__pycache__/data_management.cpython-313.pyc", - "dash_app/src/utils/__pycache__/style.cpython-313.pyc", "dash_app/src/utils/config.py", "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", @@ -360,6 +528,7 @@ "dataset_statistics", "dataset_statistics/E_MTAB_8187_rnaseq.dataset_stats.csv", "errors", + "errors/normalisation_failure_reasons.tsv", "expression_atlas", "expression_atlas/accessions", "expression_atlas/accessions/accessions.txt", @@ -369,12 +538,22 @@ "expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", "expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", "geo", + "geo/accessions", + "geo/accessions/accessions.txt", + "geo/accessions/geo_all_datasets.metadata.tsv", + "geo/accessions/geo_rejected_datasets.metadata.tsv", + "geo/accessions/geo_selected_datasets.metadata.tsv", + "geo/datasets", + "geo/datasets/warning_reason.txt", "get_candidate_genes", "get_candidate_genes/candidate_counts.parquet", "idmapping", "idmapping/E_MTAB_8187_rnaseq.rnaseq.raw.counts.mapping.csv", "idmapping/E_MTAB_8187_rnaseq.rnaseq.raw.counts.metadata.csv", "idmapping/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/beta_vulgaris.rnaseq.raw.counts.mapping.csv", + "idmapping/beta_vulgaris.rnaseq.raw.counts.metadata.csv", + "idmapping/beta_vulgaris.rnaseq.raw.counts.renamed.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", "merge_all_counts", @@ -394,6 +573,10 @@ "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", "multiqc/multiqc_data/multiqc_gene_statistics.txt", + "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_rejected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_selected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_warning_reasons.txt", "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", @@ -403,18 +586,30 @@ "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", "multiqc/multiqc_plots/pdf/gene_statistics.pdf", + "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_rejected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_selected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_warning_reasons.pdf", "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", "multiqc/multiqc_plots/png", "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", "multiqc/multiqc_plots/png/gene_statistics.png", + "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_rejected_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_selected_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_warning_reasons.png", "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", "multiqc/multiqc_plots/svg", "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", "multiqc/multiqc_plots/svg/gene_statistics.svg", + "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_rejected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_selected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_warning_reasons.svg", "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", @@ -422,6 +617,9 @@ "normalised/E_MTAB_8187_rnaseq", "normalised/E_MTAB_8187_rnaseq/normalisation_deseq2", "normalised/E_MTAB_8187_rnaseq/normalisation_deseq2/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normalised/beta_vulgaris.rnaseq.raw.counts", + "normalised/beta_vulgaris.rnaseq.raw.counts/normalisation_deseq2", + "normalised/beta_vulgaris.rnaseq.raw.counts/normalisation_deseq2/failure_reason.txt", "normfinder", "normfinder/stability_values.normfinder.csv", "pipeline_info", @@ -429,88 +627,143 @@ "quantile_normalised", "quantile_normalised/E_MTAB_8187_rnaseq", "quantile_normalised/E_MTAB_8187_rnaseq/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", - "warnings" + "warnings", + "warnings/geo_warning_reasons.csv" ], [ - "all_counts_filtered.parquet:md5,57325f7ce7f6571616b4e7f2a534c040", - "all_genes_summary.csv:md5,c382ff75abdd1b206e047edb83699708", - "top_stable_genes_summary.csv:md5,7b0892950a087f3853980486a01508b9", - "top_stable_genes_transposed_counts_filtered.csv:md5,d76c1c041caa7e4903357a88e51e72b9", - "cleaned_counts_filtered.parquet:md5,cb769635217b0b8a17d3b220a446d283", - "stats_all_genes.csv:md5,7dfe60333595d41b2e4da1e62ced9e17", - "rnaseq.stats_all_genes.csv:md5,2a2704fd5f9b2a4d60467903d044638a", - "stats_with_scores.csv:md5,06b2973f3633a6dec92f709793f7b35d", + "all_genes_summary.csv:md5,b20bf9df3ba0d7d82510fa632f5531df", + "top_stable_genes_summary.csv:md5,3f6ad9a7f2f184b7a4a4651935b1da42", + "top_stable_genes_transposed_counts_filtered.csv:md5,b9e7e88055bd0ff844bce2787c795683", + "stats_all_genes.csv:md5,0262d1ed114708648080d917cf6d735d", + "rnaseq.stats_all_genes.csv:md5,f898b1e455ab27d1c4b3642ec0680b98", + "stats_with_scores.csv:md5,b2e3634ccae3e031d32feff0bd3dfd1f", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_counts.parquet:md5,4b678f7f93296b65e35d80ce3fcdcd15", - "all_genes_summary.csv:md5,c382ff75abdd1b206e047edb83699708", + "all_genes_summary.csv:md5,b20bf9df3ba0d7d82510fa632f5531df", "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", - "common.cpython-313.pyc:md5,37cbf2a5933ace1aa714e6c49682459b", - "genes.cpython-313.pyc:md5,668854f409530bac16d2d5bf4dc781c9", - "samples.cpython-313.pyc:md5,ae5890963055f92e70b4995397080273", "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", - "graphs.cpython-313.pyc:md5,4d0a925cf3fb3f100d40e628302563a5", - "right_sidebar.cpython-313.pyc:md5,3558e524493a80dbbeb4159e4440acf4", - "stores.cpython-313.pyc:md5,0969dd13170cc9864913a33b07eabac7", - "tables.cpython-313.pyc:md5,5397d489be50816b7c943fb22dd178ec", - "tooltips.cpython-313.pyc:md5,7fa81ed404b4707a122f63ec59cf3f5c", - "top.cpython-313.pyc:md5,dc919308bd181abb3268b25a41de18fe", "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", - "genes.cpython-313.pyc:md5,30f74fb5a6c03914481e3c18614aadb6", - "samples.cpython-313.pyc:md5,14c18df8caff8c7715f8e7eb759cd359", "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", "tables.py:md5,84d17ad7f43458412e550f74a24560e5", "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.cpython-313.pyc:md5,a5f1a30f13a693977de20b1946038832", - "data_management.cpython-313.pyc:md5,5fef556524e3dc0dbb2b76e8ca9c5776", - "style.cpython-313.pyc:md5,8bc8df7c03346e65470a217d6989dafb", "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", "style.py:md5,b9f1207f06464e43c05e6e3912f12731", "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "E_MTAB_8187_rnaseq.dataset_stats.csv:md5,2f8f45b238e4bfa187765b72b0215447", + "E_MTAB_8187_rnaseq.dataset_stats.csv:md5,cfe732af026c75716331b4c043f0828c", + "normalisation_failure_reasons.tsv:md5,365a7f147247027d6346a95d80de4e9e", "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", - "candidate_counts.parquet:md5,03846e729c0b88802ff3e514bdab0f2f", + "accessions.txt:md5,63a651d9df354aef24400cebe56dd5ec", + "geo_all_datasets.metadata.tsv:md5,0df782fb63ecb75da068d780c46e5118", + "geo_rejected_datasets.metadata.tsv:md5,915b3e94343e1ade9247125cbab5ce9c", + "geo_selected_datasets.metadata.tsv:md5,9daef6dbdb514239685095bae4fd8adc", + "warning_reason.txt:md5,a5d754439419aabc9cf3ea347a9ee238", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.mapping.csv:md5,d95581c2b10beed3ba4a4556c1afc44f", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.metadata.csv:md5,faf9789296e5578fd31efee8d0c40ca9", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv:md5,f8889bf65500b67c37b7b7549df57285", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv:md5,92d1acd8e9d5e5c8f44b25e1ffce66c2", + "beta_vulgaris.rnaseq.raw.counts.mapping.csv:md5,df61b884eaf7b2734361d4d262de4833", + "beta_vulgaris.rnaseq.raw.counts.metadata.csv:md5,a9eaec4261ce39f4ab85684eb03c1581", + "beta_vulgaris.rnaseq.raw.counts.renamed.csv:md5,6da8c317c6dafccbb869be5074f597b2", "whole_gene_id_mapping.csv:md5,d95581c2b10beed3ba4a4556c1afc44f", "whole_gene_metadata.csv:md5,faf9789296e5578fd31efee8d0c40ca9", - "all_counts.parquet:md5,4b678f7f93296b65e35d80ce3fcdcd15", - "all_counts.parquet:md5,4b678f7f93296b65e35d80ce3fcdcd15", "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", - "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", - "multiqc_eatlas_all_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", - "multiqc_eatlas_selected_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", - "multiqc_expression_distributions_top_stable_genes.txt:md5,8de2cb4a0136a973122e3cb4fab52b23", - "multiqc_gene_statistics.txt:md5,143120a0f58cf66cbb444039b3b1b4fc", - "multiqc_ranked_top_stable_genes_summary.txt:md5,d4ef7039f60f434c109e95240499c049", - "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,285ad46e7f5da70041815ecbcd9b5601", - "stability_values.normfinder.csv:md5,0fa8281f140b1c2e727d6b34c2f94245", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,e815eb3150d1c97e890142fadd16219e" + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,e8dfabbc566f7c096c1a850084d4768b", + "failure_reason.txt:md5,7f26a85e66f925b9718d543c01e06c51", + "stability_values.normfinder.csv:md5,64082412b0d0c0903b3677ebfc4131af", + "geo_warning_reasons.csv:md5,7bf9cf895848e0233e69682ed50b61f4" ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-13T11:26:48.449442025" + "timestamp": "2025-11-19T14:52:40.766786924" }, - "-profile test": { + "-profile test_accessions_only": { + "content": [ + null, + [ + "errors", + "expression_atlas", + "expression_atlas/accessions", + "expression_atlas/accessions/accessions.txt", + "expression_atlas/accessions/selected_experiments.metadata.tsv", + "expression_atlas/accessions/species_experiments.metadata.tsv", + "geo", + "geo/accessions", + "geo/accessions/accessions.txt", + "geo/accessions/geo_all_datasets.metadata.tsv", + "geo/accessions/geo_rejected_datasets.metadata.tsv", + "geo/accessions/geo_selected_datasets.metadata.tsv", + "multiqc", + "multiqc/multiqc_data", + "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_eatlas_all_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_rejected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_selected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_software_versions.txt", + "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_plots", + "multiqc/multiqc_plots/pdf", + "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_rejected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_selected_experiments_metadata.pdf", + "multiqc/multiqc_plots/png", + "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", + "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_rejected_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_selected_experiments_metadata.png", + "multiqc/multiqc_plots/svg", + "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_rejected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_selected_experiments_metadata.svg", + "multiqc/multiqc_report.html", + "multiqc/versions.yml", + "pipeline_info", + "pipeline_info/software_mqc_versions.yml", + "warnings" + ], + [ + "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", + "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "accessions.txt:md5,a850f625a78be7b4b10ce08a5b638e23", + "geo_all_datasets.metadata.tsv:md5,965d93f4a53e07618e37eee8c4a7045a", + "geo_rejected_datasets.metadata.tsv:md5,bc079b56981e9f34e21bbc352603a6a9", + "geo_selected_datasets.metadata.tsv:md5,5adb3566a275e73dfefdb97288cb07a3" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-19T15:10:02.908557156" + }, + "-profile test_one_accession_low_gene_count": { "content": [ null, [ @@ -540,28 +793,14 @@ "dash_app/spec-file.txt", "dash_app/src", "dash_app/src/callbacks", - "dash_app/src/callbacks/__pycache__", - "dash_app/src/callbacks/__pycache__/common.cpython-313.pyc", - "dash_app/src/callbacks/__pycache__/genes.cpython-313.pyc", - "dash_app/src/callbacks/__pycache__/samples.cpython-313.pyc", "dash_app/src/callbacks/common.py", "dash_app/src/callbacks/genes.py", "dash_app/src/callbacks/samples.py", "dash_app/src/components", - "dash_app/src/components/__pycache__", - "dash_app/src/components/__pycache__/graphs.cpython-313.pyc", - "dash_app/src/components/__pycache__/right_sidebar.cpython-313.pyc", - "dash_app/src/components/__pycache__/stores.cpython-313.pyc", - "dash_app/src/components/__pycache__/tables.cpython-313.pyc", - "dash_app/src/components/__pycache__/tooltips.cpython-313.pyc", - "dash_app/src/components/__pycache__/top.cpython-313.pyc", "dash_app/src/components/graphs.py", "dash_app/src/components/icons.py", "dash_app/src/components/right_sidebar.py", "dash_app/src/components/settings", - "dash_app/src/components/settings/__pycache__", - "dash_app/src/components/settings/__pycache__/genes.cpython-313.pyc", - "dash_app/src/components/settings/__pycache__/samples.cpython-313.pyc", "dash_app/src/components/settings/genes.py", "dash_app/src/components/settings/samples.py", "dash_app/src/components/stores.py", @@ -569,36 +808,25 @@ "dash_app/src/components/tooltips.py", "dash_app/src/components/top.py", "dash_app/src/utils", - "dash_app/src/utils/__pycache__", - "dash_app/src/utils/__pycache__/config.cpython-313.pyc", - "dash_app/src/utils/__pycache__/data_management.cpython-313.pyc", - "dash_app/src/utils/__pycache__/style.cpython-313.pyc", "dash_app/src/utils/config.py", "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", "dash_app/versions.yml", "dataset_statistics", - "dataset_statistics/E_MTAB_8187_rnaseq.dataset_stats.csv", + "dataset_statistics/E_GEOD_51720_rnaseq.dataset_stats.csv", "errors", "expression_atlas", - "expression_atlas/accessions", - "expression_atlas/accessions/accessions.txt", - "expression_atlas/accessions/selected_experiments.metadata.tsv", - "expression_atlas/accessions/species_experiments.metadata.tsv", "expression_atlas/datasets", - "expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", - "expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", + "expression_atlas/datasets/E_GEOD_51720_rnaseq.design.csv", + "expression_atlas/datasets/E_GEOD_51720_rnaseq.rnaseq.raw.counts.csv", "geo", - "geo/accessions", - "geo/accessions/accessions.txt", - "geo/accessions/geo_all_datasets.metadata.tsv", - "geo/accessions/geo_rejected_datasets.metadata.tsv", + "geo/excluded_geo_accessions.txt", "get_candidate_genes", "get_candidate_genes/candidate_counts.parquet", "idmapping", - "idmapping/E_MTAB_8187_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/E_MTAB_8187_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/E_GEOD_51720_rnaseq.rnaseq.raw.counts.mapping.csv", + "idmapping/E_GEOD_51720_rnaseq.rnaseq.raw.counts.metadata.csv", + "idmapping/E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", "merge_all_counts", @@ -614,140 +842,89 @@ "multiqc/multiqc_data/multiqc.parquet", "multiqc/multiqc_data/multiqc_citations.txt", "multiqc/multiqc_data/multiqc_data.json", - "multiqc/multiqc_data/multiqc_eatlas_all_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", "multiqc/multiqc_data/multiqc_gene_statistics.txt", - "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_rejected_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", "multiqc/multiqc_plots", "multiqc/multiqc_plots/pdf", - "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", "multiqc/multiqc_plots/pdf/gene_statistics.pdf", - "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_rejected_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", "multiqc/multiqc_plots/png", - "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", - "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", "multiqc/multiqc_plots/png/gene_statistics.png", - "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_rejected_experiments_metadata.png", "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", "multiqc/multiqc_plots/svg", - "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", "multiqc/multiqc_plots/svg/gene_statistics.svg", - "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_rejected_experiments_metadata.svg", "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", "normalised", - "normalised/E_MTAB_8187_rnaseq", - "normalised/E_MTAB_8187_rnaseq/normalisation_deseq2", - "normalised/E_MTAB_8187_rnaseq/normalisation_deseq2/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normalised/E_GEOD_51720_rnaseq", + "normalised/E_GEOD_51720_rnaseq/normalisation_deseq2", + "normalised/E_GEOD_51720_rnaseq/normalisation_deseq2/E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", "normfinder", "normfinder/stability_values.normfinder.csv", "pipeline_info", "pipeline_info/software_mqc_versions.yml", "quantile_normalised", - "quantile_normalised/E_MTAB_8187_rnaseq", - "quantile_normalised/E_MTAB_8187_rnaseq/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", + "quantile_normalised/E_GEOD_51720_rnaseq", + "quantile_normalised/E_GEOD_51720_rnaseq/E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", "warnings" ], [ - "all_counts_filtered.parquet:md5,a8813b9cea1043b0cce3f7cfafba6d68", - "all_genes_summary.csv:md5,f0b57b4b71c51e81496892aa8cca7318", - "top_stable_genes_summary.csv:md5,bffda029a93b8471e6a1e5649c24bd74", - "top_stable_genes_transposed_counts_filtered.csv:md5,a85d810bb7e488b13216d1d1a70a2eff", - "cleaned_counts_filtered.parquet:md5,da5555270311efc0959d78c76de0bd4c", - "stats_all_genes.csv:md5,69ab95beb2e773179757dda3cc853e49", - "rnaseq.stats_all_genes.csv:md5,93e06aabacf03cbdce5bff6ef86e308e", - "stats_with_scores.csv:md5,b0d81e8d7a62b37296267a05677fb9b3", + "all_genes_summary.csv:md5,587f5144b7fa820b3754d88db9e399ce", + "top_stable_genes_summary.csv:md5,8b8d522a43d97191ed2a912d23414351", + "top_stable_genes_transposed_counts_filtered.csv:md5,0b4f76835a28815c0f4c681c8ac95cb3", + "stats_all_genes.csv:md5,9f7b7b34c3d088eb1edd33354ed11649", + "rnaseq.stats_all_genes.csv:md5,60b4a078fc13f84b0c39aa47e7158471", + "stats_with_scores.csv:md5,144fb6bd3fadebce473e7ae9adb47baf", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_counts.parquet:md5,5e78e46c71ffd1be40f69d1bb7c02185", - "all_genes_summary.csv:md5,f0b57b4b71c51e81496892aa8cca7318", - "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", + "all_genes_summary.csv:md5,587f5144b7fa820b3754d88db9e399ce", + "whole_design.csv:md5,d3aa542c4ad07d0051a84482fe6cd81c", "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", - "common.cpython-313.pyc:md5,5e5ccd935515ba3c0f90079f24bc5d53", - "genes.cpython-313.pyc:md5,18a991b02ba16b4be45e1125b1d991b6", - "samples.cpython-313.pyc:md5,824af1d55406e148cb4ee2957681d8a5", "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", - "graphs.cpython-313.pyc:md5,c8b4d9bee8ca889b9ac85fccc7d0d613", - "right_sidebar.cpython-313.pyc:md5,a33f4fcdf93b205813c7a65276e712f6", - "stores.cpython-313.pyc:md5,dc49fe39a05272df7a1d05b4d740c1cf", - "tables.cpython-313.pyc:md5,866b8529dd92c2386d584f702a03bb6e", - "tooltips.cpython-313.pyc:md5,839576f99f0094b731e9f4d65b80077c", - "top.cpython-313.pyc:md5,084cbfbd74000494104b7c723e3621c4", "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", - "genes.cpython-313.pyc:md5,341ba19409ceea3adaf85d079f6d6055", - "samples.cpython-313.pyc:md5,f2cfc03f08dbf3338d7b5db448d417c1", "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", "tables.py:md5,84d17ad7f43458412e550f74a24560e5", "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.cpython-313.pyc:md5,0bf517c900fc7f22b8b294f93bc22d50", - "data_management.cpython-313.pyc:md5,16bd285c10baaa3a8b63bfed24cf7af5", - "style.cpython-313.pyc:md5,54800903072fb7f9881e47edab7315b2", "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", "style.py:md5,b9f1207f06464e43c05e6e3912f12731", "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "E_MTAB_8187_rnaseq.dataset_stats.csv:md5,544cd73d8d9d34f668b3f5891853ffb2", - "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", - "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", - "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", - "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", - "accessions.txt:md5,d41d8cd98f00b204e9800998ecf8427e", - "geo_all_datasets.metadata.tsv:md5,773c2ef1b8f15c6a4624a54c89edca54", - "geo_rejected_datasets.metadata.tsv:md5,7e3bbb01196cb034ab2a201083e20e0b", - "candidate_counts.parquet:md5,a70d38ba1fd86533c01e79a0a37a56a4", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.mapping.csv:md5,d95581c2b10beed3ba4a4556c1afc44f", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.metadata.csv:md5,faf9789296e5578fd31efee8d0c40ca9", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv:md5,f8889bf65500b67c37b7b7549df57285", - "whole_gene_id_mapping.csv:md5,d95581c2b10beed3ba4a4556c1afc44f", - "whole_gene_metadata.csv:md5,faf9789296e5578fd31efee8d0c40ca9", - "all_counts.parquet:md5,5e78e46c71ffd1be40f69d1bb7c02185", - "all_counts.parquet:md5,5e78e46c71ffd1be40f69d1bb7c02185", - "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", - "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", - "multiqc_eatlas_all_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", - "multiqc_eatlas_selected_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", - "multiqc_expression_distributions_top_stable_genes.txt:md5,98dea2ab83b991a464dac4d6646ed9dc", - "multiqc_gene_statistics.txt:md5,5549573adfaa235f23b6246bcf28a15c", - "multiqc_geo_all_experiments_metadata.txt:md5,69462c654446efee39fa72978065fb53", - "multiqc_geo_rejected_experiments_metadata.txt:md5,320da3c2a8ae094956e30d56c4312bea", - "multiqc_ranked_top_stable_genes_summary.txt:md5,710a41b54b281337c2a87627a6c511a8", - "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,285ad46e7f5da70041815ecbcd9b5601", - "stability_values.normfinder.csv:md5,a256a7f4aaa9ad77456e1e7a6ee65f0c", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,85cfb9f6a28841e4299c57dcb3b78960" + "E_GEOD_51720_rnaseq.dataset_stats.csv:md5,db6a65d30bd7cf403d6d8da054fad8f3", + "E_GEOD_51720_rnaseq.design.csv:md5,80805afb29837b6fbb73a6aa6f3a461b", + "E_GEOD_51720_rnaseq.rnaseq.raw.counts.csv:md5,07cd448196fc2fea4663bd9705da2b98", + "excluded_geo_accessions.txt:md5,cdbec776e8b1d9dc7d0aa44aaf52aa50", + "E_GEOD_51720_rnaseq.rnaseq.raw.counts.mapping.csv:md5,1bbc8cddf18072309e81498806aa73ff", + "E_GEOD_51720_rnaseq.rnaseq.raw.counts.metadata.csv:md5,08861c29159a6a2fed38efe523ed9c56", + "E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.csv:md5,a8ca7cef22b4c6662cbb23cc56510ef7", + "whole_gene_id_mapping.csv:md5,1bbc8cddf18072309e81498806aa73ff", + "whole_gene_metadata.csv:md5,a08ab44a98542a8529d2a4b5a47e4408", + "whole_design.csv:md5,d3aa542c4ad07d0051a84482fe6cd81c", + "E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,5fe6d949da70f2adb7cee8da38d3adc7", + "stability_values.normfinder.csv:md5,8f161901b6e184e90cbd1a7fc96480e2" ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-13T11:20:32.249161175" + "timestamp": "2025-11-19T15:13:20.579340051" }, - "-profile test_accessions_only": { + "-profile test_no_dataset_found": { "content": [ null, [ @@ -755,13 +932,12 @@ "expression_atlas", "expression_atlas/accessions", "expression_atlas/accessions/accessions.txt", - "expression_atlas/accessions/selected_experiments.metadata.tsv", "expression_atlas/accessions/species_experiments.metadata.tsv", "geo", "geo/accessions", "geo/accessions/accessions.txt", - "geo/accessions/geo_all_datasets.metadata.tsv", - "geo/accessions/geo_selected_datasets.metadata.tsv", + "idmapping", + "merged_datasets", "multiqc", "multiqc/multiqc_data", "multiqc/multiqc_data/llms-full.txt", @@ -769,28 +945,8 @@ "multiqc/multiqc_data/multiqc.parquet", "multiqc/multiqc_data/multiqc_citations.txt", "multiqc/multiqc_data/multiqc_data.json", - "multiqc/multiqc_data/multiqc_eatlas_all_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_selected_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", - "multiqc/multiqc_plots", - "multiqc/multiqc_plots/pdf", - "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_selected_experiments_metadata.pdf", - "multiqc/multiqc_plots/png", - "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", - "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_selected_experiments_metadata.png", - "multiqc/multiqc_plots/svg", - "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_selected_experiments_metadata.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", "pipeline_info", @@ -798,27 +954,18 @@ "warnings" ], [ - "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", - "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", - "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", - "accessions.txt:md5,530c21fd138405dc6bcdd275633c07ef", - "geo_all_datasets.metadata.tsv:md5,843b27695ec965265a4926e5de656112", - "geo_selected_datasets.metadata.tsv:md5,843b27695ec965265a4926e5de656112", - "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", - "multiqc_eatlas_all_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", - "multiqc_eatlas_selected_experiments_metadata.txt:md5,8b7643e0ef8eaaa3fa72f7103fd7ccee", - "multiqc_geo_all_experiments_metadata.txt:md5,e7581df8fc257738d299989bc8257978", - "multiqc_geo_selected_experiments_metadata.txt:md5,e7581df8fc257738d299989bc8257978", - "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030" + "accessions.txt:md5,d41d8cd98f00b204e9800998ecf8427e", + "species_experiments.metadata.tsv:md5,68b329da9893e34099c7d8ad5cb9c940", + "accessions.txt:md5,d41d8cd98f00b204e9800998ecf8427e" ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-13T11:21:09.701097728" + "timestamp": "2025-11-19T15:17:16.594642518" }, - "-profile test_no_dataset_found": { + "-profile test_download_only": { "content": [ null, [ @@ -826,12 +973,21 @@ "expression_atlas", "expression_atlas/accessions", "expression_atlas/accessions/accessions.txt", + "expression_atlas/accessions/selected_experiments.metadata.tsv", "expression_atlas/accessions/species_experiments.metadata.tsv", + "expression_atlas/datasets", + "expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", + "expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", "geo", "geo/accessions", "geo/accessions/accessions.txt", - "idmapping", - "merged_datasets", + "geo/accessions/geo_all_datasets.metadata.tsv", + "geo/accessions/geo_rejected_datasets.metadata.tsv", + "geo/accessions/geo_selected_datasets.metadata.tsv", + "geo/datasets", + "geo/datasets/GSE55951_GPL18429.microarray.normalised.counts.csv", + "geo/datasets/GSE55951_GPL18429.microarray.normalised.design.csv", + "geo/datasets/warning_reason.txt", "multiqc", "multiqc/multiqc_data", "multiqc/multiqc_data/llms-full.txt", @@ -839,27 +995,64 @@ "multiqc/multiqc_data/multiqc.parquet", "multiqc/multiqc_data/multiqc_citations.txt", "multiqc/multiqc_data/multiqc_data.json", + "multiqc/multiqc_data/multiqc_eatlas_all_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_rejected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_selected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_warning_reasons.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_plots", + "multiqc/multiqc_plots/pdf", + "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_rejected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_selected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_warning_reasons.pdf", + "multiqc/multiqc_plots/png", + "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", + "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_rejected_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_selected_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_warning_reasons.png", + "multiqc/multiqc_plots/svg", + "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_rejected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_selected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_warning_reasons.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", "pipeline_info", "pipeline_info/software_mqc_versions.yml", - "warnings" + "warnings", + "warnings/geo_warning_reasons.csv" ], [ - "accessions.txt:md5,d41d8cd98f00b204e9800998ecf8427e", - "species_experiments.metadata.tsv:md5,68b329da9893e34099c7d8ad5cb9c940", - "accessions.txt:md5,d41d8cd98f00b204e9800998ecf8427e", - "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", - "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030" + "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", + "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", + "accessions.txt:md5,a850f625a78be7b4b10ce08a5b638e23", + "geo_all_datasets.metadata.tsv:md5,965d93f4a53e07618e37eee8c4a7045a", + "geo_rejected_datasets.metadata.tsv:md5,bc079b56981e9f34e21bbc352603a6a9", + "geo_selected_datasets.metadata.tsv:md5,5adb3566a275e73dfefdb97288cb07a3", + "GSE55951_GPL18429.microarray.normalised.counts.csv:md5,18fd2d728ad2ec5cb78f994f73375144", + "GSE55951_GPL18429.microarray.normalised.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d", + "warning_reason.txt:md5,a5d754439419aabc9cf3ea347a9ee238", + "geo_warning_reasons.csv:md5,4c26f6aa5c37055fe76b7a58c065fa30" ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-12T15:54:15.001920938" + "timestamp": "2025-11-19T15:10:51.223721623" }, "-profile test_full": { "content": [ @@ -1047,28 +1240,14 @@ "dash_app/spec-file.txt", "dash_app/src", "dash_app/src/callbacks", - "dash_app/src/callbacks/__pycache__", - "dash_app/src/callbacks/__pycache__/common.cpython-313.pyc", - "dash_app/src/callbacks/__pycache__/genes.cpython-313.pyc", - "dash_app/src/callbacks/__pycache__/samples.cpython-313.pyc", "dash_app/src/callbacks/common.py", "dash_app/src/callbacks/genes.py", "dash_app/src/callbacks/samples.py", "dash_app/src/components", - "dash_app/src/components/__pycache__", - "dash_app/src/components/__pycache__/graphs.cpython-313.pyc", - "dash_app/src/components/__pycache__/right_sidebar.cpython-313.pyc", - "dash_app/src/components/__pycache__/stores.cpython-313.pyc", - "dash_app/src/components/__pycache__/tables.cpython-313.pyc", - "dash_app/src/components/__pycache__/tooltips.cpython-313.pyc", - "dash_app/src/components/__pycache__/top.cpython-313.pyc", "dash_app/src/components/graphs.py", "dash_app/src/components/icons.py", "dash_app/src/components/right_sidebar.py", "dash_app/src/components/settings", - "dash_app/src/components/settings/__pycache__", - "dash_app/src/components/settings/__pycache__/genes.cpython-313.pyc", - "dash_app/src/components/settings/__pycache__/samples.cpython-313.pyc", "dash_app/src/components/settings/genes.py", "dash_app/src/components/settings/samples.py", "dash_app/src/components/stores.py", @@ -1076,10 +1255,6 @@ "dash_app/src/components/tooltips.py", "dash_app/src/components/top.py", "dash_app/src/utils", - "dash_app/src/utils/__pycache__", - "dash_app/src/utils/__pycache__/config.cpython-313.pyc", - "dash_app/src/utils/__pycache__/data_management.cpython-313.pyc", - "dash_app/src/utils/__pycache__/style.cpython-313.pyc", "dash_app/src/utils/config.py", "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", @@ -1087,7 +1262,6 @@ "dataset_statistics", "dataset_statistics/E_MTAB_5072_rnaseq.dataset_stats.csv", "errors", - "errors/geo_failure_reasons.csv", "expression_atlas", "expression_atlas/accessions", "expression_atlas/accessions/accessions.txt", @@ -1257,7 +1431,7 @@ "geo/accessions/geo_rejected_datasets.metadata.tsv", "geo/accessions/geo_selected_datasets.metadata.tsv", "geo/datasets", - "geo/datasets/failure_reason.txt", + "geo/datasets/warning_reason.txt", "get_candidate_genes", "get_candidate_genes/candidate_counts.parquet", "idmapping", @@ -1302,9 +1476,9 @@ "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", "multiqc/multiqc_data/multiqc_gene_statistics.txt", "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_failure_reasons.txt", "multiqc/multiqc_data/multiqc_geo_rejected_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_geo_selected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_warning_reasons.txt", "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", @@ -1315,9 +1489,9 @@ "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", "multiqc/multiqc_plots/pdf/gene_statistics.pdf", "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_failure_reasons.pdf", "multiqc/multiqc_plots/pdf/geo_rejected_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/geo_selected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_warning_reasons.pdf", "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", "multiqc/multiqc_plots/png", "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", @@ -1325,9 +1499,9 @@ "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", "multiqc/multiqc_plots/png/gene_statistics.png", "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_failure_reasons.png", "multiqc/multiqc_plots/png/geo_rejected_experiments_metadata.png", "multiqc/multiqc_plots/png/geo_selected_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_warning_reasons.png", "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", "multiqc/multiqc_plots/svg", "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", @@ -1335,9 +1509,9 @@ "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", "multiqc/multiqc_plots/svg/gene_statistics.svg", "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_failure_reasons.svg", "multiqc/multiqc_plots/svg/geo_rejected_experiments_metadata.svg", "multiqc/multiqc_plots/svg/geo_selected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_warning_reasons.svg", "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", @@ -1506,572 +1680,219 @@ "ratio_standard_variation/std.8.8.parquet", "ratio_standard_variation/std.8.9.parquet", "ratio_standard_variation/std.9.9.parquet", - "warnings" + "warnings", + "warnings/geo_warning_reasons.csv" ], [ - "all_counts_filtered.parquet:md5,109de37753cdd7012e041a89ebd2b8a3", - "all_genes_summary.csv:md5,8ff2dbe05616a18a2f2c5c33c249ec4b", - "top_stable_genes_summary.csv:md5,1bf3eb0779e2db255e26c1397e3933b6", - "top_stable_genes_transposed_counts_filtered.csv:md5,be1270a3c44d0dca96ff6461686e3aca", - "cleaned_counts_filtered.parquet:md5,01f8763f2ccd1c35b8a36236a5c6998a", - "stats_all_genes.csv:md5,cc94561057b4ce572aa676d10d0b661f", - "rnaseq.stats_all_genes.csv:md5,7530f9d42a6bc91d154f6156255fe273", - "m_measures.csv:md5,bd71bb4975077b9b8190d1335b4c9f4c", - "stats_with_scores.csv:md5,3e6a0b164106f0045189e7c6475b4dc3", - "cross_join.0.0.parquet:md5,5fd6a1dae67dc1df624139d484dbcb29", - "cross_join.0.1.parquet:md5,1e3a29760a38effb552cd0ac63e5a3c6", - "cross_join.0.10.parquet:md5,6d4334ebecedc7b72de46049826f8b51", - "cross_join.0.11.parquet:md5,bd1bcd03ed0a9e3e7e30a1082f627739", - "cross_join.0.12.parquet:md5,fb2b0410d4b8f3c230ac64842f60f3b7", - "cross_join.0.13.parquet:md5,c23e71879e6e505c1782369818d7b8f4", - "cross_join.0.14.parquet:md5,26afcbb9299dd8cb099d0a7771769e2e", - "cross_join.0.15.parquet:md5,3f7b2f991a2ffe19d9882ab892c4b645", - "cross_join.0.16.parquet:md5,ae4862e830a6dae33c3b19846ff80305", - "cross_join.0.2.parquet:md5,fb3c22ebd3979e6ec06e8428daaf14d7", - "cross_join.0.3.parquet:md5,0c851dad5b72e2ba48cb52afc0aacc23", - "cross_join.0.4.parquet:md5,75d537dd86c150824877aa5d7b6de912", - "cross_join.0.5.parquet:md5,8bdc78157b03d927545953c1d4b79f8d", - "cross_join.0.6.parquet:md5,f844f69a06953a5220617d8f0392ad74", - "cross_join.0.7.parquet:md5,732b1d409adb280dc32f56f5f1bd9c72", - "cross_join.0.8.parquet:md5,b92fc809b64b9bcae873945646bc178c", - "cross_join.0.9.parquet:md5,b786fdf8284e88d07d37344d347a858d", - "cross_join.1.1.parquet:md5,d9437c640e4e7f3b91364355f2e9c078", - "cross_join.1.10.parquet:md5,74aec5724dfad2b1ae7fe36e39de2d95", - "cross_join.1.11.parquet:md5,8af11e225a102b21f97a27bc9478618e", - "cross_join.1.12.parquet:md5,933bf8c423028a1b5c94de19db36052c", - "cross_join.1.13.parquet:md5,b0398692508b3f5adb564045f323ffbf", - "cross_join.1.14.parquet:md5,2de06195ef078f3ddabcb5c76bc7eae4", - "cross_join.1.15.parquet:md5,19305e8939f5876d999406f5e5623617", - "cross_join.1.16.parquet:md5,c1d6196e5b71d39f6ff70ab1b52802b3", - "cross_join.1.2.parquet:md5,efe4b4719b8ffc88eb51f042b268e2fb", - "cross_join.1.3.parquet:md5,bb6f16dfeb5e0463df4ccbd99cda0d0d", - "cross_join.1.4.parquet:md5,40f9db742f31ef65723960ee0e2a2b96", - "cross_join.1.5.parquet:md5,59048c9c0ae4f4b158ad4559a559118a", - "cross_join.1.6.parquet:md5,5ea8cf5a06b2ed1f79d204eac22399ef", - "cross_join.1.7.parquet:md5,24a267230327e89947f96c1a7b4ea93a", - "cross_join.1.8.parquet:md5,9e9374269f92aceba9cdbe2865864c10", - "cross_join.1.9.parquet:md5,e1ca479630ba09943a7e9ee845a838e2", - "cross_join.10.10.parquet:md5,244b9a859ba819563709bccbefdff97c", - "cross_join.10.11.parquet:md5,f2f2c84c860fdef63d978559bb121117", - "cross_join.10.12.parquet:md5,9310659e5e8b18634b34be2f183f7cc7", - "cross_join.10.13.parquet:md5,5f159adfb0c6f73bb5e9b233dc46b44a", - "cross_join.10.14.parquet:md5,bb44792fa83792223cf8db601b2a411c", - "cross_join.10.15.parquet:md5,b70dc09265a65556ae8a59c7404eafb9", - "cross_join.10.16.parquet:md5,84a5d07a174f446e202f4b064a1daa2c", - "cross_join.10.2.parquet:md5,5a3a75f4f4005283fa8815829dc632e4", - "cross_join.10.3.parquet:md5,980ce729b70c57bfb624c07ebde14a81", - "cross_join.10.4.parquet:md5,3be21cb2fbe6b2844e8aace359ec8e3d", - "cross_join.10.5.parquet:md5,17c5cf6b75fc4d5f2bfab295a56b8085", - "cross_join.10.6.parquet:md5,b472a936ca54b0a82be87ee92f1ec7e4", - "cross_join.10.7.parquet:md5,f9b9ea716ec9289a564a15c96a128ad2", - "cross_join.10.8.parquet:md5,65ca99c0ddef5be894c09475d6100dd9", - "cross_join.10.9.parquet:md5,b215ac67666e6c4e7259f069be716f51", - "cross_join.11.11.parquet:md5,079c2f972444dd419dfa924bd4984659", - "cross_join.11.12.parquet:md5,fe3a0baa33d37a9ccc233604e11d26da", - "cross_join.11.13.parquet:md5,3aded985226e07fe1cded2fed6be487c", - "cross_join.11.14.parquet:md5,754682bc82ac1ea5d6e6247c8d8abe1f", - "cross_join.11.15.parquet:md5,b7da4a1bb1107fbbd7d1b19140006d5e", - "cross_join.11.16.parquet:md5,7dfc8b3edf266e90257b85745b0e61cd", - "cross_join.11.2.parquet:md5,627bc600ef080daa45c7f79604db8e05", - "cross_join.11.3.parquet:md5,2fef0c7260ee3a77ca42ac3e63bb2424", - "cross_join.11.4.parquet:md5,2ba0c698b1161ad5a86847457c0e1992", - "cross_join.11.5.parquet:md5,68e57305be79cf8689b4cce228a2e7f1", - "cross_join.11.6.parquet:md5,42a9af5c563be4a933838220c396e4f4", - "cross_join.11.7.parquet:md5,cc9365bc90fe45673939b8f8d0e1d005", - "cross_join.11.8.parquet:md5,4f88965590647667ade4447203f356fe", - "cross_join.11.9.parquet:md5,a37890fb4c0d25184e3e6079dbbeae72", - "cross_join.12.12.parquet:md5,f744a30ab26946b0ffdb72d13bc8093f", - "cross_join.12.13.parquet:md5,d28c083a30714cbf0d1b45905be12a13", - "cross_join.12.14.parquet:md5,c820d91e28e7cbbb1314366cc4f75c01", - "cross_join.12.15.parquet:md5,b303188b2b3d2db7211986bfe3479ec8", - "cross_join.12.16.parquet:md5,daa4c8b3cd99ed117214ccd6a692c929", - "cross_join.12.2.parquet:md5,05ed39294566841e28fec0f3e776f450", - "cross_join.12.3.parquet:md5,5d7e2c939025f99d47ab1db678a87ee4", - "cross_join.12.4.parquet:md5,0bdbe47587396df4939317edd8249627", - "cross_join.12.5.parquet:md5,5749855ae83d2b4611f03e3f5c4e8a2c", - "cross_join.12.6.parquet:md5,98a2af5989c08ae83a204bc7186511bd", - "cross_join.12.7.parquet:md5,320cfc607b848c0791af8897aee40a86", - "cross_join.12.8.parquet:md5,0b046e527f232b0c58d6ba9d68049612", - "cross_join.12.9.parquet:md5,314518452a6cb830bc62c818beb5f1e2", - "cross_join.13.13.parquet:md5,36c716cb99a71bb9d040641a38cc277a", - "cross_join.13.14.parquet:md5,9b67b5364ea9084e59be1f1f07df2b39", - "cross_join.13.15.parquet:md5,5a9d644d6f37041fd6abd662ef6456b4", - "cross_join.13.16.parquet:md5,01f98ffad1cd9d36c2c6b8a5ebd32a30", - "cross_join.13.2.parquet:md5,ae1d026c82b0963bf83ce57631eed942", - "cross_join.13.3.parquet:md5,5047ee3445752221569f11dbfc197dbb", - "cross_join.13.4.parquet:md5,cfe159929a6d5a0b96f9a259760fa83b", - "cross_join.13.5.parquet:md5,8d44bc6b15b77d6277674a70f4c5494a", - "cross_join.13.6.parquet:md5,41f7706137e4f93283e63a50bd7fe0aa", - "cross_join.13.7.parquet:md5,ccf2d2ff2018b503acf662ae17839f27", - "cross_join.13.8.parquet:md5,e685e8221d57a7c389eeca0284780aee", - "cross_join.13.9.parquet:md5,144b44780ab3fbc0c32551265e67ea26", - "cross_join.14.14.parquet:md5,cdef3ac358396bfb7daddeaecc8e84cc", - "cross_join.14.15.parquet:md5,97fa15167da8e59c6f10f1f7587ed001", - "cross_join.14.16.parquet:md5,a55f95c7799f97d6fb7d3ba1593133e2", - "cross_join.14.2.parquet:md5,9f98ce2c2e9133f7fe54ac2e6f86c40f", - "cross_join.14.3.parquet:md5,7c6f697d2d615b1ccad6dbb8a6076daa", - "cross_join.14.4.parquet:md5,14fb0e5cb5fe06dbe6b7413b361fbb38", - "cross_join.14.5.parquet:md5,5b7af7eea35e3544bb37502d28757d52", - "cross_join.14.6.parquet:md5,f17a77d6af3af65e96cc9da72345bc2f", - "cross_join.14.7.parquet:md5,5f75d38297db27106453ab22a9adbc7d", - "cross_join.14.8.parquet:md5,c4bf140b8931a7a82894d4fb77d213e7", - "cross_join.14.9.parquet:md5,2e48728c6fa8a2fc4aaad3437b72bb9e", - "cross_join.15.15.parquet:md5,54002d0fc65ba9058be2d02a9daf306d", - "cross_join.15.16.parquet:md5,92731cfb4e58ba8ebd6ba0f87c9ba197", - "cross_join.15.2.parquet:md5,3354fd9691f9800ac5305c1723a7d0cf", - "cross_join.15.3.parquet:md5,37e28eb3f534b7f829d7eb2f087b8f1e", - "cross_join.15.4.parquet:md5,990ff37df8f143ef0c396d2c80c860c7", - "cross_join.15.5.parquet:md5,1bf0c54938ef505fcd43c629d0f13482", - "cross_join.15.6.parquet:md5,7dce95e2bf73756b2b58940fe3bca2a0", - "cross_join.15.7.parquet:md5,121e69b7c5f109befb8be2fd8397c125", - "cross_join.15.8.parquet:md5,9edf68ebc4fd13cd34ce1a0200b7a315", - "cross_join.15.9.parquet:md5,d40c8478aabf1621563d833b9e951364", - "cross_join.16.16.parquet:md5,a5fd4eaf58cb25fd9594b551b78315cd", - "cross_join.16.2.parquet:md5,bb38f400a2dbfad05111109d988ac3ec", - "cross_join.16.3.parquet:md5,958c90bf70813e177a9944be4f3d7980", - "cross_join.16.4.parquet:md5,e328e62d33d857077d29e9b66a16a602", - "cross_join.16.5.parquet:md5,7411115d1d8f449277c74c29b629a85c", - "cross_join.16.6.parquet:md5,cd8c5eefd5e4657b052b19eb9a181cc3", - "cross_join.16.7.parquet:md5,1ea5acffe519a38503abfe315360b015", - "cross_join.16.8.parquet:md5,0f48df1531a2fc40a88863754085ebec", - "cross_join.16.9.parquet:md5,e51b6d69f4f4ed4c184fed64db7c96bc", - "cross_join.2.2.parquet:md5,5a2f6f34e343a0c2dd7ee8299613123d", - "cross_join.2.3.parquet:md5,33249d35df701286d1735e74d6a34a7f", - "cross_join.2.4.parquet:md5,a7a00b58e62f5df689192ccf116e3c2e", - "cross_join.2.5.parquet:md5,f4a48d2afe3835d5e93fd07cf79fc02e", - "cross_join.2.6.parquet:md5,f31f3e7779caf3f519901d6b59e300c2", - "cross_join.2.7.parquet:md5,7a1d070623158efbde1e1d9500f53122", - "cross_join.2.8.parquet:md5,68b53d56cae2587196101477d6af7a8a", - "cross_join.2.9.parquet:md5,371d8849ebeadf84e016b471ebe39072", - "cross_join.3.3.parquet:md5,e54c447f8cad67ab5fbec60afe141786", - "cross_join.3.4.parquet:md5,dccc10846689d571b0a6471ce6fb484b", - "cross_join.3.5.parquet:md5,0464d29f05811f9c89c3fd45f9ee84f9", - "cross_join.3.6.parquet:md5,34091d4a7071fd831a42d64ebe14366e", - "cross_join.3.7.parquet:md5,ae98d4458becb437cdf7cd2c6d2c8d1f", - "cross_join.3.8.parquet:md5,0c2df93a10eea8029c9ad3ce5b55dc87", - "cross_join.3.9.parquet:md5,d3467b182f799b7d6fbd013cff23f6c6", - "cross_join.4.4.parquet:md5,2fbf82e2656574416570999c43f5eeaf", - "cross_join.4.5.parquet:md5,037a32ae4149210cda8e5b37111a69e5", - "cross_join.4.6.parquet:md5,231a14970e60f5f1bc361fe53f5b4e92", - "cross_join.4.7.parquet:md5,741ea6a0b9d02a6b3df70ef680632768", - "cross_join.4.8.parquet:md5,a1290d1098fcef6a87d09b1dca5deee5", - "cross_join.4.9.parquet:md5,5853c4b70c9c4301c21d461f72b2da0a", - "cross_join.5.5.parquet:md5,69274e23cf6357379df8e76d0fa575a4", - "cross_join.5.6.parquet:md5,3aabbaebf07c2d3382200082fee8156e", - "cross_join.5.7.parquet:md5,7e7c46caa8a94fd784446f96338242f9", - "cross_join.5.8.parquet:md5,6a389f98d00e90931898a54913abc79e", - "cross_join.5.9.parquet:md5,0e76b1069fdea765f2bccead9d94c87a", - "cross_join.6.6.parquet:md5,6d5f0bd4ffdb44a10bf9d72c74c29508", - "cross_join.6.7.parquet:md5,cd5614f7bd2551097682d8fef890cf2b", - "cross_join.6.8.parquet:md5,9962e5a649db1316d38f453c3206a6b2", - "cross_join.6.9.parquet:md5,39255950a677e23efddb4b6398eb4dd5", - "cross_join.7.7.parquet:md5,a4e5bcb523c0691fdf843cdde2d99f0b", - "cross_join.7.8.parquet:md5,88e64a4ea5e65033b169d59c43578252", - "cross_join.7.9.parquet:md5,a7df442687e16cd69469506e97f1af51", - "cross_join.8.8.parquet:md5,3650b5a305f79b6be474f9a05d06251c", - "cross_join.8.9.parquet:md5,df394ce09a8d51b9c24c8ea9a42428f6", - "cross_join.9.9.parquet:md5,168734bb00e24074b026b7cd344a41dc", + "all_genes_summary.csv:md5,51c2b151c90dd8c8304a875ef35104c9", + "top_stable_genes_summary.csv:md5,2dc7de173ad97c73e6c09ca493fcf1fa", + "top_stable_genes_transposed_counts_filtered.csv:md5,8f0ef2e7b839e502d5a3f6cc20034ab9", + "stats_all_genes.csv:md5,4af99acc51cf1bacda835e2aab1f0dd1", + "rnaseq.stats_all_genes.csv:md5,1489c8b543026b17051b8a4921427c50", + "m_measures.csv:md5,7db246dda58cba17bd2fc2bb79c70e93", + "stats_with_scores.csv:md5,e3e798790f3a0070104314d3ee00ace6", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_counts.parquet:md5,6e1db3f9aaee4e528db8e539e381b03f", - "all_genes_summary.csv:md5,8ff2dbe05616a18a2f2c5c33c249ec4b", + "all_genes_summary.csv:md5,51c2b151c90dd8c8304a875ef35104c9", "whole_design.csv:md5,c9bfd7bc8ca365222e03e67eb24b9a76", "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", - "common.cpython-313.pyc:md5,a3a9dbb6cd73407ef698f8386abf5f35", - "genes.cpython-313.pyc:md5,4d76b9a7317be3976387020f0ed4dc80", - "samples.cpython-313.pyc:md5,55b1e0fd8ba4990140e9637411b28da8", "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", - "graphs.cpython-313.pyc:md5,2a728177aa7f0c02834fa04aa7232538", - "right_sidebar.cpython-313.pyc:md5,85d9de2b7a22d3f0e4185662851b73a9", - "stores.cpython-313.pyc:md5,18d95e17779b0a77c39d11b71c396509", - "tables.cpython-313.pyc:md5,e0feb495e3a3d1c952f621584dab542e", - "tooltips.cpython-313.pyc:md5,bcbcc17e7a822b5aedb8c31ba518a7ba", - "top.cpython-313.pyc:md5,e29ecc7ee5a1bc73bbb54dab659b7cd1", "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", - "genes.cpython-313.pyc:md5,fdff1778df115949010d1b05924d8b01", - "samples.cpython-313.pyc:md5,eee24288cc5d55f31bf2dc68db0203f9", "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", "tables.py:md5,84d17ad7f43458412e550f74a24560e5", "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.cpython-313.pyc:md5,e90479a881bee16b3ed350484b74409e", - "data_management.cpython-313.pyc:md5,d6568239ff2e87516c58ac2c7f92bbd0", - "style.cpython-313.pyc:md5,42b1d52f5750d54f694421409d6265f3", "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", "style.py:md5,b9f1207f06464e43c05e6e3912f12731", "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "E_MTAB_5072_rnaseq.dataset_stats.csv:md5,408b24a208933eba5f442580cbbf165b", - "geo_failure_reasons.csv:md5,a8b10197854e9ace4e90b21b3f2f1470", + "E_MTAB_5072_rnaseq.dataset_stats.csv:md5,e7e95cf0ac00cf2710321e9588f5f4ad", "accessions.txt:md5,561a967c16b2ef6c29fb643cd4002947", "selected_experiments.metadata.tsv:md5,385b4d7ed40829f1e764857e805d6dc4", "species_experiments.metadata.tsv:md5,385b4d7ed40829f1e764857e805d6dc4", "E_MTAB_5072_rnaseq.design.csv:md5,a1f33d126dde387a2d542381c44bc1f3", "E_MTAB_5072_rnaseq.rnaseq.raw.counts.csv:md5,c41c84899a380515d759b99eeccfe43e", - "ratios.0.0.parquet:md5,405cd590509b6ffdd110f912222159d2", - "ratios.0.1.parquet:md5,422000f89ed0abedbd7f8b4017161535", - "ratios.0.10.parquet:md5,bb316fa0b9865dfffe747024443c059f", - "ratios.0.11.parquet:md5,eb36d82ada1ada899f80330dbd9a9e5a", - "ratios.0.12.parquet:md5,76dc7a3e2bd38c4f9d0c6fb5dbd6c3ac", - "ratios.0.13.parquet:md5,550e521560552ec799d0c09b10678890", - "ratios.0.14.parquet:md5,1145da4038b87582acddec6406467fa1", - "ratios.0.15.parquet:md5,b4d59d365a6bd5e8ef1a5d8f01cb7aac", - "ratios.0.16.parquet:md5,a16d505a5c022e2c5b3bfeb056f42155", - "ratios.0.2.parquet:md5,3bf28e74f74e119b2695adf3de551cc4", - "ratios.0.3.parquet:md5,cf55a5fdced77f76d42a1143a03124ef", - "ratios.0.4.parquet:md5,857bc2beaecfc6c1e0fec999ecc1476a", - "ratios.0.5.parquet:md5,cb07ee4affd907f3d16829e08954ae34", - "ratios.0.6.parquet:md5,ae53fe36ef593146b015fbdbb7be8bf6", - "ratios.0.7.parquet:md5,d47d8ba25f0edea0985da08ef3d601da", - "ratios.0.8.parquet:md5,17e24fec2f7229238a6f0dbc8705feaf", - "ratios.0.9.parquet:md5,f958751ed09623544d6a15620f876c63", - "ratios.1.1.parquet:md5,7b53e0104015ab8dc01c3d13527bbb85", - "ratios.1.10.parquet:md5,a3f4f8391668fd0723715c1ad7d5d521", - "ratios.1.11.parquet:md5,b2283c7a1c94f945d8faa9df83bb6fe4", - "ratios.1.12.parquet:md5,a5bcafad29f7a88f3098ca4678d97f8d", - "ratios.1.13.parquet:md5,f2945f24639146afddb43ef9b02cd05a", - "ratios.1.14.parquet:md5,3068d035a5d264683912e6398138a6f8", - "ratios.1.15.parquet:md5,fa3ed2c57e601c88b6dfcfd645734de9", - "ratios.1.16.parquet:md5,d99121ac26ec81b474e06971257a81f5", - "ratios.1.2.parquet:md5,ae7f696da83019ece1acc0aece6443f8", - "ratios.1.3.parquet:md5,23c18c27fa602fd6101769e69ce6ece3", - "ratios.1.4.parquet:md5,867e6b18f597fc1fe8e35ccc36704595", - "ratios.1.5.parquet:md5,5c41b2d632aa1d6fb2a3d2a8fd4148b4", - "ratios.1.6.parquet:md5,67e36407b266528bda7bfde5d847068f", - "ratios.1.7.parquet:md5,64a405bbcb23cf0e879ecb72cdc3b013", - "ratios.1.8.parquet:md5,604b3c093dc44b3e46bd62d7cc52008e", - "ratios.1.9.parquet:md5,4ca2b8858dbd711fa2474a7fc14a4b6d", - "ratios.10.10.parquet:md5,a2db1f83e81584fe1f64d8b117963556", - "ratios.10.11.parquet:md5,9295b90ade210e2bc3470172b920b233", - "ratios.10.12.parquet:md5,b7376d51719795d650611219656a90ef", - "ratios.10.13.parquet:md5,b4eefd83901c4776c8c131e3ff185676", - "ratios.10.14.parquet:md5,d7589a490b45710eb3ed74aaeadc18c3", - "ratios.10.15.parquet:md5,81257c2b9cca1a896a9a9ad411618bb8", - "ratios.10.16.parquet:md5,e6557f71aeb4bbbae16bc635aee3cf7e", - "ratios.10.2.parquet:md5,202cdf8eeb5b4fe6f18e2dfa787fa4ae", - "ratios.10.3.parquet:md5,f44e1a1a3cdd33459a73ff56d27ba7ea", - "ratios.10.4.parquet:md5,1903cb8be4ff1cc776a8871d3889be19", - "ratios.10.5.parquet:md5,dcd7414ba98689f8ef556c4c257e5814", - "ratios.10.6.parquet:md5,22927faa69b228fbc9cdb5bd8bd6c652", - "ratios.10.7.parquet:md5,8c28697499c50434e16e2d6a43c12f7f", - "ratios.10.8.parquet:md5,fbdc1fa2bd8b5a4cf1768c953dec4891", - "ratios.10.9.parquet:md5,deacdbf8e48988760d871a5a0be03369", - "ratios.11.11.parquet:md5,94424355bdeba61b40d2c93991bbaf6a", - "ratios.11.12.parquet:md5,401e83de0874ce1a69c07024a0478218", - "ratios.11.13.parquet:md5,c95029cc153845024e1696f95d39a6d4", - "ratios.11.14.parquet:md5,bc9403d30c0efe7bef7e07e256d2b619", - "ratios.11.15.parquet:md5,a6038b92df76732a18e12685719a9336", - "ratios.11.16.parquet:md5,39f64ad00730b6ca9d9addf366120072", - "ratios.11.2.parquet:md5,444d3372efbb7d09c57eab0a1af273a6", - "ratios.11.3.parquet:md5,89897da97b78cbf64e688c3d977d9f75", - "ratios.11.4.parquet:md5,ae9dcace2aac1ea83a4ef923c85a97c2", - "ratios.11.5.parquet:md5,228327a8ec587240f3ff83ce571042a7", - "ratios.11.6.parquet:md5,45319c62c09a476afbae5243b7f52918", - "ratios.11.7.parquet:md5,f96b8486134e5466566dc0e738ecb9af", - "ratios.11.8.parquet:md5,8f64d2be604280326fafa9346d033fa0", - "ratios.11.9.parquet:md5,74ea1c5ac78c4d5c0c41c7aca6b7d7f3", - "ratios.12.12.parquet:md5,d6d160877d1b0f8a0865a776056c03b4", - "ratios.12.13.parquet:md5,c68f4f035b589116fd530a7282742e0f", - "ratios.12.14.parquet:md5,1078ccca021b5913671380a732752758", - "ratios.12.15.parquet:md5,6e66e615298079ab166d357fcbd44400", - "ratios.12.16.parquet:md5,bccf17ed400e85224d3853977e496209", - "ratios.12.2.parquet:md5,0a8ff0fda5a4b3a922ccfdb61e3c1e42", - "ratios.12.3.parquet:md5,d3703721b93dd26570fd537ca9bb36c0", - "ratios.12.4.parquet:md5,fc587ea0c56d77e1dac6b2a9d0e21c19", - "ratios.12.5.parquet:md5,9407c12bc3fe995caf774570c887ca56", - "ratios.12.6.parquet:md5,c55f2b1eea563276b9a42ef64c776313", - "ratios.12.7.parquet:md5,f8e5256b1d613094b2563ea28e4ae0dd", - "ratios.12.8.parquet:md5,d0b8a24efb562be44ec3b6b54b4b7ad8", - "ratios.12.9.parquet:md5,7c164d3f8ccb7656f9b027223359efd0", - "ratios.13.13.parquet:md5,1930fee15bd7b60fdb3c207691ae639b", - "ratios.13.14.parquet:md5,804faff1979f837517410962b89ae85f", - "ratios.13.15.parquet:md5,091703e6b7133bf0fbcda1e6def53c80", - "ratios.13.16.parquet:md5,8a0a5e1492fbfa6c7defcc54f801dca9", - "ratios.13.2.parquet:md5,ec23f6ca9f0d726391b8022b747445de", - "ratios.13.3.parquet:md5,eb39f03a52eebf693f0821199d7b8f6c", - "ratios.13.4.parquet:md5,c8d4b2418e80fb9a4487b880682e420d", - "ratios.13.5.parquet:md5,2dea98b249322ec999616fbafa457dc6", - "ratios.13.6.parquet:md5,05567b323f94d300ed759bca7662a0ba", - "ratios.13.7.parquet:md5,e0a3a61f0ca707df4d9371459c510e97", - "ratios.13.8.parquet:md5,ccc815cdca00254863f1c6aecd13ab8c", - "ratios.13.9.parquet:md5,e8c513b87b20400d142ea2d259ade363", - "ratios.14.14.parquet:md5,ea8bf5c59d8db2ecc63aa4ad1692afb0", - "ratios.14.15.parquet:md5,ac53fc7f4eb59522bf96c72c3c4612ed", - "ratios.14.16.parquet:md5,2d84a0b748953a93a22dd892599afb11", - "ratios.14.2.parquet:md5,5c7e5d219506871719ff6a9234b31e93", - "ratios.14.3.parquet:md5,14ca78d4706ea4997d2a631b80b612a1", - "ratios.14.4.parquet:md5,5b424357d7976f48c8ea4893e91c75f2", - "ratios.14.5.parquet:md5,fb23c438af8f381cc809277084d9c0f7", - "ratios.14.6.parquet:md5,42a8d9d274bd7b31d9d8579ed9555835", - "ratios.14.7.parquet:md5,e177783b01f3b73ef582b248c6c7a343", - "ratios.14.8.parquet:md5,de9bce280597e9f2e426329d80754ffe", - "ratios.14.9.parquet:md5,33fd335e98b556fcb3542aea84084555", - "ratios.15.15.parquet:md5,3fc88687792ea38d7d30b9375d6a8f09", - "ratios.15.16.parquet:md5,a752a2fb3c2339c2520a53ea302fdce7", - "ratios.15.2.parquet:md5,18bc5c3429b6f1f41527baffe7215bfd", - "ratios.15.3.parquet:md5,addf16d3bdf7cef7db80ae163bd02242", - "ratios.15.4.parquet:md5,2381031d109a7f30e53893b7d6bc79ff", - "ratios.15.5.parquet:md5,e63862575d8d89a042c3e5e9476a7aa6", - "ratios.15.6.parquet:md5,4519fdb3b0c9f40d535540d0cab5134e", - "ratios.15.7.parquet:md5,533285de38fcb85a5665df522282d84f", - "ratios.15.8.parquet:md5,d7655554d0b75c16fbb1724629751a32", - "ratios.15.9.parquet:md5,228b9c95cd843efd3dfecf5f2fb6c2d4", - "ratios.16.16.parquet:md5,af6f4cf815fc94279fb8eca946300999", - "ratios.16.2.parquet:md5,67772b84ce8250d6da541374de7b3c0c", - "ratios.16.3.parquet:md5,40074f34554fd6867b8ea418e14e8bdd", - "ratios.16.4.parquet:md5,c35d20b1e405d0caebd1b052c27f40b7", - "ratios.16.5.parquet:md5,64a6139c4387507823a157ca7a633a7b", - "ratios.16.6.parquet:md5,af8c03de7516955f2e6b322b4e791342", - "ratios.16.7.parquet:md5,9fea140cb4bf24dfceeb3877ba79f713", - "ratios.16.8.parquet:md5,4c6710e78ddfe5b884bddc68075469a8", - "ratios.16.9.parquet:md5,bcc9f266e8b4a413ed5891c8f9004c15", - "ratios.2.2.parquet:md5,c7a8cfad502bba4584464886765669d6", - "ratios.2.3.parquet:md5,a2b971819baba756a59f54bd01a8f717", - "ratios.2.4.parquet:md5,cada010249b816fa5af1a83cbabb58e8", - "ratios.2.5.parquet:md5,d3166ea2f6007f7daebdd6a2f01f9af8", - "ratios.2.6.parquet:md5,12cc3dd399c46404dde0992758bfc12d", - "ratios.2.7.parquet:md5,d0736c1d14f36e22470fec6aaa12558a", - "ratios.2.8.parquet:md5,8c1aa3b16c8e9cefaa8c5ea3eb04f071", - "ratios.2.9.parquet:md5,00ecc1b89e5606f62a674656a576ff23", - "ratios.3.3.parquet:md5,69e387ec438162a7276ddbc1c8ad4018", - "ratios.3.4.parquet:md5,464795964bff4de3267b4849519dcca7", - "ratios.3.5.parquet:md5,d51f9b33fcbc5764a1a5f129e9c39e3b", - "ratios.3.6.parquet:md5,97d240a5624a341af59fcac729fdfe70", - "ratios.3.7.parquet:md5,57fb1098fa56c7d4ad2ba82e3993d774", - "ratios.3.8.parquet:md5,5b672e3976a735100f50e098b4062750", - "ratios.3.9.parquet:md5,0e23b6d7a9c33324ad1ccadb36a5da68", - "ratios.4.4.parquet:md5,d60a5f2789db5ac57a7f8faa091bb20c", - "ratios.4.5.parquet:md5,60eebdde4c11d7f32281f65745d2525f", - "ratios.4.6.parquet:md5,370bede0e31df9914bf1aee7773c2eea", - "ratios.4.7.parquet:md5,b33605e9c8da1021c926af7c6fd0620d", - "ratios.4.8.parquet:md5,2a68fcdd641475c74175f0fa79c7d3d0", - "ratios.4.9.parquet:md5,3845d9a7ba740c90a4c0c9e9d949b2ce", - "ratios.5.5.parquet:md5,13e1c2fa249c78521bae4e2f55f990a4", - "ratios.5.6.parquet:md5,7429980f1826dc91d35768ae028de870", - "ratios.5.7.parquet:md5,856d74554d29a0647e68ad83ee978b34", - "ratios.5.8.parquet:md5,bd4450c0695bf3ae9b97b78e7f2ce5f3", - "ratios.5.9.parquet:md5,359a82ad0f012c860ed9642d8aca69e0", - "ratios.6.6.parquet:md5,1f6aeb5dd66ba711d794e3b222311897", - "ratios.6.7.parquet:md5,94ccc05575d8a9c1a3e6d5efd4acae53", - "ratios.6.8.parquet:md5,bc576a0a98d6363a87da4747b350c2ff", - "ratios.6.9.parquet:md5,3c559259321928f055aae93a9eea1929", - "ratios.7.7.parquet:md5,2e60bfcc6557ff6a044887de00c8fdc8", - "ratios.7.8.parquet:md5,a2da6ceecf34eb4fdd94d9dbd32b901c", - "ratios.7.9.parquet:md5,00b3bfa42b5aa45d7e5ce63482c32674", - "ratios.8.8.parquet:md5,0f56ce921fe189e254a2d1975a67177f", - "ratios.8.9.parquet:md5,f90e4fca2c5e91a6762164c90f52c905", - "ratios.9.9.parquet:md5,e088b0dd1b55d37fcbc4885101a4b08c", - "accessions.txt:md5,624ae1990d8fa2dccb2c5b74c66f4956", - "geo_all_datasets.metadata.tsv:md5,e80c4bc5a3a16a5db7678979e3ab3cb3", - "geo_rejected_datasets.metadata.tsv:md5,d8e091bf86fe483af6d525c9127162a7", - "geo_selected_datasets.metadata.tsv:md5,4dc869829149eea9fba6ce4180a88e44", - "failure_reason.txt:md5,2631bb8c3ae982a1b869107f8dbfa107", - "candidate_counts.parquet:md5,14de589fd41b204db541dc58433b0d7b", + "accessions.txt:md5,48a6870e7e7e7d481e9b28002e68880e", + "geo_all_datasets.metadata.tsv:md5,aae011d6e88c3b095b45caafb67fe968", + "geo_rejected_datasets.metadata.tsv:md5,8fe446cf2da9db7df8929c49fcef18b0", + "geo_selected_datasets.metadata.tsv:md5,37ee10d019b753d09b7d6d4da3ec6e9f", + "warning_reason.txt:md5,da67cfc13dc9c91064833a73039700b2", "E_MTAB_5072_rnaseq.rnaseq.raw.counts.mapping.csv:md5,f103d18b88f8329f5fc8e7082d251deb", "E_MTAB_5072_rnaseq.rnaseq.raw.counts.metadata.csv:md5,8ec8f92cc8f81ca44c4c737251d3bd4a", - "E_MTAB_5072_rnaseq.rnaseq.raw.counts.renamed.csv:md5,e1eb2368e24142693643fb129d04f710", + "E_MTAB_5072_rnaseq.rnaseq.raw.counts.renamed.csv:md5,463d259147e6d4a0f80be34b3e4b8047", "whole_gene_id_mapping.csv:md5,8824265badf80ba59db63b6dfa8672dc", "whole_gene_metadata.csv:md5,8c89bfe0e0320d3354e78ba622764c88", - "count_chunk.0.parquet:md5,bd1ea826d49befb0cb5454948d920c39", - "count_chunk.1.parquet:md5,fd97dee3ae911d27bd47d4a840f46830", - "count_chunk.10.parquet:md5,7c340633da6fd513d6a20406b0620ba2", - "count_chunk.11.parquet:md5,9a39ccd89610ea06e14f690c251598cb", - "count_chunk.12.parquet:md5,03883954d0adaa635898288b4a72354a", - "count_chunk.13.parquet:md5,0df547c7277b5bc02dcc11324932db7e", - "count_chunk.14.parquet:md5,54e7169645b834bf53fd8dd36655ab88", - "count_chunk.15.parquet:md5,75207fa3253bf889333028cd92171304", - "count_chunk.16.parquet:md5,c5a678689803a6ef5ffa65f8d99f8112", - "count_chunk.2.parquet:md5,14d317611549f3dafcf8ceccd60bf5c4", - "count_chunk.3.parquet:md5,f39572e7750909ed6853c867e08b1518", - "count_chunk.4.parquet:md5,40e302da7eb770880fb6b7f6edb3ee3b", - "count_chunk.5.parquet:md5,ccbb86356291b99e9ffe40fc599a5aa8", - "count_chunk.6.parquet:md5,1f138aa451915fe477285246d79ebb0b", - "count_chunk.7.parquet:md5,dec2d4c03b6d87cf55fc54fcc6e2d97a", - "count_chunk.8.parquet:md5,3d778e59ed371124b0f569377f5ebeba", - "count_chunk.9.parquet:md5,41d26735819b933d1d95f4dcf01d12f7", - "all_counts.parquet:md5,6e1db3f9aaee4e528db8e539e381b03f", - "all_counts.parquet:md5,6e1db3f9aaee4e528db8e539e381b03f", "whole_design.csv:md5,c9bfd7bc8ca365222e03e67eb24b9a76", - "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", - "multiqc_eatlas_all_experiments_metadata.txt:md5,da7b03757b1d128f598e1b2b04925ca9", - "multiqc_eatlas_selected_experiments_metadata.txt:md5,da7b03757b1d128f598e1b2b04925ca9", - "multiqc_expression_distributions_top_stable_genes.txt:md5,0aba3d9fda34be5c31a091e17a42afba", - "multiqc_gene_statistics.txt:md5,376f2ed44f3aebb0f9dba66b3dffe31a", - "multiqc_geo_all_experiments_metadata.txt:md5,faf123e1f8ec253f1ed26d274b764eab", - "multiqc_geo_failure_reasons.txt:md5,5fef39d6bbcc18c4afafb1606517d335", - "multiqc_geo_rejected_experiments_metadata.txt:md5,f06159683fbc74f10ae5518a8325aae5", - "multiqc_geo_selected_experiments_metadata.txt:md5,c20dea1de9d7637fed8c58ed2b4719e7", - "multiqc_ranked_top_stable_genes_summary.txt:md5,ae54bc58d86a6f95bb3c5e525cd209eb", - "versions.yml:md5,f2e24085d3e7fa254b14b99bde5f2030", - "E_MTAB_5072_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,760061cd64c5d8b1cfd84a4f14b8bc25", - "stability_values.normfinder.csv:md5,fa28be676b85c7bad2587a4a0c15139f", - "E_MTAB_5072_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet:md5,91a6f555922d132276d6897ca8438e88", - "std.0.0.parquet:md5,bec540987111844db9a116058c7b535f", - "std.0.1.parquet:md5,f35662d53a8940682011ef471e91696e", - "std.0.10.parquet:md5,e13339c67b0da3a287a6bd4a8ea3630c", - "std.0.11.parquet:md5,be70d7213ff011d6e878dcf5b3a7e7dc", - "std.0.12.parquet:md5,d2f522a24f281c4c9c8402a7af9e9bc1", - "std.0.13.parquet:md5,169415fcd4731cbe642d62563748b7e0", - "std.0.14.parquet:md5,f84145555e3042c93d107bd42cf2b8c5", - "std.0.15.parquet:md5,45d0ed09f4a705a6c1962d8b57c51a4f", - "std.0.16.parquet:md5,c8b78eb2598c49ec2667d545fa581859", - "std.0.2.parquet:md5,86846e6c7f8c7b90bff7c16e72982bc6", - "std.0.3.parquet:md5,4374a082a646bcb342a90a0a964fc556", - "std.0.4.parquet:md5,bcc1a4e78e05a75b2c7971ee16e012b4", - "std.0.5.parquet:md5,69e7da0d7d4424b53a5a33f576c34026", - "std.0.6.parquet:md5,b00e8e2528472ef0474cb62b8f8ef061", - "std.0.7.parquet:md5,7f910c6ffd1d3ab8859ecb4f25f84c77", - "std.0.8.parquet:md5,ca4ac847e4ed42f7c4f020fd831d49cd", - "std.0.9.parquet:md5,9d8e8be6e8e055d896c5379ea7321cae", - "std.1.1.parquet:md5,f6f35561dce41b629ae2bf1441dba5d9", - "std.1.10.parquet:md5,2047d749abba655de2be7d3466e46db5", - "std.1.11.parquet:md5,7f2206102c9ae1fd5b71388daec380fb", - "std.1.12.parquet:md5,6cb2e9ca15aca5a3784a70539603b3bc", - "std.1.13.parquet:md5,6548f9ff93bc2bd77c337c6109517ba7", - "std.1.14.parquet:md5,a5c122bf0232356b85a631cb54ca5463", - "std.1.15.parquet:md5,61c53a4c539541a9cc481f16391cf184", - "std.1.16.parquet:md5,a654f0f440a72c371453cf4d9a61f9c5", - "std.1.2.parquet:md5,2eeece83a942a00d1716ec6f87275614", - "std.1.3.parquet:md5,44a19c3da4d1fc9a5f4c1d6612ad0e82", - "std.1.4.parquet:md5,3e91adda72bcb80a4949a724929c0855", - "std.1.5.parquet:md5,d94f0751b37c8c23a7c36f375c4def2e", - "std.1.6.parquet:md5,dc79205c0b2ed297d4296517de3a594b", - "std.1.7.parquet:md5,fe677376801f54571482394219c36186", - "std.1.8.parquet:md5,ed8f8b588a6a6ffd510f78f52f62ccf4", - "std.1.9.parquet:md5,6c13d527cee43cb93094e228305e965a", - "std.10.10.parquet:md5,c44b948928556c304cf211634fa96a30", - "std.10.11.parquet:md5,17879c3cdf884be20e0255178b95ac17", - "std.10.12.parquet:md5,3ed5619d7732ecf5aa36450b9e495544", - "std.10.13.parquet:md5,c2bfa8e7bba6234ee977d40088b422a3", - "std.10.14.parquet:md5,739810e7e26d04ed6c0d4e0d1aca861c", - "std.10.15.parquet:md5,88e42f1305fdec5874ca9b688cf8a78c", - "std.10.16.parquet:md5,1a9549a20a672e57a4c7a191ec282192", - "std.10.2.parquet:md5,dfe1915bb8c0a61751c6fda45e9ed408", - "std.10.3.parquet:md5,389ebded30f70efb867e15ef4cf736eb", - "std.10.4.parquet:md5,ea92f9211ee69e909d96b7b223075fe5", - "std.10.5.parquet:md5,198356c3a023d820e78011076c3d25b0", - "std.10.6.parquet:md5,d795250305cecfbaca44cb5d64ae8175", - "std.10.7.parquet:md5,aabd8b45b702eda1e8aa2796bee5fd53", - "std.10.8.parquet:md5,7e6098486c96be7ace982ca496bbe0bf", - "std.10.9.parquet:md5,8ea0f4c10cd271a5e6e3e193da6c541f", - "std.11.11.parquet:md5,7fc49febc643aa2cab8e8fb7763e4d92", - "std.11.12.parquet:md5,65af201da1f9fbadf3e17477e99152c2", - "std.11.13.parquet:md5,a7cb295b895f3f031566efe41f436e76", - "std.11.14.parquet:md5,2f585c9e82a279599eaa5bc2eda0607d", - "std.11.15.parquet:md5,0fa74121a8ac3e4c6e7eb8811cfb693a", - "std.11.16.parquet:md5,0a5beb03eb2e9947becd71b408db3147", - "std.11.2.parquet:md5,7d2e1746a91b3966cc43112fa1531038", - "std.11.3.parquet:md5,583e96eaf2a5d88e554652d8dd68d722", - "std.11.4.parquet:md5,07e7ce47b1fda2eda8539dbea53eda6e", - "std.11.5.parquet:md5,b5eaa57e2617e83d980b98661c05e3ce", - "std.11.6.parquet:md5,4109972649d4b377a57f8ac7d4de62bf", - "std.11.7.parquet:md5,96ea24852caa063d3d79949374113429", - "std.11.8.parquet:md5,3fb8da0084127c063ab1f8884cb4255f", - "std.11.9.parquet:md5,6cd716906739b18d7c6df3ab02195cfc", - "std.12.12.parquet:md5,fc2d81e292e7410cb929864f464a972d", - "std.12.13.parquet:md5,2bfa4d8ef0121ec3258f69554bff9d6b", - "std.12.14.parquet:md5,efbeb9fac34a1ba5d2ed2e37dcf29ab2", - "std.12.15.parquet:md5,5b7ab41c67f83cd958d277b9c2a5d3d8", - "std.12.16.parquet:md5,9f2c2764dd554c88b72e27836d68ccac", - "std.12.2.parquet:md5,ab6c71cbbcc2b0b2984b2e6c135c54e5", - "std.12.3.parquet:md5,d07a101fd175b01024006bb126eec216", - "std.12.4.parquet:md5,c631987fdd819424cc6ce2aa486fcb6a", - "std.12.5.parquet:md5,b1e61754391177b141c442cb5bd11b81", - "std.12.6.parquet:md5,71e691c5a815378713f7b0f3116837d1", - "std.12.7.parquet:md5,d637481baf36e7960f816fc65352d47a", - "std.12.8.parquet:md5,f22c75dbced152d62c46de9b576c0587", - "std.12.9.parquet:md5,b06eaac56ac9c76e6256ebe7d510eaf6", - "std.13.13.parquet:md5,dfdc0c55ac38f6e0df2af915dd658fd9", - "std.13.14.parquet:md5,40f93cdafce15e853560b44007b179e2", - "std.13.15.parquet:md5,b0840ba6b5a20bf4e60067e344b5ddf7", - "std.13.16.parquet:md5,665f9e95b59bea94e22ecd16a84c78c7", - "std.13.2.parquet:md5,191aaedaefa158ed3b86153058ebb1b9", - "std.13.3.parquet:md5,d4622758a37649a38ac05365289fa0f0", - "std.13.4.parquet:md5,8d8f5367a147ecc5c5b5030406ed6f89", - "std.13.5.parquet:md5,2a9165891821553ff89f2f2dff2c6af7", - "std.13.6.parquet:md5,7d76c2f040c6e3343f2830407d42c88a", - "std.13.7.parquet:md5,ff49f2cf17d7e81783783d9619b5647c", - "std.13.8.parquet:md5,2107ae2b43b98b10c98c9c85c322f7e0", - "std.13.9.parquet:md5,ea26155d7bb26435476fb476c8914265", - "std.14.14.parquet:md5,d0a615af385f5ec3578de354654beb4f", - "std.14.15.parquet:md5,52df8ad236c0242ccf9d0db7d0bb6869", - "std.14.16.parquet:md5,e3299d2236b45d18c5b85e28ef4064bd", - "std.14.2.parquet:md5,d4eaa873930291c2aaec25d81f67c9c0", - "std.14.3.parquet:md5,4a32df7e9bec4d69e0f7c0027ffda41b", - "std.14.4.parquet:md5,276ad4c29ab0ab8dedbedecf2124e81d", - "std.14.5.parquet:md5,cc138e58d0f1e647f0c955f305815b97", - "std.14.6.parquet:md5,03af0036a435cc4df2dd86bdc47bc1c9", - "std.14.7.parquet:md5,fbb6e97313f33cbb471699d3aac4de69", - "std.14.8.parquet:md5,6d4161f586f8eeaf90f7a7d3adb5fb83", - "std.14.9.parquet:md5,31b4b040a9873c91e68544a810857242", - "std.15.15.parquet:md5,9a0f78d7b4ac8482ab02cf94e97c8444", - "std.15.16.parquet:md5,88b95112a9c798f597af468dec2fb25a", - "std.15.2.parquet:md5,587409e4900fc42bc9ad268023f82ca6", - "std.15.3.parquet:md5,f0c470e9dec0b4af4f5160094762efa9", - "std.15.4.parquet:md5,23c5db2e48870e2dd8e70a11e2cc9d30", - "std.15.5.parquet:md5,8fb19f88ed5a8cf5e9bef99a2680794b", - "std.15.6.parquet:md5,7951f8b720a559e7f4795008b259fd5e", - "std.15.7.parquet:md5,92df6cd844d5888dc6d99763ddc8bcf4", - "std.15.8.parquet:md5,8327692f86547cfab55bc6ebcce52d99", - "std.15.9.parquet:md5,5145ea5595ce024696c6eda23cef5411", - "std.16.16.parquet:md5,a8043c3e8c9cdb30abc85158499cc554", - "std.16.2.parquet:md5,ad88763ff77d66f9250f7690ba52caf9", - "std.16.3.parquet:md5,9da7a3bf4af2f178deef5e624a27de4b", - "std.16.4.parquet:md5,908040f29bede0105833aef4d6b68093", - "std.16.5.parquet:md5,9ce0418b62220bcf5d02b64b501235aa", - "std.16.6.parquet:md5,1547fabcd56ea9fe1d9b52899331de37", - "std.16.7.parquet:md5,9acdb9e10f1ac984df5f0195ad6d4115", - "std.16.8.parquet:md5,0def299f731b56c6a4b2ff36a5045dcc", - "std.16.9.parquet:md5,f05b890abbdf0de6b24ef6231971478a", - "std.2.2.parquet:md5,ae084d6c926199cbfb6657f5352115b7", - "std.2.3.parquet:md5,192f00fd26a999e41698dae2f90bc260", - "std.2.4.parquet:md5,a5c639a6431e352bfc017277c2c07150", - "std.2.5.parquet:md5,d95a71f8cc3675675c9e05be21bf3120", - "std.2.6.parquet:md5,f8a526a4041fb104c3753970094c676f", - "std.2.7.parquet:md5,74bf61b2fb069e01a31cf3b167c924e4", - "std.2.8.parquet:md5,9092ca2f856be33bd5e95fb1a56aa5e0", - "std.2.9.parquet:md5,80d236ef711348375bdac41ecbb03473", - "std.3.3.parquet:md5,3e03e359e94cc0fcb06a7b258f7435db", - "std.3.4.parquet:md5,348e9f0e2b86dec18d1cb2adda6c2eb0", - "std.3.5.parquet:md5,1be93d5117a62bfb7a71094ef2a535e6", - "std.3.6.parquet:md5,c505077669c8b24b334e3d8e03ca5197", - "std.3.7.parquet:md5,b6fb46887c67da79977d0afd01c56f95", - "std.3.8.parquet:md5,4631a95dc3d5680d383a0fbdaf6df4de", - "std.3.9.parquet:md5,e2c555c89f0ab784c79d62f0738af02c", - "std.4.4.parquet:md5,82674d070f78879acfdea5fa296bc3c3", - "std.4.5.parquet:md5,0d7a772af7a202ad48390dd3f360a474", - "std.4.6.parquet:md5,780ceb469af1e8704e8bd6f4b183d1ff", - "std.4.7.parquet:md5,54be3ff3620c043d969bcd634dbb5553", - "std.4.8.parquet:md5,8f4f99b1a95d774f132967d46ada0d39", - "std.4.9.parquet:md5,dc71240ea39a5892409f21bc201d230d", - "std.5.5.parquet:md5,88cb2cc27066095fe16801ca60abf44b", - "std.5.6.parquet:md5,274bdb489783d44201c05af530afb676", - "std.5.7.parquet:md5,aacb7e15ae56a3df6866f2c0da14ebc3", - "std.5.8.parquet:md5,dcd3f126e1b0703c45948539d4402b2d", - "std.5.9.parquet:md5,d31d77a9cc80d8ead0766c5282c47e4a", - "std.6.6.parquet:md5,2a719245ca611936906b58b67286975c", - "std.6.7.parquet:md5,ec0115c3a4eac59d2932acc8c5e8f704", - "std.6.8.parquet:md5,b8508f2b809990d021c9e1b41ff693c2", - "std.6.9.parquet:md5,2a4a41092af439fb1be0f43eb53c549a", - "std.7.7.parquet:md5,719697dfd59560c06752d3e6795967f0", - "std.7.8.parquet:md5,c6c4d54af8e65e4898e46238f46a50a9", - "std.7.9.parquet:md5,d931b29807bf298dde4253df560c5b7f", - "std.8.8.parquet:md5,2ee5662d4150dcc6530897f8f5fb0e46", - "std.8.9.parquet:md5,2120a2cf36237082518b0a2a13446163", - "std.9.9.parquet:md5,2433f39ba1e0922d27a2f897fe2b4742" + "E_MTAB_5072_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,9d449d05aecf363a92ebb60ac9a16637", + "stability_values.normfinder.csv:md5,66ac05c256ad5d2a085aafcdee4d40b9", + "geo_warning_reasons.csv:md5,bd904c137f444293f64d506d156466e5" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-19T15:20:29.251259083" + }, + "-profile test_dataset_custom_mapping": { + "content": [ + null, + [ + "aggregate_results", + "aggregate_results/all_counts_filtered.parquet", + "aggregate_results/all_genes_summary.csv", + "aggregate_results/top_stable_genes_summary.csv", + "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", + "clean_count_data", + "clean_count_data/cleaned_counts_filtered.parquet", + "compute_base_statistics", + "compute_base_statistics/stats_all_genes.csv", + "compute_base_statistics_for_microarray", + "compute_base_statistics_for_microarray/microarray.stats_all_genes.csv", + "compute_base_statistics_for_rnaseq", + "compute_base_statistics_for_rnaseq/rnaseq.stats_all_genes.csv", + "compute_stability_scores", + "compute_stability_scores/stats_with_scores.csv", + "dash_app", + "dash_app/app.py", + "dash_app/assets", + "dash_app/assets/style.css", + "dash_app/data", + "dash_app/data/all_counts.parquet", + "dash_app/data/all_genes_summary.csv", + "dash_app/data/whole_design.csv", + "dash_app/file_system_backend", + "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", + "dash_app/spec-file.txt", + "dash_app/src", + "dash_app/src/callbacks", + "dash_app/src/callbacks/common.py", + "dash_app/src/callbacks/genes.py", + "dash_app/src/callbacks/samples.py", + "dash_app/src/components", + "dash_app/src/components/graphs.py", + "dash_app/src/components/icons.py", + "dash_app/src/components/right_sidebar.py", + "dash_app/src/components/settings", + "dash_app/src/components/settings/genes.py", + "dash_app/src/components/settings/samples.py", + "dash_app/src/components/stores.py", + "dash_app/src/components/tables.py", + "dash_app/src/components/tooltips.py", + "dash_app/src/components/top.py", + "dash_app/src/utils", + "dash_app/src/utils/config.py", + "dash_app/src/utils/data_management.py", + "dash_app/src/utils/style.py", + "dash_app/versions.yml", + "dataset_statistics", + "dataset_statistics/microarray.normalised.dataset_stats.csv", + "dataset_statistics/rnaseq.raw.dataset_stats.csv", + "errors", + "geo", + "get_candidate_genes", + "get_candidate_genes/candidate_counts.parquet", + "idmapping", + "idmapping/whole_gene_metadata.csv", + "merge_all_counts", + "merge_all_counts/all_counts.parquet", + "merge_microarray_counts", + "merge_microarray_counts/all_counts.parquet", + "merge_rnaseq_counts", + "merge_rnaseq_counts/all_counts.parquet", + "merged_datasets", + "merged_datasets/whole_design.csv", + "multiqc", + "multiqc/multiqc_data", + "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_expression_distributions_top_stable_genes.txt", + "multiqc/multiqc_data/multiqc_gene_statistics.txt", + "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", + "multiqc/multiqc_data/multiqc_software_versions.txt", + "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_plots", + "multiqc/multiqc_plots/pdf", + "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", + "multiqc/multiqc_plots/pdf/gene_statistics.pdf", + "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", + "multiqc/multiqc_plots/png", + "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", + "multiqc/multiqc_plots/png/gene_statistics.png", + "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", + "multiqc/multiqc_plots/svg", + "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", + "multiqc/multiqc_plots/svg/gene_statistics.svg", + "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", + "multiqc/multiqc_report.html", + "multiqc/versions.yml", + "normalised", + "normalised/rnaseq.raw", + "normalised/rnaseq.raw/normalisation_deseq2", + "normalised/rnaseq.raw/normalisation_deseq2/rnaseq.raw.cpm.csv", + "normfinder", + "normfinder/stability_values.normfinder.csv", + "pipeline_info", + "pipeline_info/software_mqc_versions.yml", + "quantile_normalised", + "quantile_normalised/microarray.normalised", + "quantile_normalised/microarray.normalised/microarray.normalised.quant_norm.parquet", + "quantile_normalised/rnaseq.raw", + "quantile_normalised/rnaseq.raw/rnaseq.raw.cpm.quant_norm.parquet", + "warnings" + ], + [ + "all_genes_summary.csv:md5,5402ddb07c9b7b2ec39217d8170094bd", + "top_stable_genes_summary.csv:md5,5402ddb07c9b7b2ec39217d8170094bd", + "top_stable_genes_transposed_counts_filtered.csv:md5,869c72204f45b8212ce2fd2e79221a6e", + "stats_all_genes.csv:md5,08b6baca5dd63d9e57b9b1c080a901c6", + "microarray.stats_all_genes.csv:md5,69c6a3908fa708e8f3670a9e96321f7d", + "rnaseq.stats_all_genes.csv:md5,427f59e5c2b5920f83bab591268e1fcb", + "stats_with_scores.csv:md5,f38a32ae4520ec843b076502b2d80074", + "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", + "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", + "all_genes_summary.csv:md5,5402ddb07c9b7b2ec39217d8170094bd", + "whole_design.csv:md5,70d6c2673e619ca52d2774fb3e368382", + "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", + "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", + "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", + "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", + "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", + "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", + "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", + "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", + "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", + "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", + "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", + "tables.py:md5,84d17ad7f43458412e550f74a24560e5", + "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", + "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", + "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", + "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", + "style.py:md5,b9f1207f06464e43c05e6e3912f12731", + "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", + "microarray.normalised.dataset_stats.csv:md5,4993a895b6561b9a4f939906bae582b4", + "rnaseq.raw.dataset_stats.csv:md5,c407ed714414e689c760fe1b450d5f58", + "whole_gene_metadata.csv:md5,544307bd920abd662a14ba43772d94b7", + "whole_design.csv:md5,70d6c2673e619ca52d2774fb3e368382", + "rnaseq.raw.cpm.csv:md5,59f604e6266fbc46948287f384c3639b", + "stability_values.normfinder.csv:md5,4e37152d1f0a8a4376b86cbf64b9f73c" ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-13T11:31:46.635883176" + "timestamp": "2025-11-19T18:05:03.755948892" } } \ No newline at end of file diff --git a/tests/modules/local/aggregate_results/main.nf.test.snap b/tests/modules/local/aggregate_results/main.nf.test.snap index 40c1fbc0..20057f31 100644 --- a/tests/modules/local/aggregate_results/main.nf.test.snap +++ b/tests/modules/local/aggregate_results/main.nf.test.snap @@ -3,16 +3,16 @@ "content": [ { "0": [ - "all_genes_summary.csv:md5,ba17539a8a7b462f0e455dd4f81c5e62" + "all_genes_summary.csv:md5,f2248dde8d461101270c5fceaf2d8c40" ], "1": [ - "top_stable_genes_summary.csv:md5,3b49b24a0cb36b9e35b37410917de0b1" + "top_stable_genes_summary.csv:md5,f2248dde8d461101270c5fceaf2d8c40" ], "2": [ "all_counts_filtered.parquet:md5,286e7595b3a7b961e39976585d3c106e" ], "3": [ - "top_stable_genes_transposed_counts_filtered.csv:md5,0380b330c64ce9efe9644f84e7a5cddb" + "top_stable_genes_transposed_counts_filtered.csv:md5,dcb4f5e4bf8fc0538bf99554fbc47f5f" ], "4": [ [ @@ -32,13 +32,13 @@ "all_counts_filtered.parquet:md5,286e7595b3a7b961e39976585d3c106e" ], "all_genes_summary": [ - "all_genes_summary.csv:md5,ba17539a8a7b462f0e455dd4f81c5e62" + "all_genes_summary.csv:md5,f2248dde8d461101270c5fceaf2d8c40" ], "top_stable_genes_summary": [ - "top_stable_genes_summary.csv:md5,3b49b24a0cb36b9e35b37410917de0b1" + "top_stable_genes_summary.csv:md5,f2248dde8d461101270c5fceaf2d8c40" ], "top_stable_genes_transposed_counts_filtered": [ - "top_stable_genes_transposed_counts_filtered.csv:md5,0380b330c64ce9efe9644f84e7a5cddb" + "top_stable_genes_transposed_counts_filtered.csv:md5,dcb4f5e4bf8fc0538bf99554fbc47f5f" ] } ], @@ -46,22 +46,22 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-12T10:36:49.026267498" + "timestamp": "2025-11-19T15:20:34.967697416" }, "With microarray": { "content": [ { "0": [ - "all_genes_summary.csv:md5,da32872124a121e27f3dc1c784c8601b" + "all_genes_summary.csv:md5,1757570c492a6aa0b150613f92304d24" ], "1": [ - "top_stable_genes_summary.csv:md5,ea79d53a1b149fa1622f85cb49aad5fc" + "top_stable_genes_summary.csv:md5,4ab34096569e9bf21e2344daa23c2f41" ], "2": [ "all_counts_filtered.parquet:md5,286e7595b3a7b961e39976585d3c106e" ], "3": [ - "top_stable_genes_transposed_counts_filtered.csv:md5,0380b330c64ce9efe9644f84e7a5cddb" + "top_stable_genes_transposed_counts_filtered.csv:md5,dcb4f5e4bf8fc0538bf99554fbc47f5f" ], "4": [ [ @@ -81,13 +81,13 @@ "all_counts_filtered.parquet:md5,286e7595b3a7b961e39976585d3c106e" ], "all_genes_summary": [ - "all_genes_summary.csv:md5,da32872124a121e27f3dc1c784c8601b" + "all_genes_summary.csv:md5,1757570c492a6aa0b150613f92304d24" ], "top_stable_genes_summary": [ - "top_stable_genes_summary.csv:md5,ea79d53a1b149fa1622f85cb49aad5fc" + "top_stable_genes_summary.csv:md5,4ab34096569e9bf21e2344daa23c2f41" ], "top_stable_genes_transposed_counts_filtered": [ - "top_stable_genes_transposed_counts_filtered.csv:md5,0380b330c64ce9efe9644f84e7a5cddb" + "top_stable_genes_transposed_counts_filtered.csv:md5,dcb4f5e4bf8fc0538bf99554fbc47f5f" ] } ], @@ -95,6 +95,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-12T11:08:47.11957525" + "timestamp": "2025-11-19T15:20:39.606518336" } } \ No newline at end of file diff --git a/tests/modules/local/compute_base_statistics/main.nf.test.snap b/tests/modules/local/compute_base_statistics/main.nf.test.snap index 854efb21..87ef14f7 100644 --- a/tests/modules/local/compute_base_statistics/main.nf.test.snap +++ b/tests/modules/local/compute_base_statistics/main.nf.test.snap @@ -3,7 +3,7 @@ "content": [ { "0": [ - "stats_all_genes.csv:md5,db4da5a9d006a3bb2552bc4c3653c894" + "stats_all_genes.csv:md5,bdcac2a45728098fe2d79775bcfdb743" ], "1": [ [ @@ -20,7 +20,7 @@ ] ], "stats": [ - "stats_all_genes.csv:md5,db4da5a9d006a3bb2552bc4c3653c894" + "stats_all_genes.csv:md5,bdcac2a45728098fe2d79775bcfdb743" ] } ], @@ -28,13 +28,13 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T14:38:59.179553063" + "timestamp": "2025-11-19T15:20:52.920513223" }, "RNAseq platform": { "content": [ { "0": [ - "rnaseq.stats_all_genes.csv:md5,dea1294e04f0571cca9d5a8301b2a711" + "rnaseq.stats_all_genes.csv:md5,4f87865422331270c8aba3077faab8fa" ], "1": [ [ @@ -51,7 +51,7 @@ ] ], "stats": [ - "rnaseq.stats_all_genes.csv:md5,dea1294e04f0571cca9d5a8301b2a711" + "rnaseq.stats_all_genes.csv:md5,4f87865422331270c8aba3077faab8fa" ] } ], @@ -59,6 +59,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T14:39:03.817265373" + "timestamp": "2025-11-19T15:20:57.621995644" } } \ No newline at end of file diff --git a/tests/modules/local/compute_stability_scores/main.nf.test.snap b/tests/modules/local/compute_stability_scores/main.nf.test.snap index cbcf0c8a..beae489b 100644 --- a/tests/modules/local/compute_stability_scores/main.nf.test.snap +++ b/tests/modules/local/compute_stability_scores/main.nf.test.snap @@ -3,7 +3,7 @@ "content": [ { "0": [ - "stats_with_scores.csv:md5,dcf0165aebf5905242fc375f2d734c1a" + "stats_with_scores.csv:md5,7a33855baf1381d7f2140bc146d196ee" ], "1": [ [ @@ -20,7 +20,7 @@ ] ], "stats_with_stability_scores": [ - "stats_with_scores.csv:md5,dcf0165aebf5905242fc375f2d734c1a" + "stats_with_scores.csv:md5,7a33855baf1381d7f2140bc146d196ee" ] } ], @@ -28,13 +28,13 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T15:23:36.939080125" + "timestamp": "2025-11-19T15:21:08.808960169" }, "Without Genorm": { "content": [ { "0": [ - "stats_with_scores.csv:md5,2c5c81309a2b09eb44163d8a02393ce0" + "stats_with_scores.csv:md5,f650d762d9456129d3d262e10ec0f17c" ], "1": [ [ @@ -51,7 +51,7 @@ ] ], "stats_with_stability_scores": [ - "stats_with_scores.csv:md5,2c5c81309a2b09eb44163d8a02393ce0" + "stats_with_scores.csv:md5,f650d762d9456129d3d262e10ec0f17c" ] } ], @@ -59,6 +59,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T15:23:31.460651402" + "timestamp": "2025-11-19T15:21:03.302792724" } } \ No newline at end of file diff --git a/tests/modules/local/expressionatlas/getdata/main.nf.test.snap b/tests/modules/local/expressionatlas/getdata/main.nf.test.snap index f242cbb9..f624143d 100644 --- a/tests/modules/local/expressionatlas/getdata/main.nf.test.snap +++ b/tests/modules/local/expressionatlas/getdata/main.nf.test.snap @@ -3,20 +3,10 @@ "content": [ { "0": [ - [ - { - "accession": "E-MTAB-552" - }, - "E_MTAB_552_rnaseq.rnaseq.raw.counts.csv:md5,830f50b60b17b62f9ca2f6a163a2879f" - ] + "E_MTAB_552_rnaseq.rnaseq.raw.counts.csv:md5,830f50b60b17b62f9ca2f6a163a2879f" ], "1": [ - [ - { - "accession": "E-MTAB-552" - }, - "E_MTAB_552_rnaseq.design.csv:md5,b81490696f638e90c1cf14236bb0c08c" - ] + "E_MTAB_552_rnaseq.design.csv:md5,b81490696f638e90c1cf14236bb0c08c" ], "2": [ @@ -39,20 +29,10 @@ ] ], "counts": [ - [ - { - "accession": "E-MTAB-552" - }, - "E_MTAB_552_rnaseq.rnaseq.raw.counts.csv:md5,830f50b60b17b62f9ca2f6a163a2879f" - ] + "E_MTAB_552_rnaseq.rnaseq.raw.counts.csv:md5,830f50b60b17b62f9ca2f6a163a2879f" ], "design": [ - [ - { - "accession": "E-MTAB-552" - }, - "E_MTAB_552_rnaseq.design.csv:md5,b81490696f638e90c1cf14236bb0c08c" - ] + "E_MTAB_552_rnaseq.design.csv:md5,b81490696f638e90c1cf14236bb0c08c" ] } ], @@ -60,26 +40,16 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T12:15:01.955520856" + "timestamp": "2025-11-17T11:22:31.479739243" }, "Arabidopsis Geo dataset": { "content": [ { "0": [ - [ - { - "accession": "E-GEOD-62537" - }, - "E_GEOD_62537_A_AFFY_2.microarray.normalised.counts.csv:md5,673c55171d0ccfc1d036bf43c49ae320" - ] + "E_GEOD_62537_A_AFFY_2.microarray.normalised.counts.csv:md5,673c55171d0ccfc1d036bf43c49ae320" ], "1": [ - [ - { - "accession": "E-GEOD-62537" - }, - "E_GEOD_62537_A_AFFY_2.design.csv:md5,7d7dd72be4f5b326dd25a36db01ebf88" - ] + "E_GEOD_62537_A_AFFY_2.design.csv:md5,7d7dd72be4f5b326dd25a36db01ebf88" ], "2": [ @@ -102,20 +72,10 @@ ] ], "counts": [ - [ - { - "accession": "E-GEOD-62537" - }, - "E_GEOD_62537_A_AFFY_2.microarray.normalised.counts.csv:md5,673c55171d0ccfc1d036bf43c49ae320" - ] + "E_GEOD_62537_A_AFFY_2.microarray.normalised.counts.csv:md5,673c55171d0ccfc1d036bf43c49ae320" ], "design": [ - [ - { - "accession": "E-GEOD-62537" - }, - "E_GEOD_62537_A_AFFY_2.design.csv:md5,7d7dd72be4f5b326dd25a36db01ebf88" - ] + "E_GEOD_62537_A_AFFY_2.design.csv:md5,7d7dd72be4f5b326dd25a36db01ebf88" ] } ], @@ -123,26 +83,16 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T12:15:21.026054246" + "timestamp": "2025-11-17T11:22:51.038290522" }, "Transcription profiling by array of Arabidopsis mutant for fis2 (microarray)": { "content": [ { "0": [ - [ - { - "accession": "E-TABM-1007" - }, - "E_TABM_1007_A_AFFY_2.microarray.normalised.counts.csv:md5,a3afe33d7eaed3339da9109bf25bb3ed" - ] + "E_TABM_1007_A_AFFY_2.microarray.normalised.counts.csv:md5,a3afe33d7eaed3339da9109bf25bb3ed" ], "1": [ - [ - { - "accession": "E-TABM-1007" - }, - "E_TABM_1007_A_AFFY_2.design.csv:md5,120f63cae2193b97d7451483bdbbaab1" - ] + "E_TABM_1007_A_AFFY_2.design.csv:md5,120f63cae2193b97d7451483bdbbaab1" ], "2": [ @@ -165,20 +115,10 @@ ] ], "counts": [ - [ - { - "accession": "E-TABM-1007" - }, - "E_TABM_1007_A_AFFY_2.microarray.normalised.counts.csv:md5,a3afe33d7eaed3339da9109bf25bb3ed" - ] + "E_TABM_1007_A_AFFY_2.microarray.normalised.counts.csv:md5,a3afe33d7eaed3339da9109bf25bb3ed" ], "design": [ - [ - { - "accession": "E-TABM-1007" - }, - "E_TABM_1007_A_AFFY_2.design.csv:md5,120f63cae2193b97d7451483bdbbaab1" - ] + "E_TABM_1007_A_AFFY_2.design.csv:md5,120f63cae2193b97d7451483bdbbaab1" ] } ], @@ -186,6 +126,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T12:15:11.97993127" + "timestamp": "2025-11-17T11:22:41.604691764" } } \ No newline at end of file diff --git a/tests/modules/local/geo/getdata/main.nf.test.snap b/tests/modules/local/geo/getdata/main.nf.test.snap index a987b657..e2b5f595 100644 --- a/tests/modules/local/geo/getdata/main.nf.test.snap +++ b/tests/modules/local/geo/getdata/main.nf.test.snap @@ -1,5 +1,5 @@ { - "Accession does not exist": { + "Drosophila simulans - Expression profiling by high throughput sequencing / One raw count found": { "content": [ { "0": [ @@ -9,29 +9,135 @@ ], "2": [ + + ], + "3": [ + + ], + "4": [ + + ], + "5": [ [ - "GSE568945478", - "failure_reason.txt:md5,2631bb8c3ae982a1b869107f8dbfa107" + "GEO_GETDATA", + "R", + "4.4.3 (2025-02-28)" ] + ], + "6": [ + [ + "GEO_GETDATA", + "GEOquery", + "2.74.0" + ] + ], + "7": [ + [ + "GEO_GETDATA", + "dplyr", + "1.1.4" + ] + ], + "counts": [ + + ], + "design": [ + + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-17T11:28:08.073509318" + }, + "Drosophila simulans - No data found": { + "content": [ + { + "0": [ + + ], + "1": [ + + ], + "2": [ + ], "3": [ ], "4": [ + + ], + "5": [ [ "GEO_GETDATA", "R", "4.4.3 (2025-02-28)" ] ], - "5": [ + "6": [ [ "GEO_GETDATA", "GEOquery", "2.74.0" ] ], + "7": [ + [ + "GEO_GETDATA", + "dplyr", + "1.1.4" + ] + ], + "counts": [ + + ], + "design": [ + + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-17T11:27:41.033275401" + }, + "Accession does not exist": { + "content": [ + { + "0": [ + + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + + ], + "4": [ + + ], + "5": [ + [ + "GEO_GETDATA", + "R", + "4.4.3 (2025-02-28)" + ] + ], "6": [ + [ + "GEO_GETDATA", + "GEOquery", + "2.74.0" + ] + ], + "7": [ [ "GEO_GETDATA", "dplyr", @@ -50,9 +156,9 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T13:40:59.903657365" + "timestamp": "2025-11-17T11:27:24.164943756" }, - "Beta vulgaris - Small RNA of sugar beet in response to drought stress": { + "Drosophila simulans - Expression profiling by array": { "content": [ { "0": [ @@ -62,29 +168,188 @@ ], "2": [ + + ], + "3": [ + + ], + "4": [ + + ], + "5": [ + [ + "GEO_GETDATA", + "R", + "4.4.3 (2025-02-28)" + ] + ], + "6": [ + [ + "GEO_GETDATA", + "GEOquery", + "2.74.0" + ] + ], + "7": [ [ - "GSE205328", - "failure_reason.txt:md5,2631bb8c3ae982a1b869107f8dbfa107" + "GEO_GETDATA", + "dplyr", + "1.1.4" ] + ], + "counts": [ + + ], + "design": [ + + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-17T11:27:49.459757662" + }, + "Drosophila simulans - Expression profiling by high throughput sequencing / Some raw counts found": { + "content": [ + { + "0": [ + + ], + "1": [ + + ], + "2": [ + ], "3": [ ], "4": [ + + ], + "5": [ [ "GEO_GETDATA", "R", "4.4.3 (2025-02-28)" ] + ], + "6": [ + [ + "GEO_GETDATA", + "GEOquery", + "2.74.0" + ] + ], + "7": [ + [ + "GEO_GETDATA", + "dplyr", + "1.1.4" + ] + ], + "counts": [ + + ], + "design": [ + + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-17T11:27:58.80892872" + }, + "Drosophila simulans - Only one sample among several": { + "content": [ + { + "0": [ + + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + + ], + "4": [ + ], "5": [ + [ + "GEO_GETDATA", + "R", + "4.4.3 (2025-02-28)" + ] + ], + "6": [ [ "GEO_GETDATA", "GEOquery", "2.74.0" ] ], + "7": [ + [ + "GEO_GETDATA", + "dplyr", + "1.1.4" + ] + ], + "counts": [ + + ], + "design": [ + + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-17T11:27:32.555586232" + }, + "Beta vulgaris - Small RNA of sugar beet in response to drought stress": { + "content": [ + { + "0": [ + + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + + ], + "4": [ + + ], + "5": [ + [ + "GEO_GETDATA", + "R", + "4.4.3 (2025-02-28)" + ] + ], "6": [ + [ + "GEO_GETDATA", + "GEOquery", + "2.74.0" + ] + ], + "7": [ [ "GEO_GETDATA", "dplyr", @@ -103,6 +368,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T13:43:46.551738497" + "timestamp": "2025-11-17T11:27:16.009504875" } } \ No newline at end of file diff --git a/tests/modules/local/idmapping/gprofiler/main.nf.test.snap b/tests/modules/local/idmapping/gprofiler/main.nf.test.snap index 9b78bb9d..733bb10f 100644 --- a/tests/modules/local/idmapping/gprofiler/main.nf.test.snap +++ b/tests/modules/local/idmapping/gprofiler/main.nf.test.snap @@ -7,7 +7,7 @@ { "dataset": "test" }, - "counts.ensembl_ids.renamed.csv:md5,b2f45fd17fcb2688ea08b94527f70c1f" + "counts.ensembl_ids.renamed.csv:md5,3a4a73f829d7dcb93420cc3de98adcdd" ] ], "1": [ @@ -45,7 +45,7 @@ { "dataset": "test" }, - "counts.ensembl_ids.renamed.csv:md5,b2f45fd17fcb2688ea08b94527f70c1f" + "counts.ensembl_ids.renamed.csv:md5,3a4a73f829d7dcb93420cc3de98adcdd" ] ], "metadata": [ @@ -57,7 +57,7 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T19:45:48.962861272" + "timestamp": "2025-11-17T11:28:57.680785987" }, "Custom mapping": { "content": [ @@ -67,7 +67,7 @@ { "dataset": "test" }, - "counts.ensembl_ids.renamed.csv:md5,b2f45fd17fcb2688ea08b94527f70c1f" + "counts.ensembl_ids.renamed.csv:md5,3a4a73f829d7dcb93420cc3de98adcdd" ] ], "1": [ @@ -105,7 +105,7 @@ { "dataset": "test" }, - "counts.ensembl_ids.renamed.csv:md5,b2f45fd17fcb2688ea08b94527f70c1f" + "counts.ensembl_ids.renamed.csv:md5,3a4a73f829d7dcb93420cc3de98adcdd" ] ], "metadata": [ @@ -117,7 +117,7 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T13:09:26.858764543" + "timestamp": "2025-11-17T11:28:52.208091927" }, "Map Ensembl IDs to themselves": { "content": [ @@ -180,7 +180,7 @@ { "dataset": "test" }, - "counts.uniprot_ids.renamed.csv:md5,a93d7145f8b35f6074cdf21c7c97de7c" + "counts.uniprot_ids.renamed.csv:md5,e8d91cae1a6f2c888d9e46e9a8dce147" ] ], "1": [ @@ -218,7 +218,7 @@ { "dataset": "test" }, - "counts.uniprot_ids.renamed.csv:md5,a93d7145f8b35f6074cdf21c7c97de7c" + "counts.uniprot_ids.renamed.csv:md5,e8d91cae1a6f2c888d9e46e9a8dce147" ] ], "metadata": [ @@ -230,7 +230,7 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T12:59:34.111655741" + "timestamp": "2025-11-17T11:28:34.969677294" }, "Map NCBI IDs": { "content": [ @@ -240,7 +240,7 @@ { "dataset": "test" }, - "counts.ncbi_ids.renamed.csv:md5,a93d7145f8b35f6074cdf21c7c97de7c" + "counts.ncbi_ids.renamed.csv:md5,e8d91cae1a6f2c888d9e46e9a8dce147" ] ], "1": [ @@ -278,7 +278,7 @@ { "dataset": "test" }, - "counts.ncbi_ids.renamed.csv:md5,a93d7145f8b35f6074cdf21c7c97de7c" + "counts.ncbi_ids.renamed.csv:md5,e8d91cae1a6f2c888d9e46e9a8dce147" ] ], "metadata": [ @@ -290,6 +290,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T12:59:28.504346412" + "timestamp": "2025-11-17T11:28:28.998501422" } } \ No newline at end of file diff --git a/tests/modules/local/normalisation/deseq2/main.nf.test b/tests/modules/local/normalisation/deseq2/main.nf.test index 958faf86..778c69eb 100644 --- a/tests/modules/local/normalisation/deseq2/main.nf.test +++ b/tests/modules/local/normalisation/deseq2/main.nf.test @@ -12,8 +12,9 @@ nextflow_process { process { """ input[0] = [ - [ accession: "accession", design: file('$projectDir/tests/test_data/normalisation/base/design.csv') ], - file('$projectDir/tests/test_data/normalisation/base/counts.csv') + [ accession: "test" ], + file('$projectDir/tests/test_data/normalisation/base/counts.csv'), + file('$projectDir/tests/test_data/normalisation/base/design.csv') ] """ } @@ -37,8 +38,9 @@ nextflow_process { process { """ input[0] = [ - [ accession: "accession", design: file('$projectDir/tests/test_data/normalisation/many_zeros/design.csv') ], - file('$projectDir/tests/test_data/normalisation/many_zeros/counts.csv') + [ accession: "test"], + file('$projectDir/tests/test_data/normalisation/many_zeros/counts.csv'), + file('$projectDir/tests/test_data/normalisation/many_zeros/design.csv') ] """ } @@ -60,8 +62,9 @@ nextflow_process { process { """ input[0] = [ - [ accession: "accession", design: file('$projectDir/tests/test_data/normalisation/one_group/design.csv') ], - file('$projectDir/tests/test_data/normalisation/one_group/counts.csv') + [ accession: "accession" ], + file('$projectDir/tests/test_data/normalisation/one_group/counts.csv'), + file('$projectDir/tests/test_data/normalisation/one_group/design.csv') ] """ } @@ -76,28 +79,6 @@ nextflow_process { } - test("Without design") { - - tag "deseq2_wo_design" - - when { - - process { - """ - input[0] = [ - [ accession: "accession" ], - file('$projectDir/tests/test_data/normalisation/base/counts.csv') - ] - """ - } - } - - then { - assert !process.success - } - - } - test("TSV files") { when { @@ -105,8 +86,9 @@ nextflow_process { process { """ input[0] = [ - [ accession: "accession", design: file('$projectDir/tests/test_data/normalisation/base/design.tsv') ], - file('$projectDir/tests/test_data/normalisation/base/counts.tsv') + [ accession: "accession" ], + file('$projectDir/tests/test_data/normalisation/base/counts.tsv'), + file('$projectDir/tests/test_data/normalisation/base/design.tsv') ] """ } diff --git a/tests/modules/local/normalisation/deseq2/main.nf.test.snap b/tests/modules/local/normalisation/deseq2/main.nf.test.snap index 1958e538..bd1a6b49 100644 --- a/tests/modules/local/normalisation/deseq2/main.nf.test.snap +++ b/tests/modules/local/normalisation/deseq2/main.nf.test.snap @@ -4,8 +4,7 @@ [ [ { - "accession": "accession", - "design": "design.csv:md5,a83dd6a15463b51d94f0a42c196d7933" + "accession": "test" }, "counts.cpm.csv:md5,df001c189c61c11dfab04d1bb47f7511" ] @@ -15,17 +14,16 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-09T09:18:21.978762401" + "timestamp": "2025-11-19T15:27:44.374650599" }, "One group": { "content": [ [ [ { - "accession": "accession", - "design": "design.csv:md5,28cf54802df5df4a9dc406003623c6a7" + "accession": "accession" }, - "counts.cpm.csv:md5,568be72a338ad95037007bea5d552f86" + "counts.cpm.csv:md5,b77d41a310a21def7b955335880f6892" ] ] ], @@ -33,15 +31,14 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-09T09:18:50.127029306" + "timestamp": "2025-11-17T14:42:06.14494924" }, "TSV files": { "content": [ [ [ { - "accession": "accession", - "design": "design.tsv:md5,7e1fd70fcb7cb6d2835748989b8c0401" + "accession": "accession" }, "counts.cpm.csv:md5,df001c189c61c11dfab04d1bb47f7511" ] @@ -51,17 +48,16 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-09T09:19:17.176614603" + "timestamp": "2025-11-17T14:42:14.745341728" }, "Rows with many zeros": { "content": [ [ [ { - "accession": "accession", - "design": "design.csv:md5,3cdf98e2e202b4af2687eaefd9bdd8e9" + "accession": "test" }, - "counts.cpm.csv:md5,f4741deb2b94a7898fe4e5bcf2135a63" + "counts.cpm.csv:md5,921d29a427ba35ee3e798b4ac2123fd4" ] ] ], @@ -69,6 +65,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-09T09:18:36.066964301" + "timestamp": "2025-11-19T15:27:53.007766023" } } \ No newline at end of file diff --git a/tests/modules/local/normalisation/edger/main.nf.test b/tests/modules/local/normalisation/edger/main.nf.test index 011b3183..e552e8d5 100644 --- a/tests/modules/local/normalisation/edger/main.nf.test +++ b/tests/modules/local/normalisation/edger/main.nf.test @@ -11,8 +11,11 @@ nextflow_process { process { """ - meta = [accession: "accession", design: file('$projectDir/tests/test_data/normalisation/base/design.csv')] - input[0] = [meta, file('$projectDir/tests/test_data/normalisation/base/counts.csv')] + input[0] = [ + [ accession: "test" ], + file('$projectDir/tests/test_data/normalisation/base/counts.csv'), + file('$projectDir/tests/test_data/normalisation/base/design.csv') + ] """ } } @@ -34,8 +37,11 @@ nextflow_process { process { """ - meta = [accession: "accession", design: file('$projectDir/tests/test_data/normalisation/many_zeros/design.csv')] - input[0] = [meta, file('$projectDir/tests/test_data/normalisation/many_zeros/counts.csv')] + input[0] = [ + [ accession: "test"], + file('$projectDir/tests/test_data/normalisation/many_zeros/counts.csv'), + file('$projectDir/tests/test_data/normalisation/many_zeros/design.csv') + ] """ } } @@ -55,8 +61,11 @@ nextflow_process { process { """ - meta = [accession: "accession", design: file('$projectDir/tests/test_data/normalisation/one_group/design.csv')] - input[0] = [meta, file('$projectDir/tests/test_data/normalisation/one_group/counts.csv')] + input[0] = [ + [ accession: "accession" ], + file('$projectDir/tests/test_data/normalisation/one_group/counts.csv'), + file('$projectDir/tests/test_data/normalisation/one_group/design.csv') + ] """ } } @@ -70,32 +79,17 @@ nextflow_process { } - test("Without design") { - - when { - - process { - """ - meta = [ accession: "accession" ] - input[0] = [meta, file('$projectDir/tests/test_data/normalisation/base/counts.csv')] - """ - } - } - - then { - assert !process.success - } - - } - test("TSV files") { when { process { """ - meta = [accession: "accession", design: file('$projectDir/tests/test_data/normalisation/base/design.tsv')] - input[0] = [meta, file('$projectDir/tests/test_data/normalisation/base/counts.tsv')] + input[0] = [ + [ accession: "accession" ], + file('$projectDir/tests/test_data/normalisation/base/counts.tsv'), + file('$projectDir/tests/test_data/normalisation/base/design.tsv') + ] """ } } diff --git a/tests/modules/local/normalisation/edger/main.nf.test.snap b/tests/modules/local/normalisation/edger/main.nf.test.snap index 9b7542ce..807fa0fb 100644 --- a/tests/modules/local/normalisation/edger/main.nf.test.snap +++ b/tests/modules/local/normalisation/edger/main.nf.test.snap @@ -4,8 +4,7 @@ [ [ { - "accession": "accession", - "design": "design.csv:md5,a83dd6a15463b51d94f0a42c196d7933" + "accession": "test" }, "counts.cpm.csv:md5,537bffb095e79d9667c955accd81e3a2" ] @@ -13,17 +12,16 @@ ], "meta": { "nf-test": "0.9.2", - "nextflow": "24.10.3" + "nextflow": "25.04.8" }, - "timestamp": "2025-01-29T14:20:44.011465207" + "timestamp": "2025-11-19T15:28:15.443780813" }, "One group": { "content": [ [ [ { - "accession": "accession", - "design": "design.csv:md5,28cf54802df5df4a9dc406003623c6a7" + "accession": "accession" }, "counts.cpm.csv:md5,680e59fd500ddd83ead508a63f3f9b48" ] @@ -31,17 +29,16 @@ ], "meta": { "nf-test": "0.9.2", - "nextflow": "24.10.3" + "nextflow": "25.04.8" }, - "timestamp": "2025-01-29T14:20:58.681438521" + "timestamp": "2025-11-17T14:43:10.243694942" }, "TSV files": { "content": [ [ [ { - "accession": "accession", - "design": "design.tsv:md5,7e1fd70fcb7cb6d2835748989b8c0401" + "accession": "accession" }, "counts.cpm.csv:md5,537bffb095e79d9667c955accd81e3a2" ] @@ -51,15 +48,14 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-09T09:22:40.458649972" + "timestamp": "2025-11-17T14:43:14.980374559" }, "Rows with many zeros": { "content": [ [ [ { - "accession": "accession", - "design": "design.csv:md5,3cdf98e2e202b4af2687eaefd9bdd8e9" + "accession": "test" }, "counts.cpm.csv:md5,f4624bdcb195b95f6e8bd9ee08470d3d" ] @@ -67,8 +63,8 @@ ], "meta": { "nf-test": "0.9.2", - "nextflow": "24.10.3" + "nextflow": "25.04.8" }, - "timestamp": "2025-01-29T14:20:51.214114937" + "timestamp": "2025-11-19T15:28:20.601710839" } } \ No newline at end of file diff --git a/tests/modules/local/normfinder/main.nf.test.snap b/tests/modules/local/normfinder/main.nf.test.snap index f813c389..77bd83d3 100644 --- a/tests/modules/local/normfinder/main.nf.test.snap +++ b/tests/modules/local/normfinder/main.nf.test.snap @@ -3,7 +3,7 @@ "content": [ { "0": [ - "stability_values.normfinder.csv:md5,2cbe1b4307c32acea9bb844631e79297" + "stability_values.normfinder.csv:md5,febede14f422e963abd897bc9efa897e" ], "1": [ [ @@ -20,7 +20,7 @@ ] ], "stability_values": [ - "stability_values.normfinder.csv:md5,2cbe1b4307c32acea9bb844631e79297" + "stability_values.normfinder.csv:md5,febede14f422e963abd897bc9efa897e" ] } ], @@ -28,13 +28,13 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T12:20:22.039948992" + "timestamp": "2025-11-19T15:28:45.03556591" }, "Very small dataset - Cq values": { "content": [ { "0": [ - "stability_values.normfinder.csv:md5,a52096f27ba998ae76c939833dbc54fc" + "stability_values.normfinder.csv:md5,bdc363268df9d133b8ae7df5a2204103" ], "1": [ [ @@ -51,7 +51,7 @@ ] ], "stability_values": [ - "stability_values.normfinder.csv:md5,a52096f27ba998ae76c939833dbc54fc" + "stability_values.normfinder.csv:md5,bdc363268df9d133b8ae7df5a2204103" ] } ], @@ -59,6 +59,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T12:20:15.183464096" + "timestamp": "2025-11-19T15:28:38.129095619" } } \ No newline at end of file diff --git a/tests/subworkflows/local/genorm/main.nf.test.snap b/tests/subworkflows/local/genorm/main.nf.test.snap index 2d481834..a5208b28 100644 --- a/tests/subworkflows/local/genorm/main.nf.test.snap +++ b/tests/subworkflows/local/genorm/main.nf.test.snap @@ -3,10 +3,10 @@ "content": [ { "0": [ - "m_measures.csv:md5,4b3f77dd146de060ef981e1b5fc59f76" + "m_measures.csv:md5,2704c9915abdddc31c3de104b1cb1b64" ], "m_measures": [ - "m_measures.csv:md5,4b3f77dd146de060ef981e1b5fc59f76" + "m_measures.csv:md5,2704c9915abdddc31c3de104b1cb1b64" ] } ], @@ -14,6 +14,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-10-16T05:59:31.943764871" + "timestamp": "2025-11-19T15:30:38.03592559" } } \ No newline at end of file diff --git a/tests/test_data/input_datasets/input.csv b/tests/test_data/input_datasets/input.csv index 6ea4aa16..73278d53 100644 --- a/tests/test_data/input_datasets/input.csv +++ b/tests/test_data/input_datasets/input.csv @@ -1,3 +1,3 @@ counts,design,platform,normalised -tests/test_data/input_datasets/microarray.normalised.csv,tests/test_data/input_datasets/microarray.normalised.design.csv,microarray,true -tests/test_data/input_datasets/rnaseq.raw.csv,tests/test_data/input_datasets/rnaseq.raw.design.csv,rnaseq,false +https://raw.githubusercontent.com/nf-core/test-datasets/stableexpression/test_data/input_datasets/microarray.normalised.csv,https://raw.githubusercontent.com/nf-core/test-datasets/stableexpression/test_data/input_datasets/microarray.normalised.design.csv,microarray,true +https://raw.githubusercontent.com/nf-core/test-datasets/stableexpression/test_data/input_datasets/rnaseq.raw.csv,https://raw.githubusercontent.com/nf-core/test-datasets/stableexpression/test_data/input_datasets/rnaseq.raw.design.csv,rnaseq,false diff --git a/tests/test_data/input_datasets/input_big.yaml b/tests/test_data/input_datasets/input_big.yaml new file mode 100644 index 00000000..f54577bb --- /dev/null +++ b/tests/test_data/input_datasets/input_big.yaml @@ -0,0 +1,4 @@ +- counts: https://raw.githubusercontent.com/nf-core/test-datasets/differentialabundance/modules_testdata/SRP254919.salmon.merged.gene_counts.top1000cov.assay.tsv + design: https://raw.githubusercontent.com/nf-core/test-datasets/stableexpression/test_data/input_datasets/rnaseq_big.design.csv + platform: rnaseq + normalised: false From 56ca9646ffbb403b0655fc2a60d366cf9aef39fb Mon Sep 17 00:00:00 2001 From: Olivier Date: Wed, 19 Nov 2025 20:44:32 +0100 Subject: [PATCH 171/258] check the number of datasets downloaded from Expression Atlas and prevent fetching accessions from GEO; add parameter to control this threshold --- nextflow.config | 1 + nextflow_schema.json | 7 + subworkflows/local/geo_fetchdata/main.nf | 47 ++++-- .../main.nf | 20 ++- .../local/geo_fetchdata/main.nf.test | 140 +++++++++++++++++ .../local/geo_fetchdata/main.nf.test.snap | 148 ++++++++++++++++++ .../get_accessions/exclude_two_accessions.txt | 2 + workflows/stableexpression.nf | 20 ++- 8 files changed, 367 insertions(+), 18 deletions(-) create mode 100644 tests/subworkflows/local/geo_fetchdata/main.nf.test create mode 100644 tests/subworkflows/local/geo_fetchdata/main.nf.test.snap create mode 100644 tests/test_data/geo/get_accessions/exclude_two_accessions.txt diff --git a/nextflow.config b/nextflow.config index 3ccf9196..32989f4b 100644 --- a/nextflow.config +++ b/nextflow.config @@ -29,6 +29,7 @@ params { exclude_eatlas_accessions_file = null // GEO + min_nb_eatlas_datasets_auto_skip_geo = 1500 skip_fetch_geo_accessions = false geo_accessions = "" exclude_geo_accessions = "" diff --git a/nextflow_schema.json b/nextflow_schema.json index 973f2d01..f395d920 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -126,6 +126,13 @@ "fa_icon": "fas fa-book-atlas", "description": "Options for fetching datasets from NCBI GEO.", "properties": { + "min_nb_eatlas_datasets_auto_skip_geo": { + "type": "integer", + "minimum": 0, + "default": 1500, + "description": "Automatically skip fetching GEO datasets when the number of downloaded Expression Atlas datasets exceeds this threshold.", + "help_text": "By default, both Expression Atlas and GEO datasets are fetched. If enough Expression Atlas datasets are downloaded, fetching GEO accessing and downloading datasets will be skipped automatically. As of November 2025, this feature was made specifically for `homo sapiens`, for which the overall number of datasets largely exceeds usual needs." + }, "skip_fetch_geo_accessions": { "type": "boolean", "fa_icon": "fas fa-cloud-arrow-down", diff --git a/subworkflows/local/geo_fetchdata/main.nf b/subworkflows/local/geo_fetchdata/main.nf index 28e95135..6eaa1de1 100644 --- a/subworkflows/local/geo_fetchdata/main.nf +++ b/subworkflows/local/geo_fetchdata/main.nf @@ -2,7 +2,8 @@ include { GEO_GETACCESSIONS } from '../../../modules/local/geo/getacces include { GEO_GETDATA } from '../../../modules/local/geo/getdata' include { addDatasetIdToMetadata } from '../utils_nfcore_stableexpression_pipeline' include { groupFilesByDatasetId } from '../utils_nfcore_stableexpression_pipeline' -include { augmentMetadata } from '../utils_nfcore_stableexpression_pipeline' +include { augmentMetadata } from '../utils_nfcore_stableexpression_pipeline' +include { geoDatasetsToFetch } from '../utils_nfcore_stableexpression_pipeline' /* ======================================================================================== @@ -14,7 +15,19 @@ workflow GEO_FETCHDATA { take: species + skip_fetch_geo_accessions + accessions_only + platform + keywords + geo_accessions + geo_accessions_file + exclude_geo_accessions + exclude_geo_accessions_file ch_eatlas_excluded_accessions + ch_nb_downloaded_eatlas_datasets + min_nb_eatlas_datasets_auto_skip_geo + outdir + main: @@ -32,10 +45,10 @@ workflow GEO_FETCHDATA { .set { ch_excluded_eatlas_accessions } // parsing file listing excluded accessions - ch_exclude_geo_accessions_file = params.exclude_geo_accessions_file ? Channel.fromPath(params.exclude_geo_accessions_file, checkIfExists: true) : Channel.empty() + ch_exclude_geo_accessions_file = exclude_geo_accessions_file ? Channel.fromPath(exclude_geo_accessions_file, checkIfExists: true) : Channel.empty() // getting accessions to exclude and preparing in the right format - Channel.fromList( params.exclude_geo_accessions.tokenize(',') ) + Channel.fromList( exclude_geo_accessions.tokenize(',') ) .mix( ch_excluded_eatlas_accessions ) .mix( ch_exclude_geo_accessions_file.splitText() ) .unique() @@ -45,7 +58,7 @@ workflow GEO_FETCHDATA { ch_excluded_accessions .collectFile( name: 'excluded_geo_accessions.txt', - storeDir: "${params.outdir}/geo/", + storeDir: "${outdir}/geo/", sort: true, newLine: true ) @@ -57,15 +70,25 @@ workflow GEO_FETCHDATA { // ------------------------------------------------------------------------------------ // fetching GEO accessions if applicable - if ( !params.skip_fetch_geo_accessions ) { + if ( !skip_fetch_geo_accessions ) { + + // checking the number of Expression Atlas datasets downloaded + // and storing whether to skip fetching GEO accessions + ch_geo_to_fetch = geoDatasetsToFetch( ch_nb_downloaded_eatlas_datasets, min_nb_eatlas_datasets_auto_skip_geo ) + + // trick to decide whether to fetch GEO accessions or not depending on ch_geo_to_fetch + Channel.value(species) + .combine( ch_geo_to_fetch ) + .filter{ species_name, to_fetch -> to_fetch } // kept only when to_fetch is true + .map { species_name, to_fetch -> species_name } + .set { ch_species } // getting GEO accessions given a species name and keywords // keywords can be an empty string - def platform = params.platform ?: 'none' GEO_GETACCESSIONS( - species, - params.keywords, - platform, + ch_species, + keywords, + platform ?: 'none', ch_excluded_accessions_file, "none" ) @@ -80,9 +103,9 @@ workflow GEO_FETCHDATA { // PREPARE ACCESSIONS PROVIDED BY THE USER // ------------------------------------------------------------------------------------ - ch_geo_accessions_file = params.geo_accessions_file ? Channel.fromPath(params.geo_accessions_file, checkIfExists: true) : Channel.empty() + ch_geo_accessions_file = geo_accessions_file ? Channel.fromPath(geo_accessions_file, checkIfExists: true) : Channel.empty() - Channel.fromList( params.geo_accessions.tokenize(',') ) + Channel.fromList( geo_accessions.tokenize(',') ) .mix( ch_geo_accessions_file.splitText() ) .mix( ch_fetched_accessions ) .unique() @@ -94,7 +117,7 @@ workflow GEO_FETCHDATA { // DOWNLOAD GEO DATASETS // ------------------------------------------------------------------------------------ - if ( !params.accessions_only ) { + if ( !accessions_only ) { // Downloading GEO datasets for each accession in ch_accessions GEO_GETDATA( diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index 4cb979d6..77627407 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -413,7 +413,7 @@ def getWholeDatasetSize( ch_counts ) { /* ======================================================================================== - FUNCTIONS FOR DISPLAYING INFORMATION ABOUT DATA + FUNCTIONS FOR CHECKING NB OF DATASETS ======================================================================================== */ @@ -432,3 +432,21 @@ def checkCounts(ch_counts) { } } } + +def geoDatasetsToFetch(ch_nb_downloaded_eatlas_datasets, threshold) { + // display a warning if no datasets are found + def msg = [ + "More than ${threshold} Expression Atlas datasets found. ", + "Will skip fetching GEO dataset accessions" + ].join("\n").trim() + + return ch_nb_downloaded_eatlas_datasets + .map { n -> + if( n >= threshold ) { + log.warn(msg) + return false + } else { + return true + } + } +} diff --git a/tests/subworkflows/local/geo_fetchdata/main.nf.test b/tests/subworkflows/local/geo_fetchdata/main.nf.test new file mode 100644 index 00000000..42625540 --- /dev/null +++ b/tests/subworkflows/local/geo_fetchdata/main.nf.test @@ -0,0 +1,140 @@ +nextflow_workflow { + + name "Test Workflow GEO_FETCHDATA" + script "subworkflows/local/geo_fetchdata/main.nf" + workflow "GEO_FETCHDATA" + tag "geo_fetchdata" + tag "subworkflow" + + test("Simple run") { + + when { + + workflow { + """ + input[0] = "beta_vulgaris" // species + input[1] = false // skip_fetch_geo_accessions + input[2] = false // accessions_only + input[3] = null // platform + input[4] = "" // keywords + input[5] = "" // geo_accessions + input[6] = null // geo_accessions_file + input[7] = "" // exclude_geo_accessions + input[8] = null // exclude_geo_accessions_file + input[9] = Channel.empty() // ch_eatlas_excluded_accessions + input[10] = Channel.value(1) // ch_nb_downloaded_eatlas_datasets + input[11] = 1500 // min_nb_eatlas_datasets_auto_skip_geo + input[12] = "test_output" // outdir + """ + } + } + + then { + assertAll( + { assert workflow.success }, + { assert snapshot(workflow.out).match() } + ) + } + + } + + test("Accesions only") { + + when { + + workflow { + """ + input[0] = "beta_vulgaris" // species + input[1] = false // skip_fetch_geo_accessions + input[2] = true // accessions_only + input[3] = null // platform + input[4] = "" // keywords + input[5] = "" // geo_accessions + input[6] = null // geo_accessions_file + input[7] = "" // exclude_geo_accessions + input[8] = null // exclude_geo_accessions_file + input[9] = Channel.empty() // ch_eatlas_excluded_accessions + input[10] = Channel.value(1) // ch_nb_downloaded_eatlas_datasets + input[11] = 1500 // min_nb_eatlas_datasets_auto_skip_geo + input[12] = "test_output" // outdir + """ + } + } + + then { + assertAll( + { assert workflow.success }, + { assert workflow.out.downloaded_datasets.size() == 0 }, + { assert snapshot(workflow.out).match() } + ) + } + + } + + test("Exclude / force include accessions + platform + keyword") { + + when { + + workflow { + """ + input[0] = "beta_vulgaris" // species + input[1] = false // skip_fetch_geo_accessions + input[2] = false // accessions_only + input[3] = "rnaseq" // platform + input[4] = "leaf" // keywords + input[5] = "GSE55951" // geo_accessions + input[6] = null // geo_accessions_file + input[7] = "GSE79526" // exclude_geo_accessions + input[8] = file( '$projectDir/tests/test_data/geo/get_accessions/exclude_two_accessions.txt', checkIfExists: true ) // exclude_geo_accessions_file + input[9] = Channel.empty() // ch_eatlas_excluded_accessions + input[10] = Channel.value(1) // ch_nb_downloaded_eatlas_datasets + input[11] = 1500 // min_nb_eatlas_datasets_auto_skip_geo + input[12] = "test_output" // outdir + """ + } + } + + then { + assertAll( + { assert workflow.success }, + { assert workflow.out.downloaded_datasets.size() == 1 }, + { assert snapshot(workflow.out).match() } + ) + } + + } + + test("Number of Eatlas datasets exceeded threshold + force download one accession") { + tag "geo_fetchdata_over_threshold" + when { + + workflow { + """ + input[0] = "beta_vulgaris" // species + input[1] = false // skip_fetch_geo_accessions + input[2] = false // accessions_only + input[3] = null // platform + input[4] = "" // keywords + input[5] = "GSE55951" // geo_accessions + input[6] = null // geo_accessions_file + input[7] = "" // exclude_geo_accessions + input[8] = null // exclude_geo_accessions_file + input[9] = Channel.empty() // ch_eatlas_excluded_accessions + input[10] = Channel.value(10) // ch_nb_downloaded_eatlas_datasets + input[11] = 10 // min_nb_eatlas_datasets_auto_skip_geo + input[12] = "test_output" // outdir + """ + } + } + + then { + assertAll( + { assert workflow.success }, + { assert workflow.out.downloaded_datasets.size() == 1 }, + { assert snapshot(workflow.out).match() } + ) + } + + } + +} diff --git a/tests/subworkflows/local/geo_fetchdata/main.nf.test.snap b/tests/subworkflows/local/geo_fetchdata/main.nf.test.snap new file mode 100644 index 00000000..9fea4ed0 --- /dev/null +++ b/tests/subworkflows/local/geo_fetchdata/main.nf.test.snap @@ -0,0 +1,148 @@ +{ + "Exclude / force include accessions + platform + keyword": { + "content": [ + { + "0": [ + [ + { + "dataset": "GSE55951_GPL18429", + "design": "GSE55951_GPL18429.microarray.normalised.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d" + }, + "GSE55951_GPL18429.microarray.normalised.counts.csv:md5,18fd2d728ad2ec5cb78f994f73375144" + ] + ], + "downloaded_datasets": [ + [ + { + "dataset": "GSE55951_GPL18429", + "design": "GSE55951_GPL18429.microarray.normalised.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d" + }, + "GSE55951_GPL18429.microarray.normalised.counts.csv:md5,18fd2d728ad2ec5cb78f994f73375144" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-19T19:57:27.31062531" + }, + "Number of Eatlas datasets exceeded threshold + one accession included": { + "content": [ + { + "0": [ + + ], + "downloaded_datasets": [ + + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-19T19:45:10.916816453" + }, + "Number of Eatlas datasets exceeded threshold": { + "content": [ + { + "0": [ + [ + { + "dataset": "GSE55951_GPL18429", + "design": "GSE55951_GPL18429.microarray.normalised.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d", + "normalised": true, + "platform": "microarray" + }, + "GSE55951_GPL18429.microarray.normalised.counts.csv:md5,18fd2d728ad2ec5cb78f994f73375144" + ] + ], + "downloaded_datasets": [ + [ + { + "dataset": "GSE55951_GPL18429", + "design": "GSE55951_GPL18429.microarray.normalised.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d", + "normalised": true, + "platform": "microarray" + }, + "GSE55951_GPL18429.microarray.normalised.counts.csv:md5,18fd2d728ad2ec5cb78f994f73375144" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-19T20:24:45.768801518" + }, + "Simple run": { + "content": [ + { + "0": [ + [ + { + "dataset": "GSE55951_GPL18429", + "design": "GSE55951_GPL18429.microarray.normalised.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d", + "normalised": true, + "platform": "microarray" + }, + "GSE55951_GPL18429.microarray.normalised.counts.csv:md5,18fd2d728ad2ec5cb78f994f73375144" + ] + ], + "downloaded_datasets": [ + [ + { + "dataset": "GSE55951_GPL18429", + "design": "GSE55951_GPL18429.microarray.normalised.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d", + "normalised": true, + "platform": "microarray" + }, + "GSE55951_GPL18429.microarray.normalised.counts.csv:md5,18fd2d728ad2ec5cb78f994f73375144" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-19T19:47:33.406202342" + }, + "Accesions only": { + "content": [ + { + "0": [ + + ], + "downloaded_datasets": [ + + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-19T19:23:32.005374433" + }, + "Exclude / force include accessions": { + "content": [ + { + "0": [ + + ], + "downloaded_datasets": [ + + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-19T19:23:59.957750144" + } +} \ No newline at end of file diff --git a/tests/test_data/geo/get_accessions/exclude_two_accessions.txt b/tests/test_data/geo/get_accessions/exclude_two_accessions.txt new file mode 100644 index 00000000..0ef19a43 --- /dev/null +++ b/tests/test_data/geo/get_accessions/exclude_two_accessions.txt @@ -0,0 +1,2 @@ +GSE79526 +GSE55951 diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index ba05fb06..76bdb968 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -20,9 +20,6 @@ include { DASH_APP } from '../modules/local/dash_a include { storeDatasetSize } from '../subworkflows/local/utils_nfcore_stableexpression_pipeline' include { checkCounts } from '../subworkflows/local/utils_nfcore_stableexpression_pipeline' - - - /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ RUN MAIN WORKFLOW @@ -51,6 +48,7 @@ workflow STABLEEXPRESSION { // ----------------------------------------------------------------- EXPRESSIONATLAS_FETCHDATA( species ) + EXPRESSIONATLAS_FETCHDATA.out.downloaded_datasets.set { ch_eatlas_downloaded_datasets } // ----------------------------------------------------------------- // FETCH AND DOWNLOAD GEO DATASETS IF NEEDED @@ -58,12 +56,24 @@ workflow STABLEEXPRESSION { GEO_FETCHDATA ( species, - EXPRESSIONATLAS_FETCHDATA.out.accessions + params.skip_fetch_geo_accessions, + params.accessions_only, + params.platform, + params.keywords, + params.geo_accessions, + params.geo_accessions_file, + params.exclude_geo_accessions, + params.exclude_geo_accessions_file, + EXPRESSIONATLAS_FETCHDATA.out.accessions, + ch_eatlas_downloaded_datasets.count(), + params.min_nb_eatlas_datasets_auto_skip_geo, + params.outdir ) + // putting all datasets together (local datasets + Expression Atlas datasets) ch_input_datasets - .concat( EXPRESSIONATLAS_FETCHDATA.out.downloaded_datasets ) + .concat( ch_eatlas_downloaded_datasets ) .concat( GEO_FETCHDATA.out.downloaded_datasets ) .set { ch_counts } From b6a7138b03265c80210a78bf4ce9d7c666bd0a17 Mon Sep 17 00:00:00 2001 From: nf-core-bot Date: Thu, 20 Nov 2025 09:32:27 +0000 Subject: [PATCH 172/258] Template update for nf-core/tools version 3.5.1 --- .github/workflows/awsfulltest.yml | 2 +- .github/workflows/awstest.yml | 2 +- .github/workflows/download_pipeline.yml | 2 +- .github/workflows/fix_linting.yml | 2 +- .github/workflows/linting.yml | 6 +-- .github/workflows/nf-test.yml | 4 +- .github/workflows/release-announcements.yml | 9 ++--- .../workflows/template-version-comment.yml | 2 +- .nf-core.yml | 2 +- .prettierignore | 2 + README.md | 4 +- modules.json | 4 +- modules/nf-core/multiqc/environment.yml | 2 +- modules/nf-core/multiqc/main.nf | 4 +- .../nf-core/multiqc/tests/main.nf.test.snap | 24 ++++++------ nextflow.config | 1 + ro-crate-metadata.json | 14 +++---- .../main.nf | 6 +-- .../nf-core/utils_nfcore_pipeline/main.nf | 2 +- workflows/stableexpression.nf | 38 ++++++++++++++----- 20 files changed, 75 insertions(+), 57 deletions(-) diff --git a/.github/workflows/awsfulltest.yml b/.github/workflows/awsfulltest.yml index 59b87e9f..a5f81c34 100644 --- a/.github/workflows/awsfulltest.yml +++ b/.github/workflows/awsfulltest.yml @@ -40,7 +40,7 @@ jobs: } profiles: test_full - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 with: name: Seqera Platform debug log file path: | diff --git a/.github/workflows/awstest.yml b/.github/workflows/awstest.yml index 6700b805..429570cd 100644 --- a/.github/workflows/awstest.yml +++ b/.github/workflows/awstest.yml @@ -25,7 +25,7 @@ jobs: } profiles: test - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 with: name: Seqera Platform debug log file path: | diff --git a/.github/workflows/download_pipeline.yml b/.github/workflows/download_pipeline.yml index 6d94bcbf..45884ff9 100644 --- a/.github/workflows/download_pipeline.yml +++ b/.github/workflows/download_pipeline.yml @@ -127,7 +127,7 @@ jobs: fi - name: Upload Nextflow logfile for debugging purposes - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 with: name: nextflow_logfile.txt path: .nextflow.log* diff --git a/.github/workflows/fix_linting.yml b/.github/workflows/fix_linting.yml index cf35e844..6df255c4 100644 --- a/.github/workflows/fix_linting.yml +++ b/.github/workflows/fix_linting.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: # Use the @nf-core-bot token to check out so we can push later - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 with: token: ${{ secrets.nf_core_bot_auth_token }} diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 30e66026..7a527a34 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -11,7 +11,7 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Set up Python 3.14 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6 @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out pipeline code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Install Nextflow uses: nf-core/setup-nextflow@v2 @@ -71,7 +71,7 @@ jobs: - name: Upload linting log file artifact if: ${{ always() }} - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 with: name: linting-logs path: | diff --git a/.github/workflows/nf-test.yml b/.github/workflows/nf-test.yml index e20bf6d0..c98d76ec 100644 --- a/.github/workflows/nf-test.yml +++ b/.github/workflows/nf-test.yml @@ -40,7 +40,7 @@ jobs: rm -rf ./* || true rm -rf ./.??* || true ls -la ./ - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 with: fetch-depth: 0 @@ -85,7 +85,7 @@ jobs: TOTAL_SHARDS: ${{ needs.nf-test-changes.outputs.total_shards }} steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 with: fetch-depth: 0 diff --git a/.github/workflows/release-announcements.yml b/.github/workflows/release-announcements.yml index e64cebd6..431d3d44 100644 --- a/.github/workflows/release-announcements.yml +++ b/.github/workflows/release-announcements.yml @@ -15,10 +15,9 @@ jobs: 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 - name: get description - id: get_topics + 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 - + 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@master with: access-token: ${{ secrets.MASTODON_ACCESS_TOKEN }} @@ -27,9 +26,7 @@ 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_topics.outputs.description }} - + ${{ steps.get_description.outputs.description }} Please see the changelog: ${{ github.event.release.html_url }} ${{ steps.get_topics.outputs.topics }} #nfcore #openscience #nextflow #bioinformatics diff --git a/.github/workflows/template-version-comment.yml b/.github/workflows/template-version-comment.yml index c5988af9..e8560fc7 100644 --- a/.github/workflows/template-version-comment.yml +++ b/.github/workflows/template-version-comment.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out pipeline code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 with: ref: ${{ github.event.pull_request.head.sha }} diff --git a/.nf-core.yml b/.nf-core.yml index 328ce751..ce1370fb 100644 --- a/.nf-core.yml +++ b/.nf-core.yml @@ -10,7 +10,7 @@ lint: nextflow_config: - params.input schema_lint: false -nf_core_version: 3.4.1 +nf_core_version: 3.5.1 repository_type: pipeline template: author: Olivier Coen diff --git a/.prettierignore b/.prettierignore index 2255e3e3..dd749d43 100644 --- a/.prettierignore +++ b/.prettierignore @@ -12,3 +12,5 @@ testing* bin/ .nf-test/ ro-crate-metadata.json +modules/nf-core/ +subworkflows/nf-core/ diff --git a/README.md b/README.md index ce6f85ca..e6a52620 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,13 @@ -[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new/nf-core/stableexpression) +[![Open in GitHub Codespaces](https://img.shields.io/badge/Open_In_GitHub_Codespaces-black?labelColor=grey&logo=github)](https://github.com/codespaces/new/nf-core/stableexpression) [![GitHub Actions CI Status](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml/badge.svg)](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml) [![GitHub Actions Linting Status](https://github.com/nf-core/stableexpression/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/stableexpression/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/stableexpression/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/version-%E2%89%A525.04.0-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-3.4.1-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/3.4.1) +[![nf-core template version](https://img.shields.io/badge/nf--core_template-3.5.1-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/3.5.1) [![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/) diff --git a/modules.json b/modules.json index c1c2b2e8..61b6f208 100644 --- a/modules.json +++ b/modules.json @@ -7,7 +7,7 @@ "nf-core": { "multiqc": { "branch": "master", - "git_sha": "e10b76ca0c66213581bec2833e30d31f239dec0b", + "git_sha": "af27af1be706e6a2bb8fe454175b0cdf77f47b49", "installed_by": ["modules"] } } @@ -21,7 +21,7 @@ }, "utils_nfcore_pipeline": { "branch": "master", - "git_sha": "05954dab2ff481bcb999f24455da29a5828af08d", + "git_sha": "271e7fc14eb1320364416d996fb077421f3faed2", "installed_by": ["subworkflows"] }, "utils_nfschema_plugin": { diff --git a/modules/nf-core/multiqc/environment.yml b/modules/nf-core/multiqc/environment.yml index dd513cbd..d02016a0 100644 --- a/modules/nf-core/multiqc/environment.yml +++ b/modules/nf-core/multiqc/environment.yml @@ -4,4 +4,4 @@ channels: - conda-forge - bioconda dependencies: - - bioconda::multiqc=1.31 + - bioconda::multiqc=1.32 diff --git a/modules/nf-core/multiqc/main.nf b/modules/nf-core/multiqc/main.nf index 5288f5cc..c1158fb0 100644 --- a/modules/nf-core/multiqc/main.nf +++ b/modules/nf-core/multiqc/main.nf @@ -3,8 +3,8 @@ process MULTIQC { conda "${moduleDir}/environment.yml" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/ef/eff0eafe78d5f3b65a6639265a16b89fdca88d06d18894f90fcdb50142004329/data' : - 'community.wave.seqera.io/library/multiqc:1.31--1efbafd542a23882' }" + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/8c/8c6c120d559d7ee04c7442b61ad7cf5a9e8970be5feefb37d68eeaa60c1034eb/data' : + 'community.wave.seqera.io/library/multiqc:1.32--d58f60e4deb769bf' }" input: path multiqc_files, stageAs: "?/*" diff --git a/modules/nf-core/multiqc/tests/main.nf.test.snap b/modules/nf-core/multiqc/tests/main.nf.test.snap index 17881d15..a88bafd6 100644 --- a/modules/nf-core/multiqc/tests/main.nf.test.snap +++ b/modules/nf-core/multiqc/tests/main.nf.test.snap @@ -2,14 +2,14 @@ "multiqc_versions_single": { "content": [ [ - "versions.yml:md5,8968b114a3e20756d8af2b80713bcc4f" + "versions.yml:md5,737bb2c7cad54ffc2ec020791dc48b8f" ] ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.6" + "nf-test": "0.9.3", + "nextflow": "24.10.4" }, - "timestamp": "2025-09-08T20:57:36.139055243" + "timestamp": "2025-10-27T13:33:24.356715" }, "multiqc_stub": { "content": [ @@ -17,25 +17,25 @@ "multiqc_report.html", "multiqc_data", "multiqc_plots", - "versions.yml:md5,8968b114a3e20756d8af2b80713bcc4f" + "versions.yml:md5,737bb2c7cad54ffc2ec020791dc48b8f" ] ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.6" + "nf-test": "0.9.3", + "nextflow": "24.10.4" }, - "timestamp": "2025-09-08T20:59:15.142230631" + "timestamp": "2025-10-27T13:34:11.103619" }, "multiqc_versions_config": { "content": [ [ - "versions.yml:md5,8968b114a3e20756d8af2b80713bcc4f" + "versions.yml:md5,737bb2c7cad54ffc2ec020791dc48b8f" ] ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.6" + "nf-test": "0.9.3", + "nextflow": "24.10.4" }, - "timestamp": "2025-09-08T20:58:29.629087066" + "timestamp": "2025-10-27T13:34:04.615233" } } \ No newline at end of file diff --git a/nextflow.config b/nextflow.config index e4e72bae..fd2b2f62 100644 --- a/nextflow.config +++ b/nextflow.config @@ -164,6 +164,7 @@ profiles { test { includeConfig 'conf/test.config' } test_full { includeConfig 'conf/test_full.config' } } + // Load nf-core custom profiles from different institutions // 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. diff --git a/ro-crate-metadata.json b/ro-crate-metadata.json index a7ebeb6a..0c98feb8 100644 --- a/ro-crate-metadata.json +++ b/ro-crate-metadata.json @@ -22,8 +22,8 @@ "@id": "./", "@type": "Dataset", "creativeWorkStatus": "InProgress", - "datePublished": "2025-10-16T13:39:07+00:00", - "description": "

    \n \n \n \"nf-core/stableexpression\"\n \n

    \n\n[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new/nf-core/stableexpression)\n[![GitHub Actions CI Status](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml/badge.svg)](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml)\n[![GitHub Actions Linting Status](https://github.com/nf-core/stableexpression/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/stableexpression/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/stableexpression/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.04.0-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-3.4.1-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/3.4.1)\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/stableexpression)\n\n[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23stableexpression-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/stableexpression)[![Follow on Bluesky](https://img.shields.io/badge/bluesky-%40nf__core-1185fe?labelColor=000000&logo=bluesky)](https://bsky.app/profile/nf-co.re)[![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/stableexpression** is a bioinformatics pipeline that ...\n\n\n\n\n2. 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/stableexpression \\\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/stableexpression/usage) and the [parameter documentation](https://nf-co.re/stableexpression/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/stableexpression/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/stableexpression/output).\n\n## Credits\n\nnf-core/stableexpression was originally written by Olivier Coen.\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 `#stableexpression` channel](https://nfcore.slack.com/channels/stableexpression) (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", + "datePublished": "2025-11-20T09:32:21+00:00", + "description": "

    \n \n \n \"nf-core/stableexpression\"\n \n

    \n\n[![Open in GitHub Codespaces](https://img.shields.io/badge/Open_In_GitHub_Codespaces-black?labelColor=grey&logo=github)](https://github.com/codespaces/new/nf-core/stableexpression)\n[![GitHub Actions CI Status](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml/badge.svg)](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml)\n[![GitHub Actions Linting Status](https://github.com/nf-core/stableexpression/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/stableexpression/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/stableexpression/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.04.0-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-3.5.1-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/3.5.1)\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/stableexpression)\n\n[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23stableexpression-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/stableexpression)[![Follow on Bluesky](https://img.shields.io/badge/bluesky-%40nf__core-1185fe?labelColor=000000&logo=bluesky)](https://bsky.app/profile/nf-co.re)[![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/stableexpression** is a bioinformatics pipeline that ...\n\n\n\n\n2. 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/stableexpression \\\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/stableexpression/usage) and the [parameter documentation](https://nf-co.re/stableexpression/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/stableexpression/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/stableexpression/output).\n\n## Credits\n\nnf-core/stableexpression was originally written by Olivier Coen.\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 `#stableexpression` channel](https://nfcore.slack.com/channels/stableexpression) (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", "hasPart": [ { "@id": "main.nf" @@ -99,7 +99,7 @@ }, "mentions": [ { - "@id": "#ee49bf85-7254-477b-a76d-88f5b94409cf" + "@id": "#ea87a9e0-dad4-4149-b745-000686183a2c" } ], "name": "nf-core/stableexpression" @@ -128,7 +128,7 @@ } ], "dateCreated": "", - "dateModified": "2025-10-16T13:39:07Z", + "dateModified": "2025-11-20T09:32:21Z", "dct:conformsTo": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE/", "keywords": ["nf-core", "nextflow", "expression", "housekeeping-genes", "qpcr-analysis"], "license": ["MIT"], @@ -160,11 +160,11 @@ "version": "!>=25.04.0" }, { - "@id": "#ee49bf85-7254-477b-a76d-88f5b94409cf", + "@id": "#ea87a9e0-dad4-4149-b745-000686183a2c", "@type": "TestSuite", "instance": [ { - "@id": "#60e3a4be-d59e-46f5-88ef-2dd82b8e7558" + "@id": "#7460c1e2-3fe8-4d1a-bffb-31ea3d133ade" } ], "mainEntity": { @@ -173,7 +173,7 @@ "name": "Test suite for nf-core/stableexpression" }, { - "@id": "#60e3a4be-d59e-46f5-88ef-2dd82b8e7558", + "@id": "#7460c1e2-3fe8-4d1a-bffb-31ea3d133ade", "@type": "TestInstance", "name": "GitHub Actions workflow for testing nf-core/stableexpression", "resource": "repos/nf-core/stableexpression/actions/workflows/nf-test.yml", diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index 4d7c04e2..0c910a6b 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -39,7 +39,7 @@ workflow PIPELINE_INITIALISATION { main: - ch_versions = Channel.empty() + ch_versions = channel.empty() // // Print version and exit if required and dump pipeline parameters to JSON file @@ -64,7 +64,7 @@ workflow PIPELINE_INITIALISATION { \033[0;35m nf-core/stableexpression ${workflow.manifest.version}\033[0m -\033[2m----------------------------------------------------\033[0m- """ - after_text = """${workflow.manifest.doi ? "\n* The pipeline\n" : ""}${workflow.manifest.doi.tokenize(",").collect { " https://doi.org/${it.trim().replace('https://doi.org/','')}"}.join("\n")}${workflow.manifest.doi ? "\n" : ""} + 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 @@ -96,7 +96,7 @@ workflow PIPELINE_INITIALISATION { // Create channel from input file provided through params.input // - Channel + channel .fromList(samplesheetToList(params.input, "${projectDir}/assets/schema_input.json")) .map { meta, fastq_1, fastq_2 -> diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/main.nf b/subworkflows/nf-core/utils_nfcore_pipeline/main.nf index bfd25876..2f30e9a4 100644 --- a/subworkflows/nf-core/utils_nfcore_pipeline/main.nf +++ b/subworkflows/nf-core/utils_nfcore_pipeline/main.nf @@ -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())) } // diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 4cc7667c..c6494c50 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -21,13 +21,31 @@ workflow STABLEEXPRESSION { ch_samplesheet // channel: samplesheet read in from --input main: - ch_versions = Channel.empty() - ch_multiqc_files = Channel.empty() + ch_versions = channel.empty() + ch_multiqc_files = channel.empty() // // Collate and save software versions // - softwareVersionsToYAML(ch_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')}" + } + + softwareVersionsToYAML(ch_versions.mix(topic_versions.versions_file)) + .mix(topic_versions_string) .collectFile( storeDir: "${params.outdir}/pipeline_info", name: 'nf_core_' + 'stableexpression_software_' + 'mqc_' + 'versions.yml', @@ -39,24 +57,24 @@ workflow STABLEEXPRESSION { // // MODULE: MultiQC // - ch_multiqc_config = Channel.fromPath( + 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() + channel.fromPath(params.multiqc_config, checkIfExists: true) : + channel.empty() ch_multiqc_logo = params.multiqc_logo ? - Channel.fromPath(params.multiqc_logo, checkIfExists: true) : - Channel.empty() + 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_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( + ch_methods_description = channel.value( methodsDescriptionText(ch_multiqc_custom_methods_description)) ch_multiqc_files = ch_multiqc_files.mix(ch_collated_versions) From 05abc9fe0dae68a23e96c86919eab391268a3e97 Mon Sep 17 00:00:00 2001 From: Olivier Date: Thu, 20 Nov 2025 11:32:28 +0100 Subject: [PATCH 173/258] prevent download of geo suppl data when multiple species are present in the series --- bin/download_geo_data.R | 52 ++++++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/bin/download_geo_data.R b/bin/download_geo_data.R index 5e921512..82719a6e 100755 --- a/bin/download_geo_data.R +++ b/bin/download_geo_data.R @@ -137,11 +137,41 @@ get_experiment_type <- function(geo_data) { } } +get_series_species <- function(geo_data) { + message("Getting species included in series") + species_list <- list() + for (i in 1:length(geo_data)) { + data <- geo_data[[ i ]] + metadata <- pData(data) + li <- unique(metadata$organism_ch1) + # check if organism_ch2 exists + if ("organism_ch2" %in% colnames(metadata)) { + li <- append(li, unique(metadata$organism_ch2)) + } + species_list[[i]] <- li + } + species_list <- unique(unlist(species_list)) + return(species_list) +} -get_series_supplementary_data <- function(geo_data) { - experiment_data <- get_experiment_data(geo_data) - suppl_data_str <- attr(experiment_data, "other")$supplementary_file - return(stringr::str_split(suppl_data_str, "\n")[[1]]) + +get_series_supplementary_data <- function(geo_data, series) { + series_species <- get_series_species(geo_data) + if (length(series_species) > 1) { + message(paste("Multiple species found in series:", paste(series_species, collapse = ", "), ". Will not download supplementary data")) + return(list()) + } else if (length(series_species) == 0) { + message("No species found in series...") + return(list()) + } else { + if (series_species != series$species) { + message(paste("Species provided by the user:", series_species, "does not match species in GEO data:", series$species)) + return(list()) + } + experiment_data <- get_experiment_data(geo_data) + suppl_data_str <- attr(experiment_data, "other")$supplementary_file + return(stringr::str_split(suppl_data_str, "\n")[[1]]) + } } @@ -175,7 +205,6 @@ get_rnaseq_samples <- function(geo_data, design_df) { } - ##################################################### ##################################################### # SAMPLE NAME MAPPING @@ -480,18 +509,23 @@ is_valid_rnaseq <- function(platform) { return(FALSE) } + return(TRUE) +} + + +check_rnaseq_normalised_state <- function(platform) { + # checking if all values are integers tryCatch({ is_all_integer <- function(x) all(floor(x) == x) int_counts <- platform$counts %>% select_if(is_all_integer) # if some values were not integers if (nrow(int_counts) < nrow(platform$counts)) { - write_warning(paste(platform$id, ": NOT ALL INTEGERS")) - return(FALSE) + return("normalised") } }, error = function(e) { write_warning(paste(platform$id, ": COULD NOT COMPUTE FLOOR")) - return(FALSE) + return("unknown") }) return(TRUE) @@ -634,7 +668,7 @@ main <- function() { series$experiment_type <- get_experiment_type(geo_data) - suppl_data_urls <- get_series_supplementary_data(geo_data) + suppl_data_urls <- get_series_supplementary_data(geo_data, series) # for now, considering suppl data as raw rnaseq data # TODO: check if these are always raw rnaseq data if (length(suppl_data_urls) > 0) { From 2c1b389d821b9e3fef0f2252866aa5e854d3e9e2 Mon Sep 17 00:00:00 2001 From: Olivier Date: Thu, 20 Nov 2025 12:45:48 +0100 Subject: [PATCH 174/258] separate the counts obtained in different supplementary file columns in different dataframes --- bin/download_geo_data.R | 164 ++++++++++++------- tests/modules/local/geo/getdata/main.nf.test | 29 +++- 2 files changed, 129 insertions(+), 64 deletions(-) diff --git a/bin/download_geo_data.R b/bin/download_geo_data.R index 82719a6e..0db91177 100755 --- a/bin/download_geo_data.R +++ b/bin/download_geo_data.R @@ -358,6 +358,7 @@ get_microarray_counts <- function(platform) { # get count data corresponding to samples in the design counts <- data.frame(exprs(platform$data)) %>% select(all_of(platform$design$sample)) + # for now, only one element in the list return(counts) } @@ -427,44 +428,63 @@ get_all_rnaseq_counts <- function(platform) { # getting list of samples samples <- pdata$geo_accession # getting list of columns corresponding to supp data - supplementary_cols <- grep("^supplementary_file(_\\d+)?$", names(pdata), value = TRUE) + # IMPORTANT: we assume here that data are of the same type (raw, TPM, FPKM, etc.) in each supplementary file column + supplementary_cols <- grep("^supplementary_file(_\\d)?$", names(pdata), value = TRUE) - count_df_list <- list() - cpt = 1 - for (i in 1:length(samples)) { - sample <- samples[[i]] + if (length(supplementary_cols) == 0) { + message("No supplementary files found") + return(data.frame()) + } else if (length(supplementary_cols) > 1) { + message("Multiple supplementary files found") + } + + suppl_df_cpt <- 1 + suppl_count_dfs <- list() + # building one count dataframe by type of suppl data + for (i in 1:length(supplementary_cols)) { + + count_df_list <- list() + cpt = 1 + for (j in 1:length(samples)) { + sample <- samples[[j]] + data_url <- pdata[pdata$geo_accession == sample, supplementary_cols[i]] - for (j in 1:length(supplementary_cols)) { - data_url <- pdata[pdata$geo_accession == sample, supplementary_cols[j]] counts <- get_raw_counts_from_url(data_url) if (is.null(counts)) { next } - # if only one column + if (ncol(counts) == 1) { colnames(counts) <- c(sample) + } else { + # if multiple columns, we don't know how to deal with it + # nut it will be filtered out later at column match checking + message(paste("Multiple columns found for sample", sample)) } + counts <- tibble::rownames_to_column(counts, var = "gene_id") # adding to list count_df_list[[cpt]] <- counts cpt = cpt + 1 } - } - # checking if all files were skipped - if (length(count_df_list) == 0) { - message("No valid files found") - return(data.frame()) - } + # checking if all files were skipped + if (length(count_df_list) == 0) { + message("No valid files found") + return(data.frame()) + } - # full outer join - joined_df <- Reduce( - function(df1, df2) merge(df1, df2, by = "gene_id", all = TRUE), - count_df_list - ) - joined_df <- tibble::column_to_rownames(joined_df, var = "gene_id") + # full outer join + joined_df <- Reduce( + function(df1, df2) merge(df1, df2, by = "gene_id", all = TRUE), + count_df_list + ) + joined_df <- tibble::column_to_rownames(joined_df, var = "gene_id") - return(joined_df) + suppl_count_dfs[[suppl_df_cpt]] <- joined_df + suppl_df_cpt = suppl_df_cpt + 1 + } + return(suppl_count_dfs) } @@ -474,14 +494,14 @@ get_all_rnaseq_counts <- function(platform) { ##################################################### ##################################################### -is_valid_microarray <- function(platform) { +is_valid_microarray <- function(counts, platform) { - if (!all(colnames(platform$counts) %in% platform$design$sample)) { + if (!all(colnames(counts) %in% platform$design$sample)) { message("Column names do not match samples in design") return(FALSE) } - vals <- unlist(platform$counts, use.names = FALSE) + vals <- unlist(counts, use.names = FALSE) vals <- vals[!is.na(vals)] all_integers <- all(abs(vals - round(vals)) < 1e-8) @@ -502,9 +522,9 @@ is_valid_microarray <- function(platform) { } } -is_valid_rnaseq <- function(platform) { +is_valid_rnaseq <- function(counts, platform) { - if (!all(colnames(platform$counts) %in% platform$design$sample)) { + if (!all(colnames(counts) %in% platform$design$sample)) { message(paste(platform$id, ": column names do not match samples in design")) return(FALSE) } @@ -513,22 +533,28 @@ is_valid_rnaseq <- function(platform) { } -check_rnaseq_normalised_state <- function(platform) { +check_rnaseq_normalisation_state <- function(counts, platform) { # checking if all values are integers tryCatch({ is_all_integer <- function(x) all(floor(x) == x) - int_counts <- platform$counts %>% select_if(is_all_integer) - # if some values were not integers - if (nrow(int_counts) < nrow(platform$counts)) { + int_counts <- counts %>% + select_if(is_all_integer) + + # if all or the majority of values are decimals + if (nrow(int_counts) < nrow(counts) * 0.01 ) { return("normalised") + } else if (nrow(int_counts) == nrow(counts)) { + return("raw") + } else { + return("unknown") } + }, error = function(e) { write_warning(paste(platform$id, ": COULD NOT COMPUTE FLOOR")) return("unknown") }) - return(TRUE) } @@ -538,29 +564,29 @@ check_rnaseq_normalised_state <- function(platform) { ##################################################### ##################################################### -export_count_data <- function(platform, series) { +export_count_data <- function(data, platform, series) { # renaming columns, to make them specific to accession and data type - colnames(platform$counts) <- paste0(series$accession, '_', colnames(platform$counts)) - outfilename <- paste0(series$accession, '_', platform$id, '.', platform$type, '.', platform$count_type, COUNT_FILE_EXTENSION) - if (!platform$is_valid) { + colnames(data$counts) <- paste0(series$accession, '_', colnames(data$counts)) + outfilename <- paste0(series$accession, '_', platform$id, '.', platform$type, '.', data$norm_state, COUNT_FILE_EXTENSION) + if (!data$is_valid) { outfilename <- file.path(get_rejected_dir(platform, series), outfilename) } # exporting to CSV file # index represents gene names message(paste(platform$id, ': exporting count data to file', outfilename)) - write.table(platform$counts, outfilename, sep = ',', row.names = TRUE, col.names = TRUE, quote = FALSE) + write.table(data$counts, outfilename, sep = ',', row.names = TRUE, col.names = TRUE, quote = FALSE) } -export_design <- function(platform, series) { +export_design <- function(data, platform, series) { new_sample_names <- paste0(series$accession, '_', series$design$sample) design_df <- series$design %>% mutate(sample = new_sample_names ) %>% select(sample, condition, batch) - outfilename <- paste0(series$accession, '_', platform$id, '.', platform$type,'.', platform$count_type, DESIGN_FILE_EXTENSION) - if (!platform$is_valid) { + outfilename <- paste0(series$accession, '_', platform$id, '.', platform$type,'.', data$norm_state, DESIGN_FILE_EXTENSION) + if (!data$is_valid) { outfilename <- file.path(get_rejected_dir(platform, series), outfilename) } @@ -569,18 +595,18 @@ export_design <- function(platform, series) { } -export_name_mapping <- function(platform, series) { - outfilename <- paste0(series$accession, '_', platform$id, '.', platform$type, '.', platform$count_type, MAPPING_FILE_EXTENSION) - if (!platform$is_valid) { +export_name_mapping <- function(data, platform, series) { + outfilename <- paste0(series$accession, '_', platform$id, '.', platform$type, '.', data$norm_state, MAPPING_FILE_EXTENSION) + if (!data$is_valid) { outfilename <- file.path(get_rejected_dir(platform, series), outfilename) } message(paste(platform$id, ': exporting design data to file', outfilename)) write.table(series$mapping, outfilename, sep = ',', row.names = FALSE, col.names = TRUE, quote = FALSE) } -export_metadata <- function(platform, series) { - outfilename <- paste0(series$accession, '_', platform$id, '.', platform$type, '.', platform$count_type, METADATA_FILE_EXTENSION) - if (!platform$is_valid) { +export_metadata <- function(data, platform, series) { + outfilename <- paste0(series$accession, '_', platform$id, '.', platform$type, '.', data$norm_state, METADATA_FILE_EXTENSION) + if (!data$is_valid) { outfilename <- file.path(get_rejected_dir(platform, series), outfilename) } message(paste(platform$id, ': exporting metadata to file', outfilename)) @@ -594,20 +620,20 @@ export_metadata <- function(platform, series) { ##################################################### ##################################################### -post_process_and_export <- function(platform, series) { +post_process_and_export <- function(data, platform, series) { # keeping only non empty data - if (nrow(platform$counts) == 0 || ncol(platform$counts) == 0) { + if (nrow(data$counts) == 0 || ncol(data$counts) == 0) { message(paste(platform$id, ': no data found')) write_warning(paste(platform$id, ": NO DATA")) return(NULL) } # rename columns when needed - platform$counts <- rename_columns(platform$counts, series$mapping) + counts <- rename_columns(counts, series$mapping) - export_count_data(platform, series) - export_design(platform, series) - export_name_mapping(platform, series) - export_metadata(platform, series) + export_count_data(data, platform, series) + export_design(data, platform, series) + export_name_mapping(data, platform, series) + export_metadata(data, platform, series) } @@ -624,14 +650,27 @@ process_platform_data <- function(platform, series) { } if (platform$type == "microarray") { - platform$counts <- get_microarray_counts(platform) - platform$is_valid <- is_valid_microarray(platform) + + counts <- get_microarray_counts(platform) + data <- list( counts = counts ) + data$is_valid <- is_valid_microarray(counts, platform) + data$norm_state <- "normalised" + post_process_and_export(data, platform, series) + } else { - platform$counts <- get_all_rnaseq_counts(platform) - platform$is_valid <- is_valid_rnaseq(platform) + + parsed_counts <- get_all_rnaseq_counts(platform) + for (counts in parsed_counts) { + data <- list( + counts = counts, + is_valid = is_valid_rnaseq(counts, platform), + norm_state = check_rnaseq_normalisation_state(counts, platform) + ) + post_process_and_export(data, platform, series) + } + } - post_process_and_export(platform, series) } @@ -682,12 +721,14 @@ main <- function() { platform <- list( type = "rnaseq", id = "suppl", - count_type = "raw", - counts = counts, design = series$design ) - platform$is_valid <- is_valid_rnaseq(platform) - post_process_and_export(platform, series) + data <- list( + counts = counts, + is_valid = is_valid_rnaseq(counts, platform), + norm_state = check_rnaseq_normalisation_state(counts, platform) + ) + post_process_and_export(data, platform, series) } } @@ -700,7 +741,6 @@ main <- function() { for (i in 1:length(geo_data)) { platform <- list( type = "microarray", - count_type = "normalised", data = geo_data[[ i ]] ) process_platform_data(platform, series) diff --git a/tests/modules/local/geo/getdata/main.nf.test b/tests/modules/local/geo/getdata/main.nf.test index b6d19420..a952a1a0 100644 --- a/tests/modules/local/geo/getdata/main.nf.test +++ b/tests/modules/local/geo/getdata/main.nf.test @@ -52,7 +52,7 @@ nextflow_process { } } - +/* test("Drosophila simulans - Only one sample among several") { when { @@ -76,7 +76,7 @@ nextflow_process { } } - +*/ test("Drosophila simulans - No data found") { when { @@ -173,4 +173,29 @@ nextflow_process { } + test("Drosophila simulans - Only series suppl data but multiple species") { + + when { + + process { + """ + input[0] = [ + [ id: "test" ], + "GSE274048" + ] + input[1] = "drosophila_simulans" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert process.out.counts.size() == 0 }, + { assert snapshot(process.out).match() } + ) + } + + } + } From 857e85ab4f9ef47fe2cc5916af597efa74846b35 Mon Sep 17 00:00:00 2001 From: Olivier Date: Thu, 20 Nov 2025 13:10:28 +0100 Subject: [PATCH 175/258] fix duplicated column names --- bin/download_geo_data.R | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/bin/download_geo_data.R b/bin/download_geo_data.R index 0db91177..53143c7e 100755 --- a/bin/download_geo_data.R +++ b/bin/download_geo_data.R @@ -75,6 +75,16 @@ get_rejected_dir <- function(platform, series) { return(rejected_dir) } + +clean_column_names <- function(df){ + + if (length(unique(colnames(df))) < length(colnames(df))){ + colnames(df) <- paste0(colnames(df), '_', seq_along(df)) + return(df) + } +} + + ##################################################### ##################################################### # DOWNLOAD @@ -462,6 +472,7 @@ get_all_rnaseq_counts <- function(platform) { message(paste("Multiple columns found for sample", sample)) } + # setting the row names (gene ids) as a column counts <- tibble::rownames_to_column(counts, var = "gene_id") # adding to list count_df_list[[cpt]] <- counts @@ -479,7 +490,11 @@ get_all_rnaseq_counts <- function(platform) { function(df1, df2) merge(df1, df2, by = "gene_id", all = TRUE), count_df_list ) + # setting the column gene_id as row names joined_df <- tibble::column_to_rownames(joined_df, var = "gene_id") + # cleaning column names in case of duplicates + # it should happen only when there were multiple columns for the same sample + joined_df <- clean_column_names(joined_df) suppl_count_dfs[[suppl_df_cpt]] <- joined_df suppl_df_cpt = suppl_df_cpt + 1 @@ -551,6 +566,8 @@ check_rnaseq_normalisation_state <- function(counts, platform) { } }, error = function(e) { + print(head(counts)) + print(e) write_warning(paste(platform$id, ": COULD NOT COMPUTE FLOOR")) return("unknown") }) From f7cc9435a9425a3d1f87f85ce7c2c2d5b191ed8c Mon Sep 17 00:00:00 2001 From: Olivier Date: Thu, 20 Nov 2025 18:08:05 +0100 Subject: [PATCH 176/258] spearate idmapping step in 3 substeps to make it faster and more scalable; add parameter to change the g:Profiler target database --- assets/multiqc_config.yml | 37 +++-- assets/schema_gene_id_mapping.json | 6 +- assets/schema_gene_metadata.json | 6 +- bin/aggregate_results.py | 26 ++-- bin/clean_count_data.py | 12 +- bin/collect_gene_ids.py | 62 +++++++++ bin/compute_base_statistics.py | 14 +- bin/compute_m_measures.py | 18 +-- bin/compute_stability_scores.py | 6 +- bin/config.py | 3 +- bin/get_candidate_genes.py | 8 +- bin/get_dataset_statistics.py | 8 +- bin/get_gene_lengths.py | 20 +-- bin/get_ratio_standard_variation.py | 35 +++-- bin/gprofiler_map_ids.py | 117 ++++++++++++++++ bin/gprofiler_utils.py | 21 +-- bin/make_pairwise_gene_expression_ratio.py | 12 +- bin/make_parquet_chunks.py | 14 +- bin/merge_counts.py | 30 ++-- bin/normalise_to_tpm.py | 67 +++++---- bin/normfinder.py | 9 +- bin/quantile_normalise.py | 4 +- ...p_ids_to_ensembl.py => rename_gene_ids.py} | 128 +++++++----------- docs/output.md | 8 +- docs/usage.md | 16 +-- galaxy/build/static/boilerplate.xml | 6 +- .../tool/nf_core_stableexpression.xml | 10 +- .../tool/test_data/microarray.normalised.csv | 2 +- modules/local/collect_gene_ids/main.nf | 25 ++++ modules/local/collect_gene_ids/spec-file.txt | 45 ++++++ .../local/dash_app/app/src/callbacks/genes.py | 7 +- .../dash_app/app/src/components/tables.py | 5 +- .../local/dash_app/app/src/utils/config.py | 2 +- .../dash_app/app/src/utils/data_management.py | 12 +- modules/local/gprofiler/idmapping/main.nf | 32 ++--- modules/local/rename_gene_ids/main.nf | 41 ++++++ modules/local/rename_gene_ids/spec-file.txt | 57 ++++++++ nextflow.config | 1 + nextflow_schema.json | 12 +- subworkflows/local/idmapping/main.nf | 46 ++++--- subworkflows/local/merge_data/main.nf | 8 +- subworkflows/local/multiqc/main.nf | 16 ++- tests/modules/local/geo/getdata/main.nf.test | 25 ++++ .../local/gprofiler/idmapping/main.nf.test | 31 +++++ .../gprofiler/idmapping/main.nf.test.snap | 43 ++++++ .../main.nf.test | 15 +- .../main.nf.test.snap | 0 tests/subworkflows/local/genorm/run_genorm.py | 7 +- tests/test_data/aggregate_results/mapping.csv | 2 +- .../test_data/aggregate_results/metadata.csv | 2 +- .../microarray_stats_all_genes.csv | 2 +- .../rnaseq_stats_all_genes.csv | 2 +- .../output/stats_all_genes.csv | 2 +- .../input/mapping1.csv | 2 +- .../input/mapping2.csv | 2 +- .../input/mapping3.csv | 2 +- .../input/metadata1.csv | 2 +- .../input/metadata2.csv | 2 +- .../input/microarray_stats_all_genes.csv | 2 +- .../input/rnaseq_stats_all_genes.csv | 2 +- .../input/genorm.m_measures.csv | 2 +- .../input/stability_values.normfinder.csv | 2 +- .../input/stats_all_genes.csv | 2 +- tests/test_data/idmapping/custom/mapping.csv | 2 +- tests/test_data/idmapping/custom/metadata.csv | 2 +- .../test_data/idmapping/gene_ids/gene_ids.txt | 9 ++ tests/test_data/idmapping/tsv/mapping.tsv | 2 +- tests/test_data/idmapping/tsv/metadata.tsv | 2 +- tests/test_data/input_datasets/mapping.csv | 2 +- tests/test_data/input_datasets/metadata.csv | 2 +- .../input_datasets/microarray.normalised.csv | 2 +- tests/test_data/input_datasets/rnaseq.raw.csv | 2 +- .../merge_data/output/all_counts.csv | 2 +- .../normfinder/very_small_cq/normfinder.R | 4 +- workflows/stableexpression.nf | 26 ++-- 75 files changed, 838 insertions(+), 384 deletions(-) create mode 100755 bin/collect_gene_ids.py create mode 100755 bin/gprofiler_map_ids.py rename bin/{map_ids_to_ensembl.py => rename_gene_ids.py} (54%) create mode 100644 modules/local/collect_gene_ids/main.nf create mode 100644 modules/local/collect_gene_ids/spec-file.txt create mode 100644 modules/local/rename_gene_ids/main.nf create mode 100644 modules/local/rename_gene_ids/spec-file.txt create mode 100644 tests/modules/local/gprofiler/idmapping/main.nf.test create mode 100644 tests/modules/local/gprofiler/idmapping/main.nf.test.snap rename tests/modules/local/{idmapping/gprofiler => rename_gene_ids}/main.nf.test (91%) rename tests/modules/local/{idmapping/gprofiler => rename_gene_ids}/main.nf.test.snap (100%) create mode 100644 tests/test_data/idmapping/gene_ids/gene_ids.txt diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index e8a971c6..cb704af3 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -41,13 +41,11 @@ custom_data: Genes are sorted by stability score - from the most stable to the least stable. plot_type: "table" pconfig: - col1_header: "Ensembl Gene ID" + col1_header: "Gene ID" sort_rows: false headers: - ensembl_gene_id: - title: "Ensembl Gene ID" - description: | - Gene IDs as shown in Ensembl + gene_id: + title: "Gene ID" rank: title: "Rank" description: | @@ -69,13 +67,9 @@ custom_data: first: - eq: 1 name: - title: "Ensembl name" - description: | - Gene name as shown in Ensembl (g:Profiler) + title: "Gene name" description: - title: "Ensembl description" - description: | - Gene description as shown in Ensembl (g:Profiler) + title: "Gene description" original_gene_ids: title: "Original gene IDs" description: | @@ -581,7 +575,18 @@ custom_data: Warnings during download of GEO datasets plot_type: "table" - id_mapping_failure_reasons: + renaming_warning_reasons: + section_name: "Warning reasons" + parent_id: idmapping + parent_name: "ID mapping" + parent_description: "Information about the ID mapping" + file_format: "tsv" + no_violin: true + description: | + Reasons of warning during gene ID renaming + plot_type: "table" + + renaming_failure_reasons: section_name: "Failure reasons" parent_id: idmapping parent_name: "ID mapping" @@ -589,7 +594,7 @@ custom_data: file_format: "tsv" no_violin: true description: | - Reasons of failure during ID mapping + Reasons of failure during gene ID renaming plot_type: "table" normalisation_failure_reasons: @@ -663,8 +668,10 @@ sp: fn: "*geo_failure_reasons.csv" geo_warning_reasons: fn: "*geo_warning_reasons.csv" - id_mapping_failure_reasons: - fn: "*id_mapping_failure_reasons.tsv" + renaming_warning_reasons: + fn: "*renaming_warning_reasons.tsv" + renaming_failure_reasons: + fn: "*renaming_failure_reasons.tsv" normalisation_failure_reasons: fn: "*normalisation_failure_reasons.csv" normalisation_warning_reasons: diff --git a/assets/schema_gene_id_mapping.json b/assets/schema_gene_id_mapping.json index f484ef45..ef9467f3 100644 --- a/assets/schema_gene_id_mapping.json +++ b/assets/schema_gene_id_mapping.json @@ -12,12 +12,12 @@ "pattern": "^\\S+$", "errorMessage": "You must provide a column for original gene IDs." }, - "ensembl_gene_id": { + "gene_id": { "type": "string", "pattern": "^\\S+$", - "errorMessage": "You must provide a column for mapped IDs (ensembl gene IDs)." + "errorMessage": "You must provide a column for mapped IDs." } }, - "required": ["original_gene_id", "ensembl_gene_id"] + "required": ["original_gene_id", "gene_id"] } } diff --git a/assets/schema_gene_metadata.json b/assets/schema_gene_metadata.json index e03bcfce..7fcddc17 100644 --- a/assets/schema_gene_metadata.json +++ b/assets/schema_gene_metadata.json @@ -7,10 +7,10 @@ "items": { "type": "object", "properties": { - "ensembl_gene_id": { + "gene_id": { "type": "string", "pattern": "^\\S+$", - "errorMessage": "You must provide a column for mapped IDs (ensembl gene IDs)." + "errorMessage": "You must provide a column for mapped IDs." }, "name": { "type": "string", @@ -23,6 +23,6 @@ "errorMessage": "You must provide a column for gene descriptions." } }, - "required": ["ensembl_gene_id", "name", "description"] + "required": ["gene_id", "name", "description"] } } diff --git a/bin/aggregate_results.py b/bin/aggregate_results.py index ef22fc10..9767ead6 100755 --- a/bin/aggregate_results.py +++ b/bin/aggregate_results.py @@ -117,18 +117,16 @@ def concat_cast_to_string_and_drop_duplicates(files: list[Path]) -> pl.LazyFrame def get_count_columns(lf: pl.LazyFrame) -> list[str]: - """Get all column names except the ENSEMBL_GENE_ID column. + """Get all column names except the GENE_ID column. - The ENSEMBL_GENE_ID column contains only gene IDs. + The GENE_ID column contains only gene IDs. """ - return ( - lf.select(pl.exclude(config.ENSEMBL_GENE_ID_COLNAME)).collect_schema().names() - ) + return lf.select(pl.exclude(config.GENE_ID_COLNAME)).collect_schema().names() def cast_count_columns_to_float32(lf: pl.LazyFrame) -> pl.LazyFrame: return lf.select( - [pl.col(config.ENSEMBL_GENE_ID_COLNAME)] + [pl.col(config.GENE_ID_COLNAME)] + [pl.col(column).cast(pl.Float32) for column in get_count_columns(lf)] ) @@ -137,13 +135,13 @@ def join_data_on_gene_id(stat_lf: pl.LazyFrame, *lfs: pl.LazyFrame) -> pl.LazyFr """Merge the statistics dataframe with the metadata dataframe and the mapping dataframe.""" # we need to ensure that the index of stat_lf are strings for lf in lfs: - stat_lf = stat_lf.join(lf, on=config.ENSEMBL_GENE_ID_COLNAME, how="left") + stat_lf = stat_lf.join(lf, on=config.GENE_ID_COLNAME, how="left") return stat_lf def get_counts(file: Path) -> pl.LazyFrame: # sorting dataframe (necessary to get consistent output) - return pl.scan_parquet(file).sort(config.ENSEMBL_GENE_ID_COLNAME, descending=False) + return pl.scan_parquet(file).sort(config.GENE_ID_COLNAME, descending=False) def get_metadata(metadata_files: list[Path]) -> pl.LazyFrame | None: @@ -160,7 +158,7 @@ def get_mappings(mapping_files: list[Path]) -> pl.LazyFrame | None: # group by new gene IDs and gets the lis # convert the list column to a string representation # separate the original gene IDs with a semicolon - return concat_lf.group_by(config.ENSEMBL_GENE_ID_COLNAME).agg( + return concat_lf.group_by(config.GENE_ID_COLNAME).agg( pl.col(config.ORIGINAL_GENE_ID_COLNAME) .unique() .sort() @@ -214,25 +212,23 @@ def get_top_stable_genes_counts( # getting list of top stable genes with their order top_genes_with_order = ( stat_summary_df.head(NB_TOP_GENES_TO_SHOW_IN_BOX_PLOTS) - .select(config.ENSEMBL_GENE_ID_COLNAME) + .select(config.GENE_ID_COLNAME) .with_row_index("sort_order") ) # join to get only existing genes and maintain order sorted_transposed_counts_df = ( log_count_lf.join( - top_genes_with_order, on=config.ENSEMBL_GENE_ID_COLNAME, how="inner" + top_genes_with_order, on=config.GENE_ID_COLNAME, how="inner" ).sort("sort_order", descending=False) ).collect() # get the actual gene names that were found (in order) actual_gene_names = ( - sorted_transposed_counts_df.select(config.ENSEMBL_GENE_ID_COLNAME) - .to_series() - .to_list() + sorted_transposed_counts_df.select(config.GENE_ID_COLNAME).to_series().to_list() ) return sorted_transposed_counts_df.drop( - ["sort_order", config.ENSEMBL_GENE_ID_COLNAME] + ["sort_order", config.GENE_ID_COLNAME] ).transpose(column_names=actual_gene_names) diff --git a/bin/clean_count_data.py b/bin/clean_count_data.py index 901debfe..333c1040 100755 --- a/bin/clean_count_data.py +++ b/bin/clean_count_data.py @@ -51,20 +51,18 @@ def parse_args(): def get_count_columns(lf: pl.LazyFrame) -> list[str]: - """Get all column names except the config.ENSEMBL_GENE_ID_COLNAME column. + """Get all column names except the config.GENE_ID_COLNAME column. - The config.ENSEMBL_GENE_ID_COLNAME column contains only gene IDs. + The config.GENE_ID_COLNAME column contains only gene IDs. """ - return ( - lf.select(pl.exclude(config.ENSEMBL_GENE_ID_COLNAME)).collect_schema().names() - ) + return lf.select(pl.exclude(config.GENE_ID_COLNAME)).collect_schema().names() def get_counts( file: Path, ) -> pl.DataFrame: # sorting dataframe (necessary to get consistent output) - return pl.read_parquet(file).sort(config.ENSEMBL_GENE_ID_COLNAME, descending=False) + return pl.read_parquet(file).sort(config.GENE_ID_COLNAME, descending=False) def remove_samples_with_low_ks_pvalue( @@ -100,7 +98,7 @@ def remove_samples_with_low_ks_pvalue( sys.exit(0) # filtering the count dataframe to keep only the valid samples - return count_lf.select([config.ENSEMBL_GENE_ID_COLNAME] + valid_samples) + return count_lf.select([config.GENE_ID_COLNAME] + valid_samples) def export_data(all_counts_lf: pl.DataFrame): diff --git a/bin/collect_gene_ids.py b/bin/collect_gene_ids.py new file mode 100755 index 00000000..20588fbc --- /dev/null +++ b/bin/collect_gene_ids.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 + +# Written by Olivier Coen. Released under the MIT license. + +import argparse +import logging +from pathlib import Path + +import pandas as pd +from tqdm import tqdm + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +ALL_GENE_IDS_OUTFILE = "all_gene_ids.txt" + + +##################################################### +##################################################### +# FUNCTIONS +##################################################### +##################################################### + + +def parse_args(): + parser = argparse.ArgumentParser(description="Merge count datasets") + parser.add_argument( + "--counts", type=str, dest="count_files", required=True, help="Count files" + ) + return parser.parse_args() + + +##################################################### +##################################################### +# MAIN +##################################################### +##################################################### + + +def parse_table(file: Path): + if file.suffix == ".csv": + return pd.read_csv(file, header=0, index_col=0) + else: # .tsv + return pd.read_csv(file, header=0, index_col=0, sep="\t") + + +def main(): + args = parse_args() + count_files = [Path(file) for file in args.count_files.split(" ")] + logger.info(f"Getting gene IDs from {len(count_files)} count files") + + all_gene_ids = set() + for count_file in tqdm(count_files): + df = parse_table(count_file) + all_gene_ids.update(list(df.index)) + + with open(ALL_GENE_IDS_OUTFILE, "w") as f: + f.write("\n".join(list(all_gene_ids))) + + +if __name__ == "__main__": + main() diff --git a/bin/compute_base_statistics.py b/bin/compute_base_statistics.py index ba8d9e91..cbbdba63 100755 --- a/bin/compute_base_statistics.py +++ b/bin/compute_base_statistics.py @@ -74,7 +74,7 @@ class GeneStatistician: def __post_init__(self): self.gene_count_per_sample_df = self.get_gene_counts_per_sample() self.samples = ( - self.count_lf.select(pl.exclude(config.ENSEMBL_GENE_ID_COLNAME)) + self.count_lf.select(pl.exclude(config.GENE_ID_COLNAME)) .collect_schema() .names() ) @@ -84,7 +84,7 @@ def get_colname(self, colname: str) -> str: return f"{self.platform}_{colname}" if self.platform else colname def get_valid_counts(self) -> pl.LazyFrame: - return self.count_lf.select(pl.exclude(config.ENSEMBL_GENE_ID_COLNAME)) + return self.count_lf.select(pl.exclude(config.GENE_ID_COLNAME)) def get_gene_counts_per_sample(self) -> pl.DataFrame: """ @@ -95,7 +95,7 @@ def get_gene_counts_per_sample(self) -> pl.DataFrame: - nb_not_nulls: number of non-null values """ return ( - self.count_lf.select(pl.exclude(config.ENSEMBL_GENE_ID_COLNAME)) + self.count_lf.select(pl.exclude(config.GENE_ID_COLNAME)) .count() .collect() .transpose( @@ -131,7 +131,7 @@ def get_main_statistics(self) -> pl.LazyFrame: ) return augmented_count_lf.select( - pl.col(config.ENSEMBL_GENE_ID_COLNAME), + pl.col(config.GENE_ID_COLNAME), pl.col("mean").alias(self.get_colname(config.MEAN_COLNAME)), pl.col("std").alias(self.get_colname(config.STANDARD_DEVIATION_COLNAME)), pl.col("median").alias(self.get_colname(config.MEDIAN_COLNAME)), @@ -153,7 +153,7 @@ def compute_ratios_null_values(self): ] nb_nulls = ( - self.count_lf.select(pl.exclude(config.ENSEMBL_GENE_ID_COLNAME).is_null()) + self.count_lf.select(pl.exclude(config.GENE_ID_COLNAME).is_null()) .collect() .sum_horizontal() ) @@ -174,7 +174,7 @@ def compute_ratios_null_values(self): def compute_ratio_zeros(self): nb_zeros = ( - self.count_lf.select(pl.exclude(config.ENSEMBL_GENE_ID_COLNAME) == 0) + self.count_lf.select(pl.exclude(config.GENE_ID_COLNAME) == 0) .collect() .sum_horizontal() ) @@ -240,7 +240,7 @@ def parse_args(): def get_counts(file: Path) -> pl.LazyFrame: # sorting dataframe (necessary to get consistent output) - return pl.scan_parquet(file).sort(config.ENSEMBL_GENE_ID_COLNAME, descending=False) + return pl.scan_parquet(file).sort(config.GENE_ID_COLNAME, descending=False) def export_data(stat_lf: pl.LazyFrame, platform: str | None): diff --git a/bin/compute_m_measures.py b/bin/compute_m_measures.py index 35a2ce44..bc1d783e 100755 --- a/bin/compute_m_measures.py +++ b/bin/compute_m_measures.py @@ -60,14 +60,14 @@ def concat_all_std_data(files: list[Path], low_memory: bool) -> pl.LazyFrame: lf = pl.concat(lfs) return ( lf.explode(config.RATIOS_STD_COLNAME) - .group_by(config.ENSEMBL_GENE_ID_COLNAME) + .group_by(config.GENE_ID_COLNAME) .agg(pl.col(config.RATIOS_STD_COLNAME)) ) def compute_m_measures(lf: pl.LazyFrame) -> pl.LazyFrame: return lf.select( - pl.col(config.ENSEMBL_GENE_ID_COLNAME), + pl.col(config.GENE_ID_COLNAME), ( pl.col(config.RATIOS_STD_COLNAME).list.sum() / (pl.col(config.RATIOS_STD_COLNAME).list.len() - 1) @@ -100,9 +100,7 @@ def main(): ############################################################################# # MAKING A FOLDER FOR EACH CHUNK OF GENE IDS ############################################################################# - gene_ids = ( - count_lf.select(config.ENSEMBL_GENE_ID_COLNAME).collect().to_series().to_list() - ) + gene_ids = count_lf.select(config.GENE_ID_COLNAME).collect().to_series().to_list() gene_ids = sorted(gene_ids) chunksize = max( @@ -137,7 +135,7 @@ def main(): # writing all data corresponding to this group of gene IDs in a specific folder outfile = gene_id_chunk_folder / f"chunk.{i}.parquet" concat_df = concat_lf.filter( - pl.col(config.ENSEMBL_GENE_ID_COLNAME).is_in(gene_id_list_chunk) + pl.col(config.GENE_ID_COLNAME).is_in(gene_id_list_chunk) ).collect() concat_df.write_parquet(outfile) @@ -154,7 +152,7 @@ def main(): chunk_files = list(gene_id_chunk_folder.iterdir()) concat_lf = concat_all_std_data(chunk_files, low_memory).sort( - config.ENSEMBL_GENE_ID_COLNAME + config.GENE_ID_COLNAME ) # computing M measures for these gene IDs @@ -164,13 +162,11 @@ def main(): ################################################# # checks ################################################# - if m_measure_df[config.ENSEMBL_GENE_ID_COLNAME].is_duplicated().any(): + if m_measure_df[config.GENE_ID_COLNAME].is_duplicated().any(): raise ValueError("Duplicate values found for gene IDs!") process_gene_ids = sorted( - m_measure_df.select(config.ENSEMBL_GENE_ID_COLNAME) - .to_series() - .to_list() + m_measure_df.select(config.GENE_ID_COLNAME).to_series().to_list() ) if process_gene_ids != gene_id_list_chunks[i]: raise ValueError("Incorrect gene IDs found!") diff --git a/bin/compute_stability_scores.py b/bin/compute_stability_scores.py index 2da538fe..d26e9b60 100755 --- a/bin/compute_stability_scores.py +++ b/bin/compute_stability_scores.py @@ -195,7 +195,7 @@ def get_stabilities(stability_files: list[Path]) -> pl.LazyFrame: if len(stability_files) > 1: for file in stability_files[1:]: new_df = pl.scan_csv(file) - lf = lf.join(new_df, on=config.ENSEMBL_GENE_ID_COLNAME, how="left") + lf = lf.join(new_df, on=config.GENE_ID_COLNAME, how="left") return lf.with_columns(pl.lit(1).alias(config.IS_CANDIDATE_COLNAME)) @@ -205,7 +205,7 @@ def get_statistics(stat_files: list[Path]) -> pl.LazyFrame: if len(stat_files) > 1: for file in stat_files[1:]: new_df = pl.scan_csv(file) - lf = lf.join(new_df, on=config.ENSEMBL_GENE_ID_COLNAME, how="left") + lf = lf.join(new_df, on=config.GENE_ID_COLNAME, how="left") return lf @@ -240,7 +240,7 @@ def main(): # getting metadata and mappings stability_lf = get_stabilities(stability_files) # merges base statistics with computed stability measurements - lf = stat_lf.join(stability_lf, on=config.ENSEMBL_GENE_ID_COLNAME, how="left") + lf = stat_lf.join(stability_lf, on=config.GENE_ID_COLNAME, how="left") # sort genes according to the metrics present in the dataframe stability_scorer = StabilityScorer(lf.collect(), args.stability_score_weights) diff --git a/bin/config.py b/bin/config.py index 81ab465f..4326c38d 100644 --- a/bin/config.py +++ b/bin/config.py @@ -1,5 +1,6 @@ # general column names -ENSEMBL_GENE_ID_COLNAME = "ensembl_gene_id" +GENE_ID_COLNAME = "gene_id" +CDNA_LENGTH_COLNAME = "cdna_length" RANK_COLNAME = "rank" # base statistics diff --git a/bin/get_candidate_genes.py b/bin/get_candidate_genes.py index 72763333..fb7a3349 100755 --- a/bin/get_candidate_genes.py +++ b/bin/get_candidate_genes.py @@ -3,11 +3,11 @@ # Written by Olivier Coen. Released under the MIT license. import argparse -import polars as pl -from pathlib import Path import logging +from pathlib import Path import config +import polars as pl logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -75,7 +75,7 @@ def get_best_candidates( return ( stat_lf.sort(column_for_sorting, descending=False, nulls_last=True) .head(nb_top_stable_genes) - .select(config.ENSEMBL_GENE_ID_COLNAME) + .select(config.GENE_ID_COLNAME) .collect() .to_series() .to_list() @@ -108,7 +108,7 @@ def filter_out_low_expression_genes( def get_counts_for_candidates(file: Path, best_candidates: list[str]) -> pl.DataFrame: logger.info("Getting counts for candidate genes") return pl.read_parquet(file).filter( - pl.col(config.ENSEMBL_GENE_ID_COLNAME).is_in(best_candidates) + pl.col(config.GENE_ID_COLNAME).is_in(best_candidates) ) diff --git a/bin/get_dataset_statistics.py b/bin/get_dataset_statistics.py index c854bb93..07901379 100755 --- a/bin/get_dataset_statistics.py +++ b/bin/get_dataset_statistics.py @@ -3,12 +3,12 @@ # Written by Olivier Coen. Released under the MIT license. import argparse -from pathlib import Path -from scipy import stats -import pandas as pd import logging +from pathlib import Path import config +import pandas as pd +from scipy import stats logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -101,7 +101,7 @@ def main(): logger.info(f"Computing dataset statistics for {count_file.name}") count_df = pd.read_parquet(count_file) - count_df.set_index(config.ENSEMBL_GENE_ID_COLNAME, inplace=True) + count_df.set_index(config.GENE_ID_COLNAME, inplace=True) dataset_stats_df = compute_dataset_statistics(count_df, args.target_distribution) diff --git a/bin/get_gene_lengths.py b/bin/get_gene_lengths.py index d3fd1d87..9cfe9a68 100755 --- a/bin/get_gene_lengths.py +++ b/bin/get_gene_lengths.py @@ -7,6 +7,7 @@ import logging from pathlib import Path +import config import pandas as pd import requests from tenacity import ( @@ -61,17 +62,17 @@ def parse_args(): stop=stop_after_delay(STOP_RETRY_AFTER_DELAY), wait=wait_exponential(multiplier=1, min=1, max=30), before_sleep=before_sleep_log(logger, logging.WARNING), - retry_error_callback=(lambda _: {}), ) def send_post_request_to_ensembl(gene_ids: list[str]) -> list[dict]: data = {"ids": gene_ids, "type": "cdna"} url = ENSEMBL_REST_SERVER + SEQUENCE_INFO_EXT response = requests.post(url, headers=HEADERS, data=json.dumps(data)) - try: + if response.status_code == 200: response.raise_for_status() - except: - logger.error(f"Could not get info for genes {gene_ids}") - return [] + else: + raise RuntimeError( + f"Failed to retrieve data: encountered error {response.status_code}" + ) return response.json() @@ -79,9 +80,8 @@ def get_gene_lengths(gene_ids: list[str]) -> list[dict]: records = send_post_request_to_ensembl(gene_ids) return [ { - "gene_id": record["query"], - "transcript_id": record["id"], - "length": len(record["seq"]), + config.GENE_ID_COLNAME: record["query"], + config.CDNA_LENGTH_COLNAME: len(record["seq"]), } for record in records if record.get("query") is not None and record.get("seq") is not None @@ -122,7 +122,9 @@ def main(): df = pd.DataFrame.from_dict(records) # taking the length of the longest transcript for each gene - df = df.groupby("gene_id", as_index=False).agg({"length": "max"}) + df = df.groupby(config.GENE_ID_COLNAME, as_index=False).agg( + {config.CDNA_LENGTH_COLNAME: "max"} + ) df.to_csv(OUTFILE, index=False, header=True) diff --git a/bin/get_ratio_standard_variation.py b/bin/get_ratio_standard_variation.py index 63667ac8..043fd9a4 100755 --- a/bin/get_ratio_standard_variation.py +++ b/bin/get_ratio_standard_variation.py @@ -2,12 +2,12 @@ # Written by Olivier Coen. Released under the MIT license. -import polars as pl -from pathlib import Path import argparse import logging +from pathlib import Path import config +import polars as pl logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -47,14 +47,14 @@ def get_nb_rows(lf: pl.LazyFrame): def get_count_columns(lf: pl.LazyFrame) -> list[str]: - """Get all column names except the config.ENSEMBL_GENE_ID_COLNAME column. + """Get all column names except the config.GENE_ID_COLNAME column. - The config.ENSEMBL_GENE_ID_COLNAME column contains only gene IDs. + The config.GENE_ID_COLNAME column contains only gene IDs. """ return [ col for col in lf.collect_schema().names() - if not col.startswith(config.ENSEMBL_GENE_ID_COLNAME) + if not col.startswith(config.GENE_ID_COLNAME) ] @@ -74,44 +74,43 @@ def compute_standard_deviations(file: Path, low_memory: bool) -> pl.LazyFrame: return pl.concat( [ concat_ratios_lf.select("ratios"), - ratios_lf.select( - pl.exclude("^.*_log_ratio$") - ), # ensembl_gene_id & ensembl_gene_id_other + ratios_lf.select(pl.exclude("^.*_log_ratio$")), # gene_id & gene_id_other ], how="horizontal", ).select( pl.col("ratios").list.std(ddof=0).alias(config.RATIOS_STD_COLNAME), - pl.col(config.ENSEMBL_GENE_ID_COLNAME), - pl.col(f"{config.ENSEMBL_GENE_ID_COLNAME}_other"), + pl.col(config.GENE_ID_COLNAME), + pl.col(f"{config.GENE_ID_COLNAME}_other"), ) def get_column_standard_deviations(std_lf: pl.LazyFrame, column: str) -> pl.LazyFrame: - # column is either config.ENSEMBL_GENE_ID_COLNAME or f"{config.ENSEMBL_GENE_ID_COLNAME}_other" + # column is either config.GENE_ID_COLNAME or f"{config.GENE_ID_COLNAME}_other" return ( std_lf.group_by(column) .agg(config.RATIOS_STD_COLNAME) # getting list of ratio std for this gene .select( - pl.col(column).alias(config.ENSEMBL_GENE_ID_COLNAME), pl.col(config.RATIOS_STD_COLNAME) + pl.col(column).alias(config.GENE_ID_COLNAME), + pl.col(config.RATIOS_STD_COLNAME), ) ) def group_standard_deviations(std_lf: pl.LazyFrame) -> pl.LazyFrame: - # getting the standard devs for genes in the ensembl_gene_id column - std_a = get_column_standard_deviations(std_lf, column=config.ENSEMBL_GENE_ID_COLNAME) - # getting the standard devs for genes in the ensembl_gene_id_other column + # getting the standard devs for genes in the gene_id column + std_a = get_column_standard_deviations(std_lf, column=config.GENE_ID_COLNAME) + # getting the standard devs for genes in the gene_id_other column std_b = get_column_standard_deviations( - std_lf, column=f"{config.ENSEMBL_GENE_ID_COLNAME}_other" + std_lf, column=f"{config.GENE_ID_COLNAME}_other" ) # concatenating both dataframes vertically # if both lists of gene ids are the identical, # we need to collect values only for one column to avoid duplicates return ( pl.concat([std_a, std_b], how="vertical") - .unique(subset=config.ENSEMBL_GENE_ID_COLNAME) + .unique(subset=config.GENE_ID_COLNAME) .sort( - config.ENSEMBL_GENE_ID_COLNAME + config.GENE_ID_COLNAME ) # only needed to have consistent output (for snapshots) ) diff --git a/bin/gprofiler_map_ids.py b/bin/gprofiler_map_ids.py new file mode 100755 index 00000000..bdf435b1 --- /dev/null +++ b/bin/gprofiler_map_ids.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 + +# Written by Olivier Coen. Released under the MIT license. + +import argparse +import logging +import sys +from pathlib import Path + +import config +import pandas as pd +from gprofiler_utils import convert_ids + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +################################################################## +# CONSTANTS +################################################################## + +MAPPED_GENE_IDS_OUTFILE = "mapped_gene_ids.csv" +METADATA_OUTFILE = "gene_metadata.csv" + +TARGET_DATABASE_CHOICES = ["ENTREZGENE", "ENSG"] + +FAILURE_REASON_FILE = "failure_reason.txt" + +################################################################## +# FUNCTIONS +################################################################## + + +def parse_args(): + parser = argparse.ArgumentParser("Map IDs using g:Profiler") + parser.add_argument( + "--gene-ids", + type=Path, + dest="gene_id_file", + required=True, + help="Input file containing gene IDs", + ) + parser.add_argument( + "--species", type=str, required=True, help="Species to convert IDs for" + ) + parser.add_argument( + "--target-db", + type=str, + dest="gprofiler_target_db", + required=True, + choices=TARGET_DATABASE_CHOICES, + help="Target database to convert IDs to", + ) + return parser.parse_args() + + +################################################################## +# MAIN +################################################################## + + +def main(): + args = parse_args() + + with open(args.gene_id_file, "r") as fin: + gene_ids = [line.strip() for line in fin] + + logger.info(f"Converting {len(gene_ids)} IDs for species {args.species} ") + + ############################################################# + # QUERYING g:PROFILER SERVER + ############################################################# + + gene_metadata_dfs = [] + + mapping_dict, gene_metadata_dfs = convert_ids( + gene_ids, args.species, args.gprofiler_target_db + ) + + if not mapping_dict: + msg = "NO MAPPINGS FOUND" + logger.warning(msg) + with open(FAILURE_REASON_FILE, "w") as f: + f.write(msg) + sys.exit(0) + + ############################################################# + # WRITING MAPPING + ############################################################# + + # making dataframe for mapping (only two columns: original and new) + mapping_df = ( + pd.DataFrame(mapping_dict, index=[0]) + .T.reset_index() # transpose: setting keys as indexes instead of columns + .rename( + columns={ + "index": config.ORIGINAL_GENE_ID_COLNAME, + 0: config.GENE_ID_COLNAME, + } + ) + ) + mapping_df.to_csv(MAPPED_GENE_IDS_OUTFILE, index=False, header=True) + + ############################################################# + # WRITING METADATA + ############################################################# + + gene_metadata_df = pd.concat(gene_metadata_dfs, ignore_index=True) + # dropping duplicates and keeping the first occurence + gene_metadata_df.drop_duplicates( + inplace=True, subset=[config.GENE_ID_COLNAME], keep="first" + ) + gene_metadata_df.to_csv(METADATA_OUTFILE, index=False, header=True) + + +if __name__ == "__main__": + main() diff --git a/bin/gprofiler_utils.py b/bin/gprofiler_utils.py index cb860832..18b9d41a 100755 --- a/bin/gprofiler_utils.py +++ b/bin/gprofiler_utils.py @@ -30,7 +30,6 @@ CHUNKSIZE = 2000 # number of IDs to convert at a time - may create trouble if > 2000 -TARGET_DATABASE = "ENSG" # Ensembl database COLS_TO_KEEP = ["incoming", "converted", "name", "description"] DESCRIPTION_PART_TO_REMOVE_REGEX = r"\s*\[Source:.*?\]" @@ -146,13 +145,15 @@ def request_conversion( else: # both servers appear down, we stop here... logger.error(GPROFILER_ERROR_MESSAGE) - raise GProfilerConnectionError + raise GProfilerConnectionError(GPROFILER_ERROR_MESSAGE) else: return response.json()["result"] -def convert_chunk_of_ids(gene_ids: list, species: str) -> tuple[dict, pd.DataFrame]: +def convert_chunk_of_ids( + gene_ids: list, species: str, gprofiler_target_db: str +) -> tuple[dict, pd.DataFrame]: """ Wrapper function that converts a list of gene IDs to another namespace. @@ -171,7 +172,7 @@ def convert_chunk_of_ids(gene_ids: list, species: str) -> tuple[dict, pd.DataFra A dictionary where the keys are the original IDs and the values are the converted IDs. """ - results = request_conversion(gene_ids, species, TARGET_DATABASE) + results = request_conversion(gene_ids, species, gprofiler_target_db) df = pd.DataFrame.from_records(results) if df.empty: @@ -185,7 +186,7 @@ def convert_chunk_of_ids(gene_ids: list, species: str) -> tuple[dict, pd.DataFra # DataFrame associating converted IDs to name and description meta_df = df.drop(columns=["incoming"]).rename( - columns={"converted": config.ENSEMBL_GENE_ID_COLNAME} + columns={"converted": config.GENE_ID_COLNAME} ) meta_df["name"] = meta_df["name"].str.replace(",", ";") @@ -213,14 +214,18 @@ def chunk_list(lst: list, chunksize: int) -> list: return [lst[i : i + chunksize] for i in range(0, len(lst), chunksize)] -def convert_ids(ids: list[str], species: str) -> tuple[dict, pd.DataFrame]: +def convert_ids( + ids: list[str], species: str, gprofiler_target_db: str +) -> tuple[dict, pd.DataFrame]: mapping_dict = {} gene_metadata_dfs = [] chunks = chunk_list(ids, chunksize=CHUNKSIZE) for chunk_gene_ids in chunks: - # converting to Ensembl IDs for all IDs comprised in this chunk - gene_mapping, meta_df = convert_chunk_of_ids(chunk_gene_ids, species) + # converting to Gene IDs for all IDs comprised in this chunk + gene_mapping, meta_df = convert_chunk_of_ids( + chunk_gene_ids, species, gprofiler_target_db + ) mapping_dict.update(gene_mapping) gene_metadata_dfs.append(meta_df) diff --git a/bin/make_pairwise_gene_expression_ratio.py b/bin/make_pairwise_gene_expression_ratio.py index 28d94b27..7ecaf194 100755 --- a/bin/make_pairwise_gene_expression_ratio.py +++ b/bin/make_pairwise_gene_expression_ratio.py @@ -2,12 +2,12 @@ # Written by Olivier Coen. Released under the MIT license. -import polars as pl -from pathlib import Path import argparse import logging +from pathlib import Path import config +import polars as pl logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -39,14 +39,14 @@ def parse_args(): def get_count_columns(lf: pl.LazyFrame) -> list[str]: - """Get all column names except the config.ENSEMBL_GENE_ID_COLNAME column. + """Get all column names except the config.GENE_ID_COLNAME column. - The config.ENSEMBL_GENE_ID_COLNAME column contains only gene IDs. + The config.GENE_ID_COLNAME column contains only gene IDs. """ return [ col for col in lf.collect_schema().names() - if not col.startswith(config.ENSEMBL_GENE_ID_COLNAME) + if not col.startswith(config.GENE_ID_COLNAME) ] @@ -59,7 +59,7 @@ def compute_ratios(file: Path, low_memory: bool) -> pl.LazyFrame: if not col.endswith("_other") } return cross_join_lf.select( - [pl.col(config.ENSEMBL_GENE_ID_COLNAME), pl.col(f"{config.ENSEMBL_GENE_ID_COLNAME}_other")] + [pl.col(config.GENE_ID_COLNAME), pl.col(f"{config.GENE_ID_COLNAME}_other")] + [ (pl.col(col) / pl.col(other_col)).log(base=2).alias(f"{col}_log_ratio") for col, other_col in column_pairs.items() diff --git a/bin/make_parquet_chunks.py b/bin/make_parquet_chunks.py index a4ff1446..f1a56578 100755 --- a/bin/make_parquet_chunks.py +++ b/bin/make_parquet_chunks.py @@ -2,13 +2,13 @@ # Written by Olivier Coen. Released under the MIT license. -import polars as pl -from pathlib import Path -from math import ceil import argparse import logging +from math import ceil +from pathlib import Path import config +import polars as pl logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -50,7 +50,7 @@ def get_nb_rows(lf: pl.LazyFrame): def parse_count_dataset(file: Path, low_memory: bool) -> pl.LazyFrame: lf = pl.scan_parquet(file, low_memory=low_memory).fill_null(0).fill_nan(0) count_columns = get_count_columns(lf) - cols = [pl.col(config.ENSEMBL_GENE_ID_COLNAME)] + [ + cols = [pl.col(config.GENE_ID_COLNAME)] + [ pl.col(column).replace({0: ZERO_REPLACE_VALUE}).cast(pl.Float64) for column in count_columns ] @@ -58,14 +58,14 @@ def parse_count_dataset(file: Path, low_memory: bool) -> pl.LazyFrame: def get_count_columns(lf: pl.LazyFrame) -> list[str]: - """Get all column names except the config.ENSEMBL_GENE_ID_COLNAME column. + """Get all column names except the config.GENE_ID_COLNAME column. - The config.ENSEMBL_GENE_ID_COLNAME column contains only gene IDs. + The config.GENE_ID_COLNAME column contains only gene IDs. """ return [ col for col in lf.collect_schema().names() - if not col.startswith(config.ENSEMBL_GENE_ID_COLNAME) + if not col.startswith(config.GENE_ID_COLNAME) ] diff --git a/bin/merge_counts.py b/bin/merge_counts.py index 5917e6bd..67c5d2da 100755 --- a/bin/merge_counts.py +++ b/bin/merge_counts.py @@ -3,13 +3,13 @@ # Written by Olivier Coen. Released under the MIT license. import argparse -from tqdm import tqdm -import polars as pl -from pathlib import Path import logging from functools import reduce +from pathlib import Path import config +import polars as pl +from tqdm import tqdm logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -39,11 +39,11 @@ def parse_args(): def parse_count_file(count_file: Path) -> pl.DataFrame: df = pl.read_parquet(count_file) - # in some cases, the first column may have an empty name or be different than config.ENSEMBL_GENE_ID_COLNAME - # in any case, this column must have the config.ENSEMBL_GENE_ID_COLNAME name + # in some cases, the first column may have an empty name or be different than config.GENE_ID_COLNAME + # in any case, this column must have the config.GENE_ID_COLNAME name first_column_name = df.columns[0] - if first_column_name != config.ENSEMBL_GENE_ID_COLNAME: - df = df.rename({first_column_name: config.ENSEMBL_GENE_ID_COLNAME}) + if first_column_name != config.GENE_ID_COLNAME: + df = df.rename({first_column_name: config.GENE_ID_COLNAME}) return df @@ -71,27 +71,27 @@ def get_valid_dfs(files: list[Path]) -> list[pl.DataFrame]: def join_count_dfs(df1: pl.DataFrame, df2: pl.DataFrame) -> pl.DataFrame: - """Join two DataFrames on the config.ENSEMBL_GENE_ID_COLNAME column. + """Join two DataFrames on the config.GENE_ID_COLNAME column. The how parameter is set to "full" to include all rows from both dfs. The coalesce parameter is set to True to fill NaN values in the resulting dataframe with values from the other dataframe. """ - return df1.join(df2, on=config.ENSEMBL_GENE_ID_COLNAME, how="full", coalesce=True) + return df1.join(df2, on=config.GENE_ID_COLNAME, how="full", coalesce=True) def get_count_columns(df: pl.DataFrame) -> list[str]: - """Get all column names except the config.ENSEMBL_GENE_ID_COLNAME column. + """Get all column names except the config.GENE_ID_COLNAME column. - The config.ENSEMBL_GENE_ID_COLNAME column contains only gene IDs. + The config.GENE_ID_COLNAME column contains only gene IDs. """ - return df.select(pl.exclude(config.ENSEMBL_GENE_ID_COLNAME)).columns + return df.select(pl.exclude(config.GENE_ID_COLNAME)).columns def get_counts(files: list[Path]) -> pl.DataFrame: """Get all count data from a list of files. - The files are merged into a single dataframe. The config.ENSEMBL_GENE_ID_COLNAME column is cast + The files are merged into a single dataframe. The config.GENE_ID_COLNAME column is cast to String, and all other columns are cast to Float64. """ logger.info("Parsing counts") @@ -99,7 +99,7 @@ def get_counts(files: list[Path]) -> pl.DataFrame: # joining all count files logger.info( - f"Joining count files recursively on the {config.ENSEMBL_GENE_ID_COLNAME} column" + f"Joining count files recursively on the {config.GENE_ID_COLNAME} column" ) merged_df = reduce(join_count_dfs, tqdm(dfs)) @@ -109,7 +109,7 @@ def get_counts(files: list[Path]) -> pl.DataFrame: # casting nans to nulls logger.info("Cleaning mergeed dataframe") return merged_df.select( - [pl.col(config.ENSEMBL_GENE_ID_COLNAME).cast(pl.String)] + [pl.col(config.GENE_ID_COLNAME).cast(pl.String)] + [pl.col(column).cast(pl.Float64) for column in count_columns] ).fill_nan(None) diff --git a/bin/normalise_to_tpm.py b/bin/normalise_to_tpm.py index a81fe6fc..0f0e4302 100755 --- a/bin/normalise_to_tpm.py +++ b/bin/normalise_to_tpm.py @@ -13,7 +13,7 @@ logger = logging.getLogger(__name__) -TPM_NORM_SUFFIX = ".tpm.csv" +TPM_NORM_SUFFIX = ".tpm.parquet" ##################################################### @@ -29,11 +29,11 @@ def parse_args(): "--counts", type=Path, dest="count_file", required=True, help="Count file" ) parser.add_argument( - "--annotation", + "--lengths", type=Path, - dest="annotation_file", + dest="lengths_file", required=True, - help="File containing gene annotations (and gene lengths in particular)", + help="File containing gene lengths", ) return parser.parse_args() @@ -46,7 +46,7 @@ def is_raw_counts(df: pd.DataFrame): def is_tpm(df: pd.DataFrame): """Check if the data are TPM (sum to 1e6 per sample).""" sample_sums = df.sum(axis=0) - return all((sample_sums - 1e6).abs() < 1e-3) # Allow for floating-point precision + return all((sample_sums - 1e6).abs() < 1e-2) # Allow for floating-point precision def is_fpkm_or_rpkm(df: pd.DataFrame): @@ -54,36 +54,55 @@ def is_fpkm_or_rpkm(df: pd.DataFrame): return not is_raw_counts(df) and not is_tpm(df) -def process_to_tpm(df: pd.DataFrame, gene_lengths: list): +def compute_tpm(df: pd.DataFrame, cdna_length_series: pd.Series): """ Process raw counts, FPKM, or RPKM to TPM. - - For raw counts: Calculate RPKM, then TPM. - - For FPKM/RPKM: Convert directly to TPM. """ if is_raw_counts(df): - # Calculate RPKM - total_reads = df.sum(axis=0) - rpkm = df.div(gene_lengths, axis=0) / total_reads * 1e9 - # Convert RPKM to TPM - tpm = rpkm.div(rpkm.sum(axis=0), axis=1) * 1e6 + logger.info("Raw counts detected → computing TPM directly.") + rpk = df.div(cdna_length_series, axis=0) # read per kilobase + tpm = rpk.div(rpk.sum(axis=0), axis=1) * 1e6 return tpm elif is_fpkm_or_rpkm(df): # Convert FPKM/RPKM to TPM + logger.info("FPKM/RPKM detected → computing TPM.") tpm = df.div(df.sum(axis=0), axis=1) * 1e6 return tpm elif is_tpm(df): - print("Data are already TPM. No conversion needed.") + logger.info("Data are already TPM. No conversion needed.") return df else: raise ValueError("Could not determine data type.") -def export_count_data(quantile_normalized_counts: pd.DataFrame, count_file: Path): - """Export gene expression data to CSV files.""" +def parse_data( + count_file: Path, gene_length_file: Path +) -> tuple[pd.DataFrame, pd.Series]: + count_df = pd.read_csv(count_file, index_col=0) + count_df.index.name = config.GENE_ID_COLNAME + + cdna_length_df = pd.read_csv( + gene_length_file, + names=[config.GENE_ID_COLNAME, config.CDNA_LENGTH_COLNAME], + ) + # merge with gene length and extracts it afterwards + # so that the genes are in the same order in df and in cdna_length_series + count_df = pd.merge( + count_df, cdna_length_df, how="left", left_index=True, right_on="gene_id" + ) + cdna_length_series = count_df[config.CDNA_LENGTH_COLNAME].astype(float) + count_df = count_df.drop( + columns=[config.CDNA_LENGTH_COLNAME] + ) # keep only expression values + return count_df, cdna_length_series + + +def export_normalised_data(count_df: pd.DataFrame, count_file: Path): + """Export gene expression data to Parquet.""" # replace .csv / .tsv by .tpm.csv outfilename = ".".join(count_file.name.split(".")[:-1]) + TPM_NORM_SUFFIX - logger.info(f"Exporting quantile normalised counts to: {outfilename}") - quantile_normalized_counts.reset_index().to_parquet(outfilename) + logger.info(f"Exporting TPM normalised counts to: {outfilename}") + count_df.reset_index().to_parquet(outfilename) ##################################################### @@ -95,15 +114,15 @@ def export_count_data(quantile_normalized_counts: pd.DataFrame, count_file: Path def main(): args = parse_args() - count_file = args.count_file - logger.info(f"Normalising {count_file.name}") - count_df = pd.read_csv(count_file, index_col=0) - count_df.index.name = config.ENSEMBL_GENE_ID_COLNAME + logger.info("Parsing data") + count_df, cdna_length_series = parse_data(args.count_file, args.gene_length_file) + + logger.info(f"Normalising {args.count_file.name}") - quantile_normalized_counts = quantile_normalise(count_df, args.target_distribution) + count_df = compute_tpm(count_df, cdna_length_series) - export_count_data(quantile_normalized_counts, count_file) + export_normalised_data(count_df, args.count_file) if __name__ == "__main__": diff --git a/bin/normfinder.py b/bin/normfinder.py index 52a0fa57..774d463d 100755 --- a/bin/normfinder.py +++ b/bin/normfinder.py @@ -127,10 +127,7 @@ def __post_init__(self): self.n_groups = len(groups) self.genes = ( - self.count_lf.select(config.ENSEMBL_GENE_ID_COLNAME) - .collect() - .to_series() - .to_list() + self.count_lf.select(config.GENE_ID_COLNAME).collect().to_series().to_list() ) self.n_genes = len(self.genes) @@ -190,7 +187,7 @@ def get_unbiased_intragroup_variance_for_group( data = {gene: [0] for gene in self.genes} return pl.DataFrame(data) - # lf is a lazyframe with a column being the gene ids (ensembl_gene_id) + # lf is a lazyframe with a column being the gene ids (gene_id) # and other columns being the samples # the current chunk corresponds to only one group # means_over_samples_df is a single column dataframe containing the means across each row (ie for each gene across samples) @@ -432,7 +429,7 @@ def get_stability_values( .mean() .transpose( include_header=True, - header_name=config.ENSEMBL_GENE_ID_COLNAME, + header_name=config.GENE_ID_COLNAME, column_names=[config.NORMFINDER_STABILITY_VALUE_COLNAME], ) ) diff --git a/bin/quantile_normalise.py b/bin/quantile_normalise.py index 88d9f66c..180f4360 100755 --- a/bin/quantile_normalise.py +++ b/bin/quantile_normalise.py @@ -79,8 +79,10 @@ def main(): count_file = args.count_file logger.info(f"Quantile normalising {count_file.name}") + # count_df = pd.read_parquet(count_file) + # count_df.set_index(config.GENE_ID_COLNAME, inplace=True) count_df = pd.read_csv(count_file, index_col=0) - count_df.index.name = config.ENSEMBL_GENE_ID_COLNAME + count_df.index.name = config.GENE_ID_COLNAME quantile_normalized_counts = quantile_normalise(count_df, args.target_distribution) diff --git a/bin/map_ids_to_ensembl.py b/bin/rename_gene_ids.py similarity index 54% rename from bin/map_ids_to_ensembl.py rename to bin/rename_gene_ids.py index e53ed142..3921a9cc 100755 --- a/bin/map_ids_to_ensembl.py +++ b/bin/rename_gene_ids.py @@ -9,7 +9,6 @@ import config import pandas as pd -from gprofiler_utils import GProfilerConnectionError, convert_ids logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -23,6 +22,7 @@ METADATA_FILE_SUFFIX = ".metadata.csv" MAPPING_FILE_SUFFIX = ".mapping.csv" +WARNING_REASON_FILE = "warning_reason.txt" FAILURE_REASON_FILE = "failure_reason.txt" ################################################################## @@ -31,25 +31,22 @@ def parse_args(): - parser = argparse.ArgumentParser("Map IDs to Ensembl") + parser = argparse.ArgumentParser("Rename gene IDs using mapped IDs") parser.add_argument( "--count-file", type=Path, required=True, help="Input file containing counts" ) parser.add_argument( - "--species", type=str, required=True, help="Species to convert IDs for" + "--mappings", + type=Path, + dest="mapping_file", + help="Mapping file containing gene IDs", ) parser.add_argument( "--custom-mappings", type=Path, - dest="custom_mappings", + dest="custom_mapping_file", help="Optional file containing custom mappings", ) - parser.add_argument( - "--custom-metadata", - type=Path, - dest="custom_metadata", - help="Optional file containing custom metadata", - ) return parser.parse_args() @@ -68,21 +65,15 @@ def parse_table(file: Path, **kwargs): def main(): args = parse_args() - count_file = args.count_file - custom_mapping_file = args.custom_mappings - custom_metadata_file = args.custom_metadata - - logger.info( - f"Converting IDs for species {args.species} and count file {count_file.name}..." - ) + logger.info(f"Converting IDs for count file {args.count_file.name}...") ############################################################# # PARSING FILES ############################################################# - # whatever the name of the first col, rename it to "ensembl_gene_id" - df = parse_table(count_file, index_col=0) - df.index.rename(config.ENSEMBL_GENE_ID_COLNAME, inplace=True) + # whatever the name of the first col, rename it to "gene_id" + df = parse_table(args.count_file, index_col=0) + df.index.rename(config.GENE_ID_COLNAME, inplace=True) if df.empty: msg = "COUNT FILE IS EMPTY" @@ -92,49 +83,29 @@ def main(): sys.exit(0) df.index = df.index.astype(str) - gene_ids = df.index.tolist() - - custom_mappings_dict = {} - if custom_mapping_file: - custom_mapping_df = parse_table(custom_mapping_file) - custom_mappings_dict = custom_mapping_df.set_index( - config.ORIGINAL_GENE_ID_COLNAME - )[config.ENSEMBL_GENE_ID_COLNAME].to_dict() - - gene_ids_left_to_map = [ - gene_id for gene_id in gene_ids if gene_id not in custom_mappings_dict - ] - logger.info(f"Number of genes left to map: {len(gene_ids_left_to_map)}") ############################################################# - # QUERYING g:PROFILER SERVER + # GETTING MAPPINGS ############################################################# - gprofiler_mapping_dict = {} - gene_metadata_dfs = [] - - try: - if gene_ids_left_to_map: - gprofiler_mapping_dict, gene_metadata_dfs = convert_ids( - gene_ids_left_to_map, args.species - ) - except GProfilerConnectionError: - msg = "COULD NOT CONNECT TO GPROFILER SERVER" - logger.warning(msg) - with open(FAILURE_REASON_FILE, "w") as f: - f.write(msg) - sys.exit(0) + mapping_dict = {} + if args.mapping_file is not None: + mapping_df = parse_table(args.mapping_file) + mapping_dict = mapping_df.set_index(config.ORIGINAL_GENE_ID_COLNAME)[ + config.GENE_ID_COLNAME + ].to_dict() + + custom_mapping_dict = {} + if args.custom_mapping_file is not None: + custom_mapping_df = parse_table(args.custom_mapping_file) + custom_mapping_dict = custom_mapping_df.set_index( + config.ORIGINAL_GENE_ID_COLNAME + )[config.GENE_ID_COLNAME].to_dict() - # overall mappings is the custom_mappings_dict complemented with gprofiler_mapping_dict - mapping_dict = custom_mappings_dict | gprofiler_mapping_dict + mapping_dict |= custom_mapping_dict - # if mapping dict is empty if not mapping_dict: - msg = f"NO MAPPING FOR GENE IDS: {', '.join(df.index[:5].tolist())}, ..." - logger.warning(msg) - with open(FAILURE_REASON_FILE, "w") as f: - f.write(msg) - sys.exit(0) + raise ValueError("No mapping found") # should not happen ############################################################# # MAPPING GENE IDS IN DATAFRAME @@ -142,24 +113,40 @@ def main(): # IMPORTANT: KEEPING ONLY GENES THAT HAVE BEEN CONVERTED # filtering the DataFrame to keep only the rows where the index can be mapped + original_nb_genes = len(df) + df = df.loc[df.index.isin(mapping_dict)] + if df.empty: + msg = "NO GENES WERE MAPPED" + logger.error(msg) + with open(FAILURE_REASON_FILE, "w") as f: + f.write(msg) + sys.exit(0) + + if len(df) < original_nb_genes: + msg = f"Only {len(df) / original_nb_genes:.2%} of genes were mapped ({len(df)} out of {original_nb_genes})" + logger.warning(msg) + with open(WARNING_REASON_FILE, "a") as f: + f.write(msg) + else: + logger.info(f"All genes were mapped ({len(df)} out of {original_nb_genes})") # renaming gene names to mapped ids using mapping dict df.index = df.index.map(mapping_dict) df.reset_index(inplace=True) # TODO: check is there is another way to avoid duplicate gene names - # sometimes different gene names have the same ensembl ID + # sometimes different gene names have the same Gene ID # for now, we just get the mean of values, but this is not ideal ############################################################# # GENE COUNT HANDLING ############################################################# - # handling cases where multiple genes have the same ensembl ID + # handling cases where multiple genes have the same Gene ID # since subsequent steps in the pipeline require integer values, # we need to ensure that the resulting DataFrame has integer values - df = df.groupby(config.ENSEMBL_GENE_ID_COLNAME, as_index=False, sort=False).agg( + df = df.groupby(config.GENE_ID_COLNAME, as_index=False, sort=False).agg( lambda x: x.mean().astype(int) ) @@ -167,26 +154,9 @@ def main(): # WRITING OUTFILES ############################################################# # writing to output file - outfile = count_file.with_name(count_file.stem + RENAMED_FILE_SUFFIX) + outfile = args.count_file.with_name(args.count_file.stem + RENAMED_FILE_SUFFIX) df.to_csv(outfile, index=False, header=True) - # if the user provides custom metadata file - if custom_metadata_file: - custom_metadata_df = parse_table(custom_metadata_file) - # prepending custom metadata in gene metadata - gene_metadata_dfs = [custom_metadata_df] + gene_metadata_dfs - - # concatenating all metadata and ensuring there are no duplicates - if gene_metadata_dfs: - gene_metadata_df = pd.concat(gene_metadata_dfs, ignore_index=True) - # dropping duplicates and keeping the first occurence - gene_metadata_df.drop_duplicates( - inplace=True, subset=[config.ENSEMBL_GENE_ID_COLNAME], keep="first" - ) - # writing gene metadata to file - metadata_file = count_file.with_name(count_file.stem + METADATA_FILE_SUFFIX) - gene_metadata_df.to_csv(metadata_file, index=False, header=True) - # making dataframe for mapping (only two columns: original and new) mapping_df = ( pd.DataFrame(mapping_dict, index=[0]) @@ -194,11 +164,11 @@ def main(): .rename( columns={ "index": config.ORIGINAL_GENE_ID_COLNAME, - 0: config.ENSEMBL_GENE_ID_COLNAME, + 0: config.GENE_ID_COLNAME, } ) ) - mapping_file = count_file.with_name(count_file.stem + MAPPING_FILE_SUFFIX) + mapping_file = args.count_file.with_name(args.count_file.stem + MAPPING_FILE_SUFFIX) mapping_df.to_csv(mapping_file, index=False, header=True) diff --git a/docs/output.md b/docs/output.md index 82b0f57a..47ca0cf1 100644 --- a/docs/output.md +++ b/docs/output.md @@ -23,7 +23,7 @@ The pipeline is built using [Nextflow](https://www.nextflow.io/) and processes d - Download [Expression Atlas](https://www.ebi.ac.uk/gxa/home) data (run by default; optional) - Download NBCI [GEO](https://www.ncbi.nlm.nih.gov/gds) data (run by default; optional) 3. ID Mapping - - Map gene IDS to Ensembl IDS for standardisation among datasets using [g:Profiler](https://biit.cs.ut.ee/gprofiler/gost) (run by default; optional) + - Map gene IDS to NCBI Entrez Gene IDS (or Ensembl IDs) for standardisation among datasets using [g:Profiler](https://biit.cs.ut.ee/gprofiler/gost) (run by default; optional) 4. Data normalisation - Normalize RNAseq raw data using [DESeq2](https://bioconductor.org/packages/release/bioc/html/DESeq2.html) or [EdgeR](https://bioconductor.org/packages/release/bioc/html/edgeR.html) - Perform quantile normalisation on each dataset separately using [scikit-learn](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.quantile_transform.html) @@ -109,9 +109,9 @@ and open your browser at `http://localhost:8080` Output files - `idmapping/` - - Count datasets whose gene IDs have been mapped to Ensembl IDs: `*.renamed.csv`. - - Table associating original gene IDs and Ensembl IDs: `*.mapping.csv`. - - Ensembl gene metadata (name and description): `*.metadata.csv`. + - Count datasets whose gene IDs have been mapped: `*.renamed.csv`. + - Table associating original gene IDs and mapped gene IDs: `*.mapping.csv`. + - Gene metadata (name and description): `*.metadata.csv`.
    diff --git a/docs/usage.md b/docs/usage.md index 1cd03b3d..f940ebf4 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -175,7 +175,7 @@ nextflow run nf-core/stableexpression \ > The `--skip_fetch_eatlas_accessions` and `--skip_fetch_geo_accessions` parameters are supplied here to show how to analyse __only your own dataset__. You may remove these parameters if you want to mix you dataset(s) with public ones. > [!IMPORTANT] -> By default, the pipeline tries to map gene IDs to Ensembl gene IDs. __All genes that cannot be mapped are discarded from the analysis__. This ensures that all genes are named the same between datasets and allows comparing multiple datasets with each other. If you are confident that your genes have the same name between your different datasets or if you think that your gene IDs won't be mapped properly, you can disable this mapping by adding the `--skip_id_mapping` parameter. In such case, you may supply your own gene id mapping file and gene metadata file with the `--gene_id_mapping` and `--gene_metadata` parameters respectively. See [next section](#5-custom-gene-id-mapping-and-metadata) for further details. +> By default, the pipeline tries to map gene IDs to NCBI Entrez Gene IDs. __All genes that cannot be mapped are discarded from the analysis__. This ensures that all genes are named the same between datasets and allows comparing multiple datasets with each other. If you are confident that your genes have the same name between your different datasets or if you think that your gene IDs won't be mapped properly, you can disable this mapping by adding the `--skip_id_mapping` parameter. In such case, you may supply your own gene id mapping file and gene metadata file with the `--gene_id_mapping` and `--gene_metadata` parameters respectively. See [next section](#5-custom-gene-id-mapping-and-metadata) for further details. > [!TIP] > You can check if your gene IDs can be mapped using the [g:Profiler server](https://biit.cs.ut.ee/gprofiler/convert). @@ -202,32 +202,30 @@ Structure of the gene id mapping file: | Column | Description | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `original_gene_id` | Gene ID used in the provided count dataset(s) | -| `ensembl_gene_id` | Mapped gene ID | +| `gene_id` | Mapped gene ID | It should look as follows: ```csv title=gene_id_mapping.csv -original_gene_id,ensembl_gene_id +original_gene_id,gene_id gene_A,ENSG1234567890 geneB,OTHERmappedgeneID ``` -> [!NOTE] -> The gene IDs in the `ensembl_gene_id` column do not have to be real Ensembl gene IDs. Structure of the gene metadata file: | Column | Description | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `ensembl_gene_id` | Mapped gene ID | -| `name` | Gene common name | -| `description` | Gene description | +| `gene_id` | Mapped gene ID | +| `name` | Gene common name | +| `description` | Gene description | It should look as follows: ```csv title=gene_metadata.csv -ensembl_gene_id,name,description +gene_id,name,description ENSG1234567890,Gene A,Description of gene A OTHERmappedgeneID,My OTHER Gene,Another description ``` diff --git a/galaxy/build/static/boilerplate.xml b/galaxy/build/static/boilerplate.xml index 4f1ef82d..13fc6700 100644 --- a/galaxy/build/static/boilerplate.xml +++ b/galaxy/build/static/boilerplate.xml @@ -78,7 +78,7 @@ INPUTS - + @@ -97,7 +97,7 @@ INPUTS - + @@ -118,7 +118,7 @@ INPUTS - + diff --git a/galaxy/tool_shed/tool/nf_core_stableexpression.xml b/galaxy/tool_shed/tool/nf_core_stableexpression.xml index bd502632..6764ec86 100644 --- a/galaxy/tool_shed/tool/nf_core_stableexpression.xml +++ b/galaxy/tool_shed/tool/nf_core_stableexpression.xml @@ -180,8 +180,8 @@ VERSION="1.0dev"; echo "$VERSION"
    - - + +
    @@ -229,7 +229,7 @@ VERSION="1.0dev"; echo "$VERSION"
    - + @@ -248,7 +248,7 @@ VERSION="1.0dev"; echo "$VERSION" - + @@ -269,7 +269,7 @@ VERSION="1.0dev"; echo "$VERSION" - + diff --git a/galaxy/tool_shed/tool/test_data/microarray.normalised.csv b/galaxy/tool_shed/tool/test_data/microarray.normalised.csv index 1f93b0ca..81f3f904 100644 --- a/galaxy/tool_shed/tool/test_data/microarray.normalised.csv +++ b/galaxy/tool_shed/tool/test_data/microarray.normalised.csv @@ -1,4 +1,4 @@ -ensembl_gene_id,GSM1528575,GSM1528576,GSM1528579,GSM1528583,GSM1528584,GSM1528585,GSM1528580,GSM1528586,GSM1528582,GSM1528578,GSM1528581,GSM1528577 +gene_id,GSM1528575,GSM1528576,GSM1528579,GSM1528583,GSM1528584,GSM1528585,GSM1528580,GSM1528586,GSM1528582,GSM1528578,GSM1528581,GSM1528577 ENSRNA049453121,20925.1255070264,136184.261516502,144325.370645564,89427.0987612997,164143.182734208,34178.6378088171,28842.7323281157,76973.395782103,41906.9367255656,44756.5602263121,252562.049703724,6953.65643340122 ENSRNA049453138,196173.051628372,16607.8367703051,344972.83715281,22602.4535330758,13678.598561184,104546.421532852,15451.4637472048,71664.8857281649,160643.257448002,91459.0578537683,88396.7173963033,281623.08555275 ENSRNA049454388,91547.4240932405,11625.4857392136,84483.143792525,80582.6604222701,218857.576978944,58304.7350856292,42234.0009090266,88475.1675656357,87306.1181782617,17513.436610296,90922.3378933406,76490.2207674135 diff --git a/modules/local/collect_gene_ids/main.nf b/modules/local/collect_gene_ids/main.nf new file mode 100644 index 00000000..36170583 --- /dev/null +++ b/modules/local/collect_gene_ids/main.nf @@ -0,0 +1,25 @@ +process COLLECT_GENE_IDS { + + label "process_high" + + conda "${moduleDir}/spec-file.txt" + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/60/604657081a64b39e17bb6ad307e545aa6aebf4133b64d6766515c9789bb2d304/data': + 'community.wave.seqera.io/library/pandas_tqdm:2ca37c1047243549' }" + + input: + path count_files, stageAs: "?/*" + + output: + path 'all_gene_ids.txt', emit: gene_ids + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions + tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions + tuple val("${task.process}"), val('tqdm'), eval('python3 -c "import tqdm; print(tqdm.__version__)"'), topic: versions + + script: + """ + collect_gene_ids.py \\ + --counts "$count_files" + """ + +} diff --git a/modules/local/collect_gene_ids/spec-file.txt b/modules/local/collect_gene_ids/spec-file.txt new file mode 100644 index 00000000..3635dc57 --- /dev/null +++ b/modules/local/collect_gene_ids/spec-file.txt @@ -0,0 +1,45 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda#f7b4d76975aac7e5d9e6ad13845f92fe +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda#c0374badb3a5d4b1372db28d19462c53 +https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda#f0991f0f84902f6b6009b4d2350a83aa +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda#5b767048b1b3ee9a954b06f4084f93dc +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 +https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda#6432cb5d4ac0046c3ac0a8a0f95842f9 +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-h1aa0949_0.conda#1450224b3e7d17dfeb985364b77a4d47 +https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda#8b09ae86839581147ef2e5c5e229d164 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda#35f29eec58405aaf55e01cb470d8c26a +https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc +https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda#280ea6eee9e2ddefde25ff799c4f0363 +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda#f627678cf829bd70bccf141a19c3ad3e +https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.0-hee844dc_0.conda#729a572a3ebb8c43933b30edcc628ceb +https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda#80c07c68d2f6870250959dcc95b209d1 +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda#9ee58d5c534af06558933af3c845a780 +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 +https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 +https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda#86bc20552bf46075e3d92b67f089172d +https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a +https://conda.anaconda.org/conda-forge/linux-64/python-3.13.9-hc97d973_101_cp313.conda#4780fe896e961722d0623fa91d0d3378 +https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda#962b9857ee8e7018c22f2776ffa0b2d7 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-hcd61629_7.conda#f116940d825ffc9104400f0d7f1a4551 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_7.conda#8621a450add4e231f676646880703f49 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda#be43915efc66345cccb3c310b6ed0374 +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-1_h4a7cf45_openblas.conda#8b39e1ae950f1b54a3959c58ca2c32b8 +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-1_h0358290_openblas.conda#a670bff9eb7963ea41b4e09a4e4ab608 +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-1_h47877c9_openblas.conda#dee12a83aa4aca5077ea23c0605de044 +https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.5-py313hf6604e3_0.conda#15f43bcd12c90186e78801fafc53d89b +https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda#3339e3b65d58accf4ca4fb8748ab16b3 +https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 +https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 +https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 +https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.3-py313h08cd8bf_1.conda#9e87d4bda0c2711161d765332fa38781 +https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda#bf47878473e5ab9fdb4115735230e191 +https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda#9efbfdc37242619130ea42b1cc4ed861 diff --git a/modules/local/dash_app/app/src/callbacks/genes.py b/modules/local/dash_app/app/src/callbacks/genes.py index 704196f5..10cd2f74 100644 --- a/modules/local/dash_app/app/src/callbacks/genes.py +++ b/modules/local/dash_app/app/src/callbacks/genes.py @@ -1,6 +1,5 @@ -from dash_extensions.enrich import Input, Output, State, callback, ctx, Serverside import plotly.graph_objects as go - +from dash_extensions.enrich import Input, Output, Serverside, State, callback, ctx from src.utils.data_management import DataManager data_manager = DataManager() @@ -15,7 +14,7 @@ def get_selected_rows(selected_genes: list[str]) -> list[dict]: return data_manager.all_genes_stat_df.filter( - data_manager.all_genes_stat_df["ensembl_gene_id"].is_in(selected_genes) + data_manager.all_genes_stat_df["gene_id"].is_in(selected_genes) ).to_dicts() @@ -35,7 +34,7 @@ def update_gene_stored_data( if ctx.triggered_id == "gene-stats-table": # updating selected genes if table_selected_rows is not None: - selected_genes = [row["ensembl_gene_id"] for row in table_selected_rows] + selected_genes = [row["gene_id"] for row in table_selected_rows] else: selected_genes = [] else: diff --git a/modules/local/dash_app/app/src/components/tables.py b/modules/local/dash_app/app/src/components/tables.py index cc68c81e..f9ba8605 100644 --- a/modules/local/dash_app/app/src/components/tables.py +++ b/modules/local/dash_app/app/src/components/tables.py @@ -1,8 +1,5 @@ import dash_ag_grid as dag - from src.utils import style - - from src.utils.data_management import DataManager data_manager = DataManager() @@ -39,7 +36,7 @@ animateRows=False, rowSelection=dict(mode="multiRow"), headerCheckboxSelection=False, - getRowId="params.data.ensembl_gene_id", + getRowId="params.data.gene_id", ), selectedRows=default_selected_rows, style=style.AG_GRID, diff --git a/modules/local/dash_app/app/src/utils/config.py b/modules/local/dash_app/app/src/utils/config.py index 50aaa5fa..0aac91b4 100644 --- a/modules/local/dash_app/app/src/utils/config.py +++ b/modules/local/dash_app/app/src/utils/config.py @@ -15,7 +15,7 @@ ALL_GENES_STAT_FILENAME = "all_genes_summary.csv" ALL_DESIGNS_FILENAME = "whole_design.csv" -ENSEMBL_GENE_ID_COLNAME = "ensembl_gene_id" +GENE_ID_COLNAME = "gene_id" STD_COLNAME = "standard_deviation" STABILITY_SCORE_COLNAME = "stability_score" diff --git a/modules/local/dash_app/app/src/utils/data_management.py b/modules/local/dash_app/app/src/utils/data_management.py index 6e38569c..4548ea4f 100644 --- a/modules/local/dash_app/app/src/utils/data_management.py +++ b/modules/local/dash_app/app/src/utils/data_management.py @@ -1,7 +1,7 @@ -import polars as pl -import pandas as pd from functools import lru_cache +import pandas as pd +import polars as pl from src.utils import config @@ -18,7 +18,7 @@ def get_all_count_data() -> pl.LazyFrame: def get_sorted_samples(self) -> list[str]: return sorted( - self.all_counts_lf.select(pl.exclude(config.ENSEMBL_GENE_ID_COLNAME)) + self.all_counts_lf.select(pl.exclude(config.GENE_ID_COLNAME)) .collect_schema() .names() ) @@ -59,15 +59,15 @@ def get_sorted_genes(self) -> list[str]: self.all_genes_stat_df.sort( by=config.STABILITY_SCORE_COLNAME, descending=False ) - .select(config.ENSEMBL_GENE_ID_COLNAME) + .select(config.GENE_ID_COLNAME) .to_series() .to_list() ) def get_gene_counts(self, gene: str) -> pd.Series: return ( - self.all_counts_lf.filter(pl.col(config.ENSEMBL_GENE_ID_COLNAME) == gene) - .select(pl.exclude(config.ENSEMBL_GENE_ID_COLNAME)) + self.all_counts_lf.filter(pl.col(config.GENE_ID_COLNAME) == gene) + .select(pl.exclude(config.GENE_ID_COLNAME)) .collect() .to_pandas() .iloc[0] diff --git a/modules/local/gprofiler/idmapping/main.nf b/modules/local/gprofiler/idmapping/main.nf index f3a35bb2..73f2d523 100644 --- a/modules/local/gprofiler/idmapping/main.nf +++ b/modules/local/gprofiler/idmapping/main.nf @@ -1,11 +1,8 @@ process GPROFILER_IDMAPPING { - label 'process_single' + label 'process_medium' - tag "${meta.dataset} on ${meta.platform_taxon}" - - // limiting to 8 threads at a time to avoid 429 errors with the G Profiler API server - maxForks 8 + tag "${species} IDs to ${gprofiler_target_db}" conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? @@ -13,37 +10,30 @@ process GPROFILER_IDMAPPING { 'community.wave.seqera.io/library/pandas_requests_tenacity:5ba56df089a9d718' }" input: - tuple val(meta), path(count_file) + path gene_id_file val species - val gene_id_mapping_file - val gene_metadata_file + val gprofiler_target_db output: - tuple val(meta), path('*.renamed.csv'), optional: true, emit: counts - path('*.metadata.csv'), optional: true, emit: metadata - path('*.mapping.csv'), optional: true, emit: mapping - tuple val(meta.dataset), path("failure_reason.txt"), optional: true, topic: id_mapping_failure_reason + path('mapped_gene_ids.csv'), optional: true, emit: mapping + path('gene_metadata.csv'), optional: true, emit: metadata tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions tuple val("${task.process}"), val('requests'), eval('python3 -c "import requests; print(requests.__version__)"'), topic: versions script: - def custom_mapping_arg = gene_id_mapping_file ? "--custom-mappings $gene_id_mapping_file" : "" - def custom_metadata_arg = gene_metadata_file ? "--custom-metadata $gene_metadata_file" : "" """ - map_ids_to_ensembl.py \\ - --count-file "$count_file" \\ + gprofiler_map_ids.py \\ + --gene-ids $gene_id_file \\ --species "$species" \\ - $custom_mapping_arg \\ - $custom_metadata_arg + --target-db "$gprofiler_target_db" """ stub: """ - touch fake_renamed.csv - touch fake_metadata.csv - touch fake_mapping.json + touch mapped_gene_ids.csv + touch gene_metadata.csv """ } diff --git a/modules/local/rename_gene_ids/main.nf b/modules/local/rename_gene_ids/main.nf new file mode 100644 index 00000000..f22af9a6 --- /dev/null +++ b/modules/local/rename_gene_ids/main.nf @@ -0,0 +1,41 @@ +process RENAME_GENE_IDS { + + label 'process_low' + + tag "${meta.dataset}" + + conda "${moduleDir}/spec-file.txt" + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/5c/5c28c8e613c062828aaee4b950029bc90a1a1aa94d5f61016a588c8ec7be8b65/data': + 'community.wave.seqera.io/library/pandas_requests_tenacity:5ba56df089a9d718' }" + + input: + tuple val(meta), path(count_file) + path gene_id_mapping_file + path custom_gene_id_mapping_file + + output: + tuple val(meta), path('*.renamed.csv'), optional: true, emit: counts + tuple val(meta.dataset), path("failure_reason.txt"), optional: true, topic: renaming_failure_reason + tuple val(meta.dataset), path("warning_reason.txt"), optional: true, topic: renaming_warning_reason + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions + tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions + tuple val("${task.process}"), val('requests'), eval('python3 -c "import requests; print(requests.__version__)"'), topic: versions + + script: + def mapping_arg = gene_id_mapping_file ? "--mappings $gene_id_mapping_file" : "" + def custom_mapping_arg = custom_gene_id_mapping_file ? "--custom-mappings $custom_gene_id_mapping_file" : "" + """ + rename_gene_ids.py \\ + --count-file "$count_file" \\ + $mapping_arg \\ + $custom_mapping_arg + """ + + + stub: + """ + touch fake_renamed.csv + """ + +} diff --git a/modules/local/rename_gene_ids/spec-file.txt b/modules/local/rename_gene_ids/spec-file.txt new file mode 100644 index 00000000..3233c10b --- /dev/null +++ b/modules/local/rename_gene_ids/spec-file.txt @@ -0,0 +1,57 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda#3cd1a7238a0dd3d0860fdefc496cc854 +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda#9e60c55e725c20d23125a5f0dd69af5d +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda#6d11a5edae89fe413c0569f16d308f5a +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda#e66f2b8ad787e7beb0f846e4bd7e8493 +https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62ee74e96c5ebb0af99386de58cf9553 +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_0.conda#e31316a586cac398b1fcdb10ace786b9 +https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda#db0bfbe7dd197b68ad5f30333bae6ce0 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 +https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc +https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda#d864d34357c3b65a4b731f78c0801dc4 +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.2-h6cd9bfd_0.conda#b04c7eda6d7dab1e6503135e7fad4d25 +https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b +https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.6.15-hbd8a1cb_0.conda#72525f07d72806e3b639ad4504c30ce5 +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda#c87df2ab1448ba69169652ab9547082d +https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 +https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 +https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a +https://conda.anaconda.org/conda-forge/linux-64/python-3.12.8-h9e4cc4f_1_cpython.conda#7fd2fd79436d9b473812f14e86746844 +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda#0dfcdc155cf23812a0c9deada86fb723 +https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h2ec8cdc_3.conda#a32e0c069f6c3dcac635f7b0b0dac67e +https://conda.anaconda.org/conda-forge/noarch/certifi-2025.6.15-pyhd8ed1ab_0.conda#781d068df0cc2407d4db0ecfbb29225b +https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda#12c566707c80111f9799308d9e265aef +https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py312h06ac9bb_0.conda#a861504bbea4161a9170b85d4d2be840 +https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.2-pyhd8ed1ab_0.conda#40fe4284b8b5835a9073a645139f35af +https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda#0a802cb9888dd14eeefc611f05c40b6e +https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda#8e6923fc12f1fe8f8c4e5c9f343256ac +https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda#b4754fb1bdcb70c8fd54f918301582c6 +https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda#39a4f67be3286c86d696df570b1201b7 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda#530566b68c3b8ce7eec4cd047eae19fe +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda#bfbca721fd33188ef923dfe9ba172f29 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda#323dc8f259224d13078aaf7ce96c3efe +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-32_h59b9bed_openblas.conda#2af9f3d5c2e39f417ce040f5a35c40c6 +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-32_he106b2a_openblas.conda#3d3f9355e52f269cd8bc2c440d8a5263 +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-32_h7ac8fdf_openblas.conda#6c3f04ccb6c578138e9f9899da0bd714 +https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.1-py312h6cf2f7f_0.conda#7e086a30150af2536a1059885368dcf0 +https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda#a451d576819089b0d672f18768be0f65 +https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 +https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 +https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 +https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.3-py312hf9745cd_3.conda#2979458c23c7755683a0598fb33e7666 +https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e +https://conda.anaconda.org/conda-forge/noarch/wheel-0.45.1-pyhd8ed1ab_1.conda#75cb7132eb58d97896e173ef12ac9986 +https://conda.anaconda.org/conda-forge/noarch/pip-25.1.1-pyh8b19718_0.conda#32d0781ace05105cc99af55d36cbec7c +https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda#461219d1a5bd61342293efa2c0c90eac +https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py312h66e93f0_2.conda#630db208bc7bbb96725ce9832c7423bb +https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda#436c165519e140cb08d246a4472a9d6a +https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda#a9b9368f3701a417eac9edbcae7cb737 +https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.2-pyhd8ed1ab_0.conda#5d99943f2ae3cc69e1ada12ce9d4d701 diff --git a/nextflow.config b/nextflow.config index 32989f4b..cbc454f6 100644 --- a/nextflow.config +++ b/nextflow.config @@ -37,6 +37,7 @@ params { exclude_geo_accessions_file = null // ID mapping + gprofiler_target_db = "ENTREZGENE" gene_metadata = null gene_id_mapping_file = null skip_id_mapping = false diff --git a/nextflow_schema.json b/nextflow_schema.json index f395d920..b1f31aae 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -183,6 +183,14 @@ "fa_icon": "fas fa-ban", "help": "If you don't want to map gene IDs with g:Profiler, you can skip this step by providing `--skip_id_mapping`. It can be in particular useful if the g:Profiler is down and if you already have a custom mapping file." }, + "gprofiler_target_db": { + "type": "string", + "description": "Target database for g:Profiler", + "fa_icon": "fas fa-divide", + "enum": ["ENTREZGENE", "ENSG"], + "default": "ENTREZGENE", + "help_text": "Target database for g:Profiler. By default, it is set to ENTREZGENE (NCBI Entrez Gene IDs). You can choose to use ENSEMBL IDs instead." + }, "gene_id_mapping_file": { "type": "string", "format": "file-path", @@ -191,7 +199,7 @@ "mimetype": "text/csv", "pattern": "^\\S+\\.(csv|tsv|dat)$", "description": "Custom gene id mapping file", - "help_text": "Path to comma-separated file containing custom gene id mappings. Each row represents a mapping from the original gene ID in your count datasets to the ensembl ID in g:Profiler. The mapping file should be a comma-separated file with 2 columns (original_gene_id and ensembl_gene_id) and a header row.", + "help_text": "Path to comma-separated file containing custom gene id mappings. Each row represents a mapping from the original gene ID in your count datasets to a prefered gene ID. The mapping file should be a comma-separated file with 2 columns (original_gene_id and gene_id) and a header row.", "fa_icon": "fas fa-file" }, "gene_metadata": { @@ -202,7 +210,7 @@ "mimetype": "text/csv", "pattern": "^\\S+\\.(csv|tsv|dat)$", "description": "Custom gene metadata file", - "help_text": "Path to comma-separated file containing custom gene metadata information. Each row represents a gene and links its ensembl gene ID to its name and description. The metadata file should be a comma-separated file with 3 columns (ensembl_gene_id, name and description) and a header row.", + "help_text": "Path to comma-separated file containing custom gene metadata information. Each row represents a gene and links its gene ID to its name and description. The metadata file should be a comma-separated file with 3 columns (gene_id, name and description) and a header row.", "fa_icon": "fas fa-file" } } diff --git a/subworkflows/local/idmapping/main.nf b/subworkflows/local/idmapping/main.nf index 3aa0800b..b2a8e48f 100644 --- a/subworkflows/local/idmapping/main.nf +++ b/subworkflows/local/idmapping/main.nf @@ -1,4 +1,6 @@ +include { COLLECT_GENE_IDS } from '../../../modules/local/collect_gene_ids' include { GPROFILER_IDMAPPING } from '../../../modules/local/gprofiler/idmapping' +include { RENAME_GENE_IDS } from '../../../modules/local/rename_gene_ids' /* ======================================================================================== @@ -11,35 +13,39 @@ workflow ID_MAPPING { take: ch_counts species - ch_gene_id_mapping - ch_gene_metadata + skip_id_mapping + gprofiler_target_db + ch_custom_gene_id_mapping + ch_custom_gene_metadata main: - ch_counts - .map { - meta, file -> - def platform_taxon = meta.platform_taxon ?: species - meta.platform_taxon = platform_taxon - [ meta, file ] - } - .set { ch_counts } - - GPROFILER_IDMAPPING( + ch_gene_id_mapping = Channel.empty() + + if ( !params.skip_id_mapping ) { + + COLLECT_GENE_IDS( + ch_counts.map{ meta, file -> file }.collect() + ) + + GPROFILER_IDMAPPING( + COLLECT_GENE_IDS.out.gene_ids, + species, + gprofiler_target_db + ) + GPROFILER_IDMAPPING.out.mapping.set { ch_gene_id_mapping } + } + + RENAME_GENE_IDS( ch_counts, - species, ch_gene_id_mapping, - ch_gene_metadata + ch_custom_gene_id_mapping ) - GPROFILER_IDMAPPING.out.counts.set { ch_counts } - GPROFILER_IDMAPPING.out.mapping.set { ch_gene_id_mapping } - GPROFILER_IDMAPPING.out.metadata.set { ch_gene_metadata } - emit: - counts = GPROFILER_IDMAPPING.out.counts - mapping = GPROFILER_IDMAPPING.out.mapping + counts = RENAME_GENE_IDS.out.counts + mapping = ch_gene_id_mapping metadata = GPROFILER_IDMAPPING.out.metadata } diff --git a/subworkflows/local/merge_data/main.nf b/subworkflows/local/merge_data/main.nf index 325612fa..20a1d999 100644 --- a/subworkflows/local/merge_data/main.nf +++ b/subworkflows/local/merge_data/main.nf @@ -106,12 +106,12 @@ workflow MERGE_DATA { .unique() .collectFile( name: 'whole_gene_id_mapping.csv', - seed: "original_gene_id,ensembl_gene_id", + seed: "original_gene_id,gene_id", newLine: true, sort: true, storeDir: "${params.outdir}/idmapping/" ) { - item -> "${item.original_gene_id},${item.ensembl_gene_id}" + item -> "${item.original_gene_id},${item.gene_id}" } .ifEmpty([]) // handle case where there are no mappings .set { ch_whole_gene_id_mapping } @@ -126,12 +126,12 @@ workflow MERGE_DATA { .unique() .collectFile( name: 'whole_gene_metadata.csv', - seed: "ensembl_gene_id,name,description", + seed: "gene_id,name,description", newLine: true, sort: true, storeDir: "${params.outdir}/idmapping/" ) { - item -> "${item.ensembl_gene_id},${item.name},${item.description}" + item -> "${item.gene_id},${item.name},${item.description}" } .ifEmpty([]) // handle case where there are no mappings .set { ch_whole_gene_metadata } diff --git a/subworkflows/local/multiqc/main.nf b/subworkflows/local/multiqc/main.nf index 4379626a..ad59e234 100644 --- a/subworkflows/local/multiqc/main.nf +++ b/subworkflows/local/multiqc/main.nf @@ -72,10 +72,22 @@ workflow MULTIQC_WORKFLOW { } .set { ch_geo_warning_reasons } - Channel.topic('id_mapping_failure_reason') + Channel.topic('renaming_warning_reason') .map { accession, file -> [ accession, file.readLines()[0] ] } .collectFile( - name: 'id_mapping_failure_reasons.tsv', + name: 'renaming_warning_reasons.tsv', + seed: "Dataset\tReason", + newLine: true, + storeDir: "${params.outdir}/warnings/" + ) { + item -> "${item[0]}\t${item[1]}" + } + .set { ch_id_mapping_failure_reasons } + + Channel.topic('renaming_failure_reason') + .map { accession, file -> [ accession, file.readLines()[0] ] } + .collectFile( + name: 'renaming_failure_reasons.tsv', seed: "Dataset\tReason", newLine: true, storeDir: "${params.outdir}/errors/" diff --git a/tests/modules/local/geo/getdata/main.nf.test b/tests/modules/local/geo/getdata/main.nf.test index a952a1a0..c7cd11f9 100644 --- a/tests/modules/local/geo/getdata/main.nf.test +++ b/tests/modules/local/geo/getdata/main.nf.test @@ -198,4 +198,29 @@ nextflow_process { } + test("Drosophila simulans - Mismatch in suppl data colnames / design") { + + when { + + process { + """ + input[0] = [ + [ id: "test" ], + "GSE49127" + ] + input[1] = "drosophila_simulans" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert process.out.counts.size() == 0 }, + { assert snapshot(process.out).match() } + ) + } + + } + } diff --git a/tests/modules/local/gprofiler/idmapping/main.nf.test b/tests/modules/local/gprofiler/idmapping/main.nf.test new file mode 100644 index 00000000..e307a561 --- /dev/null +++ b/tests/modules/local/gprofiler/idmapping/main.nf.test @@ -0,0 +1,31 @@ +nextflow_process { + + name "Test Process GPROFILER_IDMAPPING" + script "modules/local/gprofiler/idmapping/main.nf" + process "GPROFILER_IDMAPPING" + tag "gprofiler_idmapping" + + test("Should run without failures") { + + when { + params { + // define parameters here. Example: + // outdir = "tests/results" + } + process { + """ + input[0] = file("$projectDir/tests/test_data/idmapping/gene_ids/gene_ids.txt", checkIfExists: true) + input[1] = "Solanum tuberosum" + input[2] = "ENTREZGENE" + """ + } + } + + then { + assert process.success + assert snapshot(process.out).match() + } + + } + +} diff --git a/tests/modules/local/gprofiler/idmapping/main.nf.test.snap b/tests/modules/local/gprofiler/idmapping/main.nf.test.snap new file mode 100644 index 00000000..d919decf --- /dev/null +++ b/tests/modules/local/gprofiler/idmapping/main.nf.test.snap @@ -0,0 +1,43 @@ +{ + "Should run without failures": { + "content": [ + { + "0": [ + + ], + "1": [ + + ], + "2": [ + [ + "GPROFILER_IDMAPPING", + "python", + "3.13.5" + ] + ], + "3": [ + [ + "GPROFILER_IDMAPPING", + "pandas", + "2.3.1" + ] + ], + "4": [ + [ + "GPROFILER_IDMAPPING", + "requests", + "2.32.4" + ] + ], + "metadata": [ + + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-20T16:46:09.976019836" + } +} \ No newline at end of file diff --git a/tests/modules/local/idmapping/gprofiler/main.nf.test b/tests/modules/local/rename_gene_ids/main.nf.test similarity index 91% rename from tests/modules/local/idmapping/gprofiler/main.nf.test rename to tests/modules/local/rename_gene_ids/main.nf.test index 06a58471..cf84800d 100644 --- a/tests/modules/local/idmapping/gprofiler/main.nf.test +++ b/tests/modules/local/rename_gene_ids/main.nf.test @@ -1,11 +1,11 @@ nextflow_process { - name "Test Process GPROFILER_IDMAPPING" - script "modules/local/gprofiler/idmapping/main.nf" - process "GPROFILER_IDMAPPING" - tag "idmapping" + name "Test Process RENAME_GENE_IDS" + script "modules/local/rename_gene_ids/main.nf" + process "RENAME_GENE_IDS" + tag "rename_gene_ids" - test("Map Ensembl IDs to themselves") { + test("Map Ensembl IDs") { when { process { @@ -16,9 +16,8 @@ nextflow_process { file("$projectDir/tests/test_data/idmapping/base/counts.ensembl_ids.csv", checkIfExists: true) ] ) - input[1] = "Beta vulgaris" + input[1] = "Solanum tuberosum" input[2] = Channel.value([]) - input[3] = Channel.value([]) """ } } @@ -166,7 +165,6 @@ nextflow_process { ) input[1] = "Beta vulgaris" input[2] = Channel.fromPath( "$projectDir/tests/test_data/idmapping/custom/mapping.csv", checkIfExists: true) - input[3] = Channel.fromPath( "$projectDir/tests/test_data/idmapping/custom/metadata.csv", checkIfExists: true) """ } } @@ -195,7 +193,6 @@ nextflow_process { ) input[1] = "Beta vulgaris" input[2] = Channel.fromPath( "$projectDir/tests/test_data/idmapping/tsv/mapping.tsv", checkIfExists: true) - input[3] = Channel.fromPath( "$projectDir/tests/test_data/idmapping/tsv/metadata.tsv", checkIfExists: true) """ } } diff --git a/tests/modules/local/idmapping/gprofiler/main.nf.test.snap b/tests/modules/local/rename_gene_ids/main.nf.test.snap similarity index 100% rename from tests/modules/local/idmapping/gprofiler/main.nf.test.snap rename to tests/modules/local/rename_gene_ids/main.nf.test.snap diff --git a/tests/subworkflows/local/genorm/run_genorm.py b/tests/subworkflows/local/genorm/run_genorm.py index 3d91ae26..9704d7dc 100644 --- a/tests/subworkflows/local/genorm/run_genorm.py +++ b/tests/subworkflows/local/genorm/run_genorm.py @@ -1,11 +1,12 @@ -import pandas as pd -import numpy as np import sys +import numpy as np +import pandas as pd + file = sys.argv[1] # Expression data for three control genes. counts = pd.read_parquet(file) -counts.set_index("ensembl_gene_id", inplace=True) +counts.set_index("gene_id", inplace=True) counts = counts.T.replace(0, 1e-8) diff --git a/tests/test_data/aggregate_results/mapping.csv b/tests/test_data/aggregate_results/mapping.csv index 51303291..b3c00132 100644 --- a/tests/test_data/aggregate_results/mapping.csv +++ b/tests/test_data/aggregate_results/mapping.csv @@ -1,4 +1,4 @@ -original_gene_id,ensembl_gene_id +original_gene_id,gene_id ENSRNA049434199,ENSRNA049454747 ENSRNA049434246,ENSRNA049454887 ENSRNA049434252,SNSRNA049434252 diff --git a/tests/test_data/aggregate_results/metadata.csv b/tests/test_data/aggregate_results/metadata.csv index ec53b6cc..5e8f3142 100644 --- a/tests/test_data/aggregate_results/metadata.csv +++ b/tests/test_data/aggregate_results/metadata.csv @@ -1,4 +1,4 @@ -ensembl_gene_id,name,description +gene_id,name,description ENSRNA049454747,geneA,descriptionA ENSRNA049454887,geneB,descriptionB ENSRNA049454747,geneC,descriptionC diff --git a/tests/test_data/aggregate_results/microarray_stats_all_genes.csv b/tests/test_data/aggregate_results/microarray_stats_all_genes.csv index d1d2fecb..0fe7d08f 100644 --- a/tests/test_data/aggregate_results/microarray_stats_all_genes.csv +++ b/tests/test_data/aggregate_results/microarray_stats_all_genes.csv @@ -1,4 +1,4 @@ -ensembl_gene_id,microarray_mean,microarray_standard_deviation,microarray_median,microarray_median_absolute_deviation,microarray_coefficient_of_variation,microarray_robust_coefficient_of_variation_median,microarray_ratio_nulls_in_all_samples,microarray_ratio_nulls_in_valid_samples,microarray_ratio_zeros,microarray_expression_level_quantile_interval +gene_id,microarray_mean,microarray_standard_deviation,microarray_median,microarray_median_absolute_deviation,microarray_coefficient_of_variation,microarray_robust_coefficient_of_variation_median,microarray_ratio_nulls_in_all_samples,microarray_ratio_nulls_in_valid_samples,microarray_ratio_zeros,microarray_expression_level_quantile_interval ENSRNA049454747,0.9375,0.11572751247156893,1.0,0.0,0.12344267996967352,0.0,0.0,0.0,0.0,99 ENSRNA049454887,0.140625,0.15580293184477811,0.125,0.125,1.1079319597850887,1.4826,0.0,0.0,0.5,11 ENSRNA049454931,0.4453125,0.12246309575308217,0.4375,0.0625,0.2750048466034126,0.2118,0.0,0.0,0.0,66 diff --git a/tests/test_data/aggregate_results/rnaseq_stats_all_genes.csv b/tests/test_data/aggregate_results/rnaseq_stats_all_genes.csv index 87a27785..de9af372 100644 --- a/tests/test_data/aggregate_results/rnaseq_stats_all_genes.csv +++ b/tests/test_data/aggregate_results/rnaseq_stats_all_genes.csv @@ -1,4 +1,4 @@ -ensembl_gene_id,rnaseq_mean,rnaseq_standard_deviation,rnaseq_median,rnaseq_median_absolute_deviation,rnaseq_coefficient_of_variation,rnaseq_robust_coefficient_of_variation_median,rnaseq_ratio_nulls_in_all_samples,rnaseq_ratio_nulls_in_valid_samples,rnaseq_ratio_zeros,rnaseq_expression_level_quantile_interval +gene_id,rnaseq_mean,rnaseq_standard_deviation,rnaseq_median,rnaseq_median_absolute_deviation,rnaseq_coefficient_of_variation,rnaseq_robust_coefficient_of_variation_median,rnaseq_ratio_nulls_in_all_samples,rnaseq_ratio_nulls_in_valid_samples,rnaseq_ratio_zeros,rnaseq_expression_level_quantile_interval ENSRNA049454747,0.9375,0.11572751247156893,1.0,0.0,0.12344267996967352,0.0,0.0,0.0,0.0,99 ENSRNA049454887,0.140625,0.15580293184477811,0.125,0.125,1.1079319597850887,1.4826,0.0,0.0,0.5,11 ENSRNA049454931,0.4453125,0.12246309575308217,0.4375,0.0625,0.2750048466034126,0.2118,0.0,0.0,0.0,66 diff --git a/tests/test_data/base_statistics/output/stats_all_genes.csv b/tests/test_data/base_statistics/output/stats_all_genes.csv index cda6fe60..85153b9e 100644 --- a/tests/test_data/base_statistics/output/stats_all_genes.csv +++ b/tests/test_data/base_statistics/output/stats_all_genes.csv @@ -1,4 +1,4 @@ -ensembl_gene_id,mean,standard_deviation,median,median_absolute_deviation,coefficient_of_variation,robust_coefficient_of_variation_median,ratio_nulls_in_all_samples,ratio_nulls_in_valid_samples,ratio_zeros,expression_level_quantile_interval +gene_id,mean,standard_deviation,median,median_absolute_deviation,coefficient_of_variation,robust_coefficient_of_variation_median,ratio_nulls_in_all_samples,ratio_nulls_in_valid_samples,ratio_zeros,expression_level_quantile_interval ENSRNA049454747,0.9375,0.11572751247156893,1.0,0.0,0.12344267996967352,0.0,0.0,0.0,0.0,99 ENSRNA049454887,0.140625,0.15580293184477811,0.125,0.125,1.1079319597850887,1.4826,0.0,0.0,0.5,11 ENSRNA049454931,0.4453125,0.12246309575308217,0.4375,0.0625,0.2750048466034126,0.2118,0.0,0.0,0.0,66 diff --git a/tests/test_data/compute_gene_statistics/input/mapping1.csv b/tests/test_data/compute_gene_statistics/input/mapping1.csv index d8abe730..8c5865b4 100644 --- a/tests/test_data/compute_gene_statistics/input/mapping1.csv +++ b/tests/test_data/compute_gene_statistics/input/mapping1.csv @@ -1,4 +1,4 @@ -original_gene_id,ensembl_gene_id +original_gene_id,gene_id Q8VWG3,AT1G34790 Q9FJA2,AT5G35550 Q8RYD9,AT5G23260 diff --git a/tests/test_data/compute_gene_statistics/input/mapping2.csv b/tests/test_data/compute_gene_statistics/input/mapping2.csv index 305ccbea..080dbefd 100644 --- a/tests/test_data/compute_gene_statistics/input/mapping2.csv +++ b/tests/test_data/compute_gene_statistics/input/mapping2.csv @@ -1,4 +1,4 @@ -original_gene_id,ensembl_gene_id +original_gene_id,gene_id Q8VWG3,AT1G34790 Q9FJA2,AT5G35550 Q8RYD9,AT5G23260 diff --git a/tests/test_data/compute_gene_statistics/input/mapping3.csv b/tests/test_data/compute_gene_statistics/input/mapping3.csv index e20257b0..c8fbe3f9 100644 --- a/tests/test_data/compute_gene_statistics/input/mapping3.csv +++ b/tests/test_data/compute_gene_statistics/input/mapping3.csv @@ -1,4 +1,4 @@ -original_gene_id,ensembl_gene_id +original_gene_id,gene_id Q8VWG3,AT1G34790 Q9FJA2,AT5G35550 Q8RYD9,AT5G23260 diff --git a/tests/test_data/compute_gene_statistics/input/metadata1.csv b/tests/test_data/compute_gene_statistics/input/metadata1.csv index ea4db477..399628bf 100644 --- a/tests/test_data/compute_gene_statistics/input/metadata1.csv +++ b/tests/test_data/compute_gene_statistics/input/metadata1.csv @@ -1,4 +1,4 @@ -ensembl_gene_id,name,description +gene_id,name,description AT1G34790,TT1,C2H2 and C2HC zinc fingers superfamily protein AT5G35550,TT2,Duplicated homeodomain-like superfamily protein AT5G23260,TT16,K-box region and MADS-box transcription factor family protein diff --git a/tests/test_data/compute_gene_statistics/input/metadata2.csv b/tests/test_data/compute_gene_statistics/input/metadata2.csv index b5890d89..69fadca4 100644 --- a/tests/test_data/compute_gene_statistics/input/metadata2.csv +++ b/tests/test_data/compute_gene_statistics/input/metadata2.csv @@ -1,4 +1,4 @@ -ensembl_gene_id,name,description +gene_id,name,description AT1G34790,TT1,C2H2 and C2HC zinc fingers superfamily protein AT5G35550,TT2,Duplicated homeodomain-like superfamily protein AT5G23260,TT16,K-box region and MADS-box transcription factor family protein diff --git a/tests/test_data/compute_gene_statistics/input/microarray_stats_all_genes.csv b/tests/test_data/compute_gene_statistics/input/microarray_stats_all_genes.csv index acb56a11..e40090d6 100644 --- a/tests/test_data/compute_gene_statistics/input/microarray_stats_all_genes.csv +++ b/tests/test_data/compute_gene_statistics/input/microarray_stats_all_genes.csv @@ -1,4 +1,4 @@ -ensembl_gene_id,microarray_mean,microarray_standard_deviation,microarray_median,microarray_median_absolute_deviation,microarray_variation_coefficient,microarray_total_nb_nulls,microarray_nb_nulls_valid_samples,microarray_stability_score,microarray_expression_level_quantile_interval +gene_id,microarray_mean,microarray_standard_deviation,microarray_median,microarray_median_absolute_deviation,microarray_variation_coefficient,microarray_total_nb_nulls,microarray_nb_nulls_valid_samples,microarray_stability_score,microarray_expression_level_quantile_interval AT1G34790,0.6041722385984585,0.2965945020346847,0.8210634736950527,0.07852066041592076,0.49091051042450434,678,643,1.4392880915454482,71 AT5G35550,0.04211885958141837,0.017403154131542625,0.04081717758449555,0.00889668425147598,0.41319148487155133,678,643,1.3615690659924953,0 AT5G23260,0.3265572056851324,0.12636844695328353,0.2977133397782717,0.09861099799987358,0.3869718528738528,678,643,1.3353494339947967,35 diff --git a/tests/test_data/compute_gene_statistics/input/rnaseq_stats_all_genes.csv b/tests/test_data/compute_gene_statistics/input/rnaseq_stats_all_genes.csv index 789ddd31..e4c7327d 100644 --- a/tests/test_data/compute_gene_statistics/input/rnaseq_stats_all_genes.csv +++ b/tests/test_data/compute_gene_statistics/input/rnaseq_stats_all_genes.csv @@ -1,4 +1,4 @@ -ensembl_gene_id,rnaseq_mean,rnaseq_standard_deviation,rnaseq_median,rnaseq_median_absolute_deviation,rnaseq_variation_coefficient,rnaseq_total_nb_nulls,rnaseq_nb_nulls_valid_samples,rnaseq_stability_score,rnaseq_expression_level_quantile_interval +gene_id,rnaseq_mean,rnaseq_standard_deviation,rnaseq_median,rnaseq_median_absolute_deviation,rnaseq_variation_coefficient,rnaseq_total_nb_nulls,rnaseq_nb_nulls_valid_samples,rnaseq_stability_score,rnaseq_expression_level_quantile_interval AT1G34790,0.029004004004004002,0.061217504567865136,0.0,0.0,2.110657016852365,345,336,3.0544772415714663,0 AT5G35550,0.2921254587921254,0.028005675342417956,0.28128128128128127,0.025025025025025016,0.09586865676896245,356,347,1.070587757892558,41 AT5G23260,0.051621388830691145,0.04715133948046024,0.04154154154154154,0.027027027027027035,0.9134070304677029,322,313,1.7926205136137703,3 diff --git a/tests/test_data/compute_stability_scores/input/genorm.m_measures.csv b/tests/test_data/compute_stability_scores/input/genorm.m_measures.csv index 9a7ab4e1..b4b6ae10 100644 --- a/tests/test_data/compute_stability_scores/input/genorm.m_measures.csv +++ b/tests/test_data/compute_stability_scores/input/genorm.m_measures.csv @@ -1,4 +1,4 @@ -ensembl_gene_id,genorm_m_measure +gene_id,genorm_m_measure ENSRNA049454747,0.16034699963469335 ENSRNA049454887,0.525024672172669794 ENSRNA049454931,0.264017707597323344 diff --git a/tests/test_data/compute_stability_scores/input/stability_values.normfinder.csv b/tests/test_data/compute_stability_scores/input/stability_values.normfinder.csv index c9a22636..238572e2 100644 --- a/tests/test_data/compute_stability_scores/input/stability_values.normfinder.csv +++ b/tests/test_data/compute_stability_scores/input/stability_values.normfinder.csv @@ -1,4 +1,4 @@ -ensembl_gene_id,normfinder_stability_value +gene_id,normfinder_stability_value ENSRNA049454747,0.036034699963469335 ENSRNA049454887,0.05024672172669794 ENSRNA049454931,0.014017707597323344 diff --git a/tests/test_data/compute_stability_scores/input/stats_all_genes.csv b/tests/test_data/compute_stability_scores/input/stats_all_genes.csv index cda6fe60..85153b9e 100644 --- a/tests/test_data/compute_stability_scores/input/stats_all_genes.csv +++ b/tests/test_data/compute_stability_scores/input/stats_all_genes.csv @@ -1,4 +1,4 @@ -ensembl_gene_id,mean,standard_deviation,median,median_absolute_deviation,coefficient_of_variation,robust_coefficient_of_variation_median,ratio_nulls_in_all_samples,ratio_nulls_in_valid_samples,ratio_zeros,expression_level_quantile_interval +gene_id,mean,standard_deviation,median,median_absolute_deviation,coefficient_of_variation,robust_coefficient_of_variation_median,ratio_nulls_in_all_samples,ratio_nulls_in_valid_samples,ratio_zeros,expression_level_quantile_interval ENSRNA049454747,0.9375,0.11572751247156893,1.0,0.0,0.12344267996967352,0.0,0.0,0.0,0.0,99 ENSRNA049454887,0.140625,0.15580293184477811,0.125,0.125,1.1079319597850887,1.4826,0.0,0.0,0.5,11 ENSRNA049454931,0.4453125,0.12246309575308217,0.4375,0.0625,0.2750048466034126,0.2118,0.0,0.0,0.0,66 diff --git a/tests/test_data/idmapping/custom/mapping.csv b/tests/test_data/idmapping/custom/mapping.csv index 9cb9aee4..cd43e30f 100644 --- a/tests/test_data/idmapping/custom/mapping.csv +++ b/tests/test_data/idmapping/custom/mapping.csv @@ -1,4 +1,4 @@ -original_gene_id,ensembl_gene_id +original_gene_id,gene_id ENSRNA049434199,SNSRNA049434199 ENSRNA049434246,SNSRNA049434246 ENSRNA049434252,SNSRNA049434252 diff --git a/tests/test_data/idmapping/custom/metadata.csv b/tests/test_data/idmapping/custom/metadata.csv index ca3c36b9..0c4095a9 100644 --- a/tests/test_data/idmapping/custom/metadata.csv +++ b/tests/test_data/idmapping/custom/metadata.csv @@ -1,4 +1,4 @@ -ensembl_gene_id,name,description +gene_id,name,description SNSRNA049434199,geneA,descriptionA SNSRNA049434246,geneB,descriptionB SNSRNA049434252,geneC,descriptionC diff --git a/tests/test_data/idmapping/gene_ids/gene_ids.txt b/tests/test_data/idmapping/gene_ids/gene_ids.txt new file mode 100644 index 00000000..94233419 --- /dev/null +++ b/tests/test_data/idmapping/gene_ids/gene_ids.txt @@ -0,0 +1,9 @@ +ENSRNA049434199 +ENSRNA049434246 +ENSRNA049434252 +840386 +833520 +832390 +Q8VWG3 +Q9FJA2 +Q8RYD9 diff --git a/tests/test_data/idmapping/tsv/mapping.tsv b/tests/test_data/idmapping/tsv/mapping.tsv index fa258254..c425f89f 100644 --- a/tests/test_data/idmapping/tsv/mapping.tsv +++ b/tests/test_data/idmapping/tsv/mapping.tsv @@ -1,4 +1,4 @@ -original_gene_id ensembl_gene_id +original_gene_id gene_id ENSRNA049434199 SNSRNA049434199 ENSRNA049434246 SNSRNA049434246 ENSRNA049434252 SNSRNA049434252 diff --git a/tests/test_data/idmapping/tsv/metadata.tsv b/tests/test_data/idmapping/tsv/metadata.tsv index d2406e51..11eae353 100644 --- a/tests/test_data/idmapping/tsv/metadata.tsv +++ b/tests/test_data/idmapping/tsv/metadata.tsv @@ -1,4 +1,4 @@ -ensembl_gene_id name description +gene_id name description SNSRNA049434199 geneA descriptionA SNSRNA049434246 geneB descriptionB SNSRNA049434252 geneC descriptionC diff --git a/tests/test_data/input_datasets/mapping.csv b/tests/test_data/input_datasets/mapping.csv index 5eac321f..04489426 100644 --- a/tests/test_data/input_datasets/mapping.csv +++ b/tests/test_data/input_datasets/mapping.csv @@ -1,4 +1,4 @@ -original_gene_id,ensembl_gene_id +original_gene_id,gene_id ENSRNA049453121,SNSRNA049434199 ENSRNA049453138,SNSRNA049434246 ENSRNA049454388,SNSRNA049434252 diff --git a/tests/test_data/input_datasets/metadata.csv b/tests/test_data/input_datasets/metadata.csv index a7cd3a84..fcccf222 100644 --- a/tests/test_data/input_datasets/metadata.csv +++ b/tests/test_data/input_datasets/metadata.csv @@ -1,4 +1,4 @@ -ensembl_gene_id,name,description +gene_id,name,description ENSRNA049453121,geneA,descriptionA ENSRNA049453138,geneB,descriptionB ENSRNA049454388,geneC,descriptionC diff --git a/tests/test_data/input_datasets/microarray.normalised.csv b/tests/test_data/input_datasets/microarray.normalised.csv index 1f93b0ca..81f3f904 100644 --- a/tests/test_data/input_datasets/microarray.normalised.csv +++ b/tests/test_data/input_datasets/microarray.normalised.csv @@ -1,4 +1,4 @@ -ensembl_gene_id,GSM1528575,GSM1528576,GSM1528579,GSM1528583,GSM1528584,GSM1528585,GSM1528580,GSM1528586,GSM1528582,GSM1528578,GSM1528581,GSM1528577 +gene_id,GSM1528575,GSM1528576,GSM1528579,GSM1528583,GSM1528584,GSM1528585,GSM1528580,GSM1528586,GSM1528582,GSM1528578,GSM1528581,GSM1528577 ENSRNA049453121,20925.1255070264,136184.261516502,144325.370645564,89427.0987612997,164143.182734208,34178.6378088171,28842.7323281157,76973.395782103,41906.9367255656,44756.5602263121,252562.049703724,6953.65643340122 ENSRNA049453138,196173.051628372,16607.8367703051,344972.83715281,22602.4535330758,13678.598561184,104546.421532852,15451.4637472048,71664.8857281649,160643.257448002,91459.0578537683,88396.7173963033,281623.08555275 ENSRNA049454388,91547.4240932405,11625.4857392136,84483.143792525,80582.6604222701,218857.576978944,58304.7350856292,42234.0009090266,88475.1675656357,87306.1181782617,17513.436610296,90922.3378933406,76490.2207674135 diff --git a/tests/test_data/input_datasets/rnaseq.raw.csv b/tests/test_data/input_datasets/rnaseq.raw.csv index 4d558cc2..5688c066 100644 --- a/tests/test_data/input_datasets/rnaseq.raw.csv +++ b/tests/test_data/input_datasets/rnaseq.raw.csv @@ -1,4 +1,4 @@ -ensembl_gene_id,ESM1528575,ESM1528576,ESM1528579,ESM1528583,ESM1528584,ESM1528585,ESM1528580,ESM1528586,ESM1528582,ESM1528578,ESM1528581,ESM1528577 +gene_id,ESM1528575,ESM1528576,ESM1528579,ESM1528583,ESM1528584,ESM1528585,ESM1528580,ESM1528586,ESM1528582,ESM1528578,ESM1528581,ESM1528577 ENSRNA049453121,1,82,8,82,4,68,88,73,46,57,25,22 ENSRNA049453138,68,93,41,84,36,18,28,92,84,85,92,32 ENSRNA049454388,38,10,0,23,11,17,95,57,25,82,10,70 diff --git a/tests/test_data/merge_data/output/all_counts.csv b/tests/test_data/merge_data/output/all_counts.csv index 0ba6456a..527a2205 100644 --- a/tests/test_data/merge_data/output/all_counts.csv +++ b/tests/test_data/merge_data/output/all_counts.csv @@ -1,4 +1,4 @@ -ensembl_gene_id,URR029909,URR029910,URR029911,URR029912,URR029913,URR029914,URR029915,URR029916,URR029917,ERR029909,ERR029910,ERR029911,ERR029912,ERR029913,ERR029914,ERR029915,ERR029916,ERR029917,ARR029909,ARR029910,ARR029911,ARR029912,ARR029913,ARR029914,ARR029915,ARR029916,ARR029917 +gene_id,URR029909,URR029910,URR029911,URR029912,URR029913,URR029914,URR029915,URR029916,URR029917,ERR029909,ERR029910,ERR029911,ERR029912,ERR029913,ERR029914,ERR029915,ERR029916,ERR029917,ARR029909,ARR029910,ARR029911,ARR029912,ARR029913,ARR029914,ARR029915,ARR029916,ARR029917 AT1G34790,0.60113057,0.64080682,0.6,0.6197164000000003,0.60115891,0.63052843,0.61002869,0.65849011,0.66239896,0.60113057,0.64080682,0.6348181099999999,0.6519716400000001,0.60115891,0.63052843,0.61002869,0.65849011,0.66239896,0.60113057,0.64080682,0.6348181099999999,0.6519716400000001,0.20115891000000002,0.93052843,0.71002869,0.65849011,0.16239896 AT5G35550,0.7148504699999999,0.21713193,0.03318757,0.18404821999999998,0.70246917,0.0,0.8336608,0.00340416,0.23179154000000002,0.0,0.21713193,0.03318757,0.18404821999999998,0.70246917,0.7555268599999999,0.8336608,0.00340416,0.23179154000000002,0.7148504699999999,0.21713193,0.03318757,0.18404821999999998,0.70246917,0.7555268599999999,0.8336608,0.00340416,0.23179154000000002 AT5G23260,0.71122807,0.47981484,0.85599454,0.69023553,0.40420572,0.30220852000000004,0.73996866,0.08559519,0.80013134,0.71122807,0.47981484,0.85599454,0.69023553,0.40420572,0.30220852000000004,0.73996866,0.08559519,0.80013134,0.71122807,0.47981484,0.85599454,0.69023553,0.40420572,0.30220852000000004,0.73996866,0.08559519,0.80013134 diff --git a/tests/test_data/normfinder/very_small_cq/normfinder.R b/tests/test_data/normfinder/very_small_cq/normfinder.R index c120d305..f415f95f 100644 --- a/tests/test_data/normfinder/very_small_cq/normfinder.R +++ b/tests/test_data/normfinder/very_small_cq/normfinder.R @@ -280,11 +280,11 @@ library(dplyr) data <- counts %>% tidyr::pivot_longer( - cols = -ensembl_gene_id, + cols = -gene_id, names_to = "sample", values_to = "cq" ) %>% - dplyr::rename(gene = ensembl_gene_id) %>% + dplyr::rename(gene = gene_id) %>% dplyr::left_join(design, by = "sample") # Inspect diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 76bdb968..aa515e63 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -92,20 +92,18 @@ workflow STABLEEXPRESSION { ch_gene_id_mapping = params.gene_id_mapping_file ? Channel.fromPath( params.gene_id_mapping_file, checkIfExists: true ) : Channel.value( [] ) ch_gene_metadata = params.gene_metadata ? Channel.fromPath( params.gene_metadata, checkIfExists: true ) : Channel.value( [] ) - if ( !params.skip_id_mapping ) { - - // tries to map gene IDs to Ensembl IDs whenever possible - ID_MAPPING( - ch_counts, - species, - ch_gene_id_mapping, - ch_gene_metadata - ) - ID_MAPPING.out.counts.set { ch_counts } - ID_MAPPING.out.mapping.set { ch_gene_id_mapping } - ID_MAPPING.out.metadata.set { ch_gene_metadata } - - } + // tries to map gene IDs to Ensembl IDs whenever possible + ID_MAPPING( + ch_counts, + species, + params.skip_id_mapping, + params.gprofiler_target_db, + ch_gene_id_mapping, + ch_gene_metadata + ) + ID_MAPPING.out.counts.set { ch_counts } + ID_MAPPING.out.mapping.set { ch_gene_id_mapping } + ID_MAPPING.out.metadata.set { ch_gene_metadata } ch_counts = storeDatasetSize( ch_counts, "nb_genes_after_idmapping", "nb_samples_after_idmapping" ) From 553f3a9b6b064f2a151dd53625f4442b4e85bd27 Mon Sep 17 00:00:00 2001 From: Olivier Date: Thu, 20 Nov 2025 18:08:25 +0100 Subject: [PATCH 177/258] change error / retry strategy for all processes --- conf/base.config | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/conf/base.config b/conf/base.config index c0f825d5..7909e851 100644 --- a/conf/base.config +++ b/conf/base.config @@ -15,8 +15,17 @@ process { memory = { 6.GB * task.attempt } time = { 4.h * task.attempt } - errorStrategy = { task.exitStatus in ((130..145) + 104 + 175) ? 'retry' : 'finish' } - maxRetries = 1 + errorStrategy = { + if (task.exitStatus in (100..102)) { // managed errors; they should not be retried + 'ignore' + } else if (task.attempt <= 5) { // all other errors should be retried with exponential backoff + sleep(Math.pow(2, task.attempt) * 200 as long) + return 'retry' + } else { // after 5 retries, ignore the error + return 'ignore' + } + } + maxRetries = 3 maxErrors = '-1' // Process-specific resource requirements From 09f49a933b5a2a4ec1094721e37bc93af28c7fb1 Mon Sep 17 00:00:00 2001 From: Olivier Date: Thu, 20 Nov 2025 18:22:53 +0100 Subject: [PATCH 178/258] add new tests for idmapping processes --- .../local/gprofiler/idmapping/main.nf.test | 35 ++- .../gprofiler/idmapping/main.nf.test.snap | 45 +++- .../local/rename_gene_ids/main.nf.test | 153 +----------- .../local/rename_gene_ids/main.nf.test.snap | 219 ++---------------- .../idmapping/mapped/mapped_gene_ids.csv | 4 + 5 files changed, 98 insertions(+), 358 deletions(-) create mode 100644 tests/test_data/idmapping/mapped/mapped_gene_ids.csv diff --git a/tests/modules/local/gprofiler/idmapping/main.nf.test b/tests/modules/local/gprofiler/idmapping/main.nf.test index e307a561..63925534 100644 --- a/tests/modules/local/gprofiler/idmapping/main.nf.test +++ b/tests/modules/local/gprofiler/idmapping/main.nf.test @@ -5,7 +5,7 @@ nextflow_process { process "GPROFILER_IDMAPPING" tag "gprofiler_idmapping" - test("Should run without failures") { + test("ENSG - Mapping found") { when { params { @@ -16,16 +16,43 @@ nextflow_process { """ input[0] = file("$projectDir/tests/test_data/idmapping/gene_ids/gene_ids.txt", checkIfExists: true) input[1] = "Solanum tuberosum" - input[2] = "ENTREZGENE" + input[2] = "ENSG" """ } } then { - assert process.success - assert snapshot(process.out).match() + assertAll( + { assert process.success }, + { assert process.out.mapping.size() == 1 }, + { assert snapshot(process.out).match() } + ) } + } + + test("Entrez - No mapping found") { + when { + params { + // define parameters here. Example: + // outdir = "tests/results" + } + process { + """ + input[0] = file("$projectDir/tests/test_data/idmapping/gene_ids/gene_ids.txt", checkIfExists: true) + input[1] = "Solanum tuberosum" + input[2] = "ENTREZGENE" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert process.out.mapping.size() == 0 }, + { assert snapshot(process.out).match() } + ) + } } } diff --git a/tests/modules/local/gprofiler/idmapping/main.nf.test.snap b/tests/modules/local/gprofiler/idmapping/main.nf.test.snap index d919decf..4416cbc8 100644 --- a/tests/modules/local/gprofiler/idmapping/main.nf.test.snap +++ b/tests/modules/local/gprofiler/idmapping/main.nf.test.snap @@ -1,5 +1,5 @@ { - "Should run without failures": { + "ENtrez - No mapping found": { "content": [ { "0": [ @@ -38,6 +38,47 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-20T16:46:09.976019836" + "timestamp": "2025-11-20T18:15:42.597266116" + }, + "ENSG - Mapping found": { + "content": [ + { + "0": [ + "mapped_gene_ids.csv:md5,c4ef4df6530509b486662a107ba8de44" + ], + "1": [ + "gene_metadata.csv:md5,f4dad0185e6f2d780f561d3efc301562" + ], + "2": [ + [ + "GPROFILER_IDMAPPING", + "python", + "3.13.5" + ] + ], + "3": [ + [ + "GPROFILER_IDMAPPING", + "pandas", + "2.3.1" + ] + ], + "4": [ + [ + "GPROFILER_IDMAPPING", + "requests", + "2.32.4" + ] + ], + "metadata": [ + "gene_metadata.csv:md5,f4dad0185e6f2d780f561d3efc301562" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-11-20T18:15:36.900376779" } } \ No newline at end of file diff --git a/tests/modules/local/rename_gene_ids/main.nf.test b/tests/modules/local/rename_gene_ids/main.nf.test index cf84800d..438a7425 100644 --- a/tests/modules/local/rename_gene_ids/main.nf.test +++ b/tests/modules/local/rename_gene_ids/main.nf.test @@ -16,7 +16,7 @@ nextflow_process { file("$projectDir/tests/test_data/idmapping/base/counts.ensembl_ids.csv", checkIfExists: true) ] ) - input[1] = "Solanum tuberosum" + input[1] = file("$projectDir/tests/test_data/idmapping/mapped/mapped_gene_ids.csv", checkIfExists: true) input[2] = Channel.value([]) """ } @@ -31,153 +31,6 @@ nextflow_process { } - test("Map NCBI IDs") { - - when { - process { - """ - input[0] = Channel.of( - [ - [ dataset: "test" ], - file("$projectDir/tests/test_data/idmapping/base/counts.ncbi_ids.csv", checkIfExists: true) - ] - ) - input[1] = "Arabidopsis thaliana" - input[2] = Channel.value([]) - input[3] = Channel.value([]) - """ - } - } - - then { - assertAll( - { assert process.success }, - { assert snapshot(process.out).match() } - ) - } - - } - - - test("Map Uniprot IDs") { - - when { - process { - """ - input[0] = Channel.of( - [ - [ dataset: "test" ], - file("$projectDir/tests/test_data/idmapping/base/counts.uniprot_ids.csv", checkIfExists: true) - ] - ) - input[1] = "Arabidopsis thaliana" - input[2] = Channel.value([]) - input[3] = Channel.value([]) - """ - } - } - - then { - assertAll( - { assert process.success }, - { assert snapshot(process.out).match() } - ) - } - - } - - test("Empty count file - ignore error") { - - tag "idmapping_empty" - - when { - process { - """ - input[0] = Channel.of( - [ - [ dataset: "test" ], - file("$projectDir/tests/test_data/idmapping/empty/counts.csv", checkIfExists: true) - ] - ) - input[1] = "Arabidopsis thaliana" - input[2] = Channel.value([]) - input[3] = Channel.value([]) - """ - } - } - - // check for the absence of expected output (the error is ignored but no output is produced) - then { - assertAll( - { assert process.success }, - { assert process.out.counts.size() == 0 }, - { assert process.out.metadata.size() == 0 }, - { assert process.out.mapping.size() == 0 } - ) - } - - } - - test("Mapping not found - ignore error") { - - tag "idmapping_not_found" - - when { - process { - """ - input[0] = Channel.of( - [ - [ dataset: "test" ], - file("$projectDir/tests/test_data/idmapping/not_found/counts.csv", checkIfExists: true) - ] - ) - input[1] = "Homo sapiens" - input[2] = Channel.value([]) - input[3] = Channel.value([]) - """ - } - } - - // check for the absence of expected output (the error is ignored but no output is produced) - then { - assertAll( - { assert process.success }, - { assert process.out.counts.size() == 0 }, - { assert process.out.metadata.size() == 0 }, - { assert process.out.mapping.size() == 0 } - ) - } - - } - - test("Custom mapping") { - - tag "custom_mapping" - - when { - process { - """ - input[0] = Channel.of( - [ - [ dataset: "test" ], - file("$projectDir/tests/test_data/idmapping/base/counts.ensembl_ids.csv", checkIfExists: true) - ] - ) - input[1] = "Beta vulgaris" - input[2] = Channel.fromPath( "$projectDir/tests/test_data/idmapping/custom/mapping.csv", checkIfExists: true) - """ - } - } - - then { - assertAll( - { assert process.success }, - { assert snapshot(process.out).match() } - ) - } - - } - test("Custom mapping - TSV") { tag "custom_mapping_tsv" @@ -191,8 +44,8 @@ nextflow_process { file("$projectDir/tests/test_data/idmapping/tsv/counts.ensembl_ids.tsv", checkIfExists: true) ] ) - input[1] = "Beta vulgaris" - input[2] = Channel.fromPath( "$projectDir/tests/test_data/idmapping/tsv/mapping.tsv", checkIfExists: true) + input[1] = file("$projectDir/tests/test_data/idmapping/mapped/mapped_gene_ids.csv", checkIfExists: true) + input[2] = file( "$projectDir/tests/test_data/idmapping/tsv/mapping.tsv", checkIfExists: true) """ } } diff --git a/tests/modules/local/rename_gene_ids/main.nf.test.snap b/tests/modules/local/rename_gene_ids/main.nf.test.snap index 733bb10f..31c09ceb 100644 --- a/tests/modules/local/rename_gene_ids/main.nf.test.snap +++ b/tests/modules/local/rename_gene_ids/main.nf.test.snap @@ -7,35 +7,32 @@ { "dataset": "test" }, - "counts.ensembl_ids.renamed.csv:md5,3a4a73f829d7dcb93420cc3de98adcdd" + "counts.ensembl_ids.renamed.csv:md5,a72744efb93ed948149c48626a338f04" ] ], "1": [ - "counts.ensembl_ids.metadata.csv:md5,af7e8d352d9c4591d53d95f660457beb" + ], "2": [ - "counts.ensembl_ids.mapping.csv:md5,6ff8d8f71b9df7a1b08ff0bfda8da755" - ], - "3": [ ], - "4": [ + "3": [ [ - "GPROFILER_IDMAPPING", + "RENAME_GENE_IDS", "python", "3.13.5" ] ], - "5": [ + "4": [ [ - "GPROFILER_IDMAPPING", + "RENAME_GENE_IDS", "pandas", "2.3.1" ] ], - "6": [ + "5": [ [ - "GPROFILER_IDMAPPING", + "RENAME_GENE_IDS", "requests", "2.32.4" ] @@ -45,11 +42,8 @@ { "dataset": "test" }, - "counts.ensembl_ids.renamed.csv:md5,3a4a73f829d7dcb93420cc3de98adcdd" + "counts.ensembl_ids.renamed.csv:md5,a72744efb93ed948149c48626a338f04" ] - ], - "metadata": [ - "counts.ensembl_ids.metadata.csv:md5,af7e8d352d9c4591d53d95f660457beb" ] } ], @@ -57,9 +51,9 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-17T11:28:57.680785987" + "timestamp": "2025-11-20T18:21:55.432510225" }, - "Custom mapping": { + "Map Ensembl IDs": { "content": [ { "0": [ @@ -67,63 +61,8 @@ { "dataset": "test" }, - "counts.ensembl_ids.renamed.csv:md5,3a4a73f829d7dcb93420cc3de98adcdd" + "counts.ensembl_ids.renamed.csv:md5,a1633a75d1eb91103208014705409320" ] - ], - "1": [ - "counts.ensembl_ids.metadata.csv:md5,af7e8d352d9c4591d53d95f660457beb" - ], - "2": [ - "counts.ensembl_ids.mapping.csv:md5,6ff8d8f71b9df7a1b08ff0bfda8da755" - ], - "3": [ - - ], - "4": [ - [ - "GPROFILER_IDMAPPING", - "python", - "3.13.5" - ] - ], - "5": [ - [ - "GPROFILER_IDMAPPING", - "pandas", - "2.3.1" - ] - ], - "6": [ - [ - "GPROFILER_IDMAPPING", - "requests", - "2.32.4" - ] - ], - "counts": [ - [ - { - "dataset": "test" - }, - "counts.ensembl_ids.renamed.csv:md5,3a4a73f829d7dcb93420cc3de98adcdd" - ] - ], - "metadata": [ - "counts.ensembl_ids.metadata.csv:md5,af7e8d352d9c4591d53d95f660457beb" - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-17T11:28:52.208091927" - }, - "Map Ensembl IDs to themselves": { - "content": [ - { - "0": [ - ], "1": [ @@ -133,142 +72,21 @@ ], "3": [ [ - "test", - "failure_reason.txt:md5,7ae1b80b4f94b2bed454756d6487c03f" - ] - ], - "4": [ - [ - "GPROFILER_IDMAPPING", + "RENAME_GENE_IDS", "python", "3.13.5" ] - ], - "5": [ - [ - "GPROFILER_IDMAPPING", - "pandas", - "2.3.1" - ] - ], - "6": [ - [ - "GPROFILER_IDMAPPING", - "requests", - "2.32.4" - ] - ], - "counts": [ - - ], - "metadata": [ - - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-08T12:59:22.863610113" - }, - "Map Uniprot IDs": { - "content": [ - { - "0": [ - [ - { - "dataset": "test" - }, - "counts.uniprot_ids.renamed.csv:md5,e8d91cae1a6f2c888d9e46e9a8dce147" - ] - ], - "1": [ - "counts.uniprot_ids.metadata.csv:md5,b87d6533848a3ae07b289ec6b0c4a1ff" - ], - "2": [ - "counts.uniprot_ids.mapping.csv:md5,fe88c79c45d45825d28f325f7a2f383e" - ], - "3": [ - ], "4": [ [ - "GPROFILER_IDMAPPING", - "python", - "3.13.5" - ] - ], - "5": [ - [ - "GPROFILER_IDMAPPING", + "RENAME_GENE_IDS", "pandas", "2.3.1" ] ], - "6": [ - [ - "GPROFILER_IDMAPPING", - "requests", - "2.32.4" - ] - ], - "counts": [ - [ - { - "dataset": "test" - }, - "counts.uniprot_ids.renamed.csv:md5,e8d91cae1a6f2c888d9e46e9a8dce147" - ] - ], - "metadata": [ - "counts.uniprot_ids.metadata.csv:md5,b87d6533848a3ae07b289ec6b0c4a1ff" - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-17T11:28:34.969677294" - }, - "Map NCBI IDs": { - "content": [ - { - "0": [ - [ - { - "dataset": "test" - }, - "counts.ncbi_ids.renamed.csv:md5,e8d91cae1a6f2c888d9e46e9a8dce147" - ] - ], - "1": [ - "counts.ncbi_ids.metadata.csv:md5,b87d6533848a3ae07b289ec6b0c4a1ff" - ], - "2": [ - "counts.ncbi_ids.mapping.csv:md5,fe4fd9005dce99b7722b84134f51badd" - ], - "3": [ - - ], - "4": [ - [ - "GPROFILER_IDMAPPING", - "python", - "3.13.5" - ] - ], "5": [ [ - "GPROFILER_IDMAPPING", - "pandas", - "2.3.1" - ] - ], - "6": [ - [ - "GPROFILER_IDMAPPING", + "RENAME_GENE_IDS", "requests", "2.32.4" ] @@ -278,11 +96,8 @@ { "dataset": "test" }, - "counts.ncbi_ids.renamed.csv:md5,e8d91cae1a6f2c888d9e46e9a8dce147" + "counts.ensembl_ids.renamed.csv:md5,a1633a75d1eb91103208014705409320" ] - ], - "metadata": [ - "counts.ncbi_ids.metadata.csv:md5,b87d6533848a3ae07b289ec6b0c4a1ff" ] } ], @@ -290,6 +105,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-17T11:28:28.998501422" + "timestamp": "2025-11-20T18:20:21.92552791" } } \ No newline at end of file diff --git a/tests/test_data/idmapping/mapped/mapped_gene_ids.csv b/tests/test_data/idmapping/mapped/mapped_gene_ids.csv new file mode 100644 index 00000000..84561688 --- /dev/null +++ b/tests/test_data/idmapping/mapped/mapped_gene_ids.csv @@ -0,0 +1,4 @@ +original_gene_id,gene_id +ENSRNA049434199,ENSRNA049434199 +ENSRNA049434246,ENSRNA049434246 +ENSRNA049434252,ENSRNA049434252 From e74b4d23fe4ef74d17d8ed3b9cd2f90042e40500 Mon Sep 17 00:00:00 2001 From: Olivier Date: Thu, 20 Nov 2025 18:57:42 +0100 Subject: [PATCH 179/258] fix get_candidate_genes.py when ids are Entrez gene ids --- bin/get_candidate_genes.py | 29 ++++++++++++++++------------- bin/quantile_normalise.py | 6 ++++-- nextflow_schema.json | 6 +++--- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/bin/get_candidate_genes.py b/bin/get_candidate_genes.py index fb7a3349..ec5e12d5 100755 --- a/bin/get_candidate_genes.py +++ b/bin/get_candidate_genes.py @@ -65,18 +65,24 @@ def parse_args(): return parser.parse_args() +def parse_stats(file: Path) -> pl.DataFrame: + return pl.read_csv(file).select( + pl.col(config.GENE_ID_COLNAME).cast(pl.String()), + pl.exclude(config.GENE_ID_COLNAME).cast(pl.Float64()), + ) + + def get_best_candidates( - stat_lf: pl.LazyFrame, candidate_selection_descriptor: str, nb_top_stable_genes: int + stat_df: pl.DataFrame, candidate_selection_descriptor: str, nb_top_stable_genes: int ) -> list[str]: logger.info("Getting best candidates") column_for_sorting = config.SCORING_BASE_TO_STABILITY_SCORE_COLUMN[ candidate_selection_descriptor ] return ( - stat_lf.sort(column_for_sorting, descending=False, nulls_last=True) + stat_df.sort(column_for_sorting, descending=False, nulls_last=True) .head(nb_top_stable_genes) .select(config.GENE_ID_COLNAME) - .collect() .to_series() .to_list() ) @@ -90,16 +96,13 @@ def filter_out_genes_with_zero_counts(stat_lf: pl.LazyFrame) -> pl.LazyFrame: def filter_out_low_expression_genes( - stat_lf: pl.LazyFrame, min_pct_quantile_expr_level: float -) -> pl.LazyFrame: + stat_df: pl.DataFrame, min_pct_quantile_expr_level: float +) -> pl.DataFrame: logger.info("Filtering out low expression genes") max_quantile = ( - stat_lf.select(config.EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME) - .max() - .collect() - .item() + stat_df.select(config.EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME).max().item() ) - return stat_lf.filter( + return stat_df.filter( pl.col(config.EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME) >= max_quantile * min_pct_quantile_expr_level ) @@ -131,15 +134,15 @@ def export_data(filtered_count_df: pl.DataFrame): def main(): args = parse_args() - stat_lf = pl.scan_csv(args.stat_file) + stat_df = parse_stats(args.stat_file) # first basic filters - stat_lf = filter_out_low_expression_genes(stat_lf, args.min_pct_quantile_expr_level) + stat_df = filter_out_low_expression_genes(stat_df, args.min_pct_quantile_expr_level) # stat_lf = filter_out_genes_with_zero_counts(stat_lf) # get base candidate genes based on the chosen statistical descriptor (cv, rcvm) best_candidates = get_best_candidates( - stat_lf, args.candidate_selection_descriptor, args.nb_top_stable_genes + stat_df, args.candidate_selection_descriptor, args.nb_top_stable_genes ) # get counts for candidate genes diff --git a/bin/quantile_normalise.py b/bin/quantile_normalise.py index 180f4360..3403cd75 100755 --- a/bin/quantile_normalise.py +++ b/bin/quantile_normalise.py @@ -60,11 +60,13 @@ def quantile_normalise(data: pd.DataFrame, target_distribution: str): return normalised_data -def export_count_data(quantile_normalized_counts: pd.DataFrame, count_file: Path): +def export_count_data(count_df: pd.DataFrame, count_file: Path): """Export gene expression data to CSV files.""" outfilename = count_file.name.replace(".csv", QUANT_NORM_SUFFIX) logger.info(f"Exporting quantile normalised counts to: {outfilename}") - quantile_normalized_counts.reset_index().to_parquet(outfilename) + count_df.reset_index(inplace=True) + count_df[config.GENE_ID_COLNAME] = count_df[config.GENE_ID_COLNAME].astype(str) + count_df.to_parquet(outfilename) ##################################################### diff --git a/nextflow_schema.json b/nextflow_schema.json index b1f31aae..7e5aa942 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -187,9 +187,9 @@ "type": "string", "description": "Target database for g:Profiler", "fa_icon": "fas fa-divide", - "enum": ["ENTREZGENE", "ENSG"], - "default": "ENTREZGENE", - "help_text": "Target database for g:Profiler. By default, it is set to ENTREZGENE (NCBI Entrez Gene IDs). You can choose to use ENSEMBL IDs instead." + "enum": ["ENSG", "ENTREZGENE", "UNIPROTSPTREMBL", "UNIPROTSWISSPROT"], + "default": "ENSG", + "help_text": "Target database for g:Profiler. You can see the full list of available target databases at https://biit.cs.ut.ee/gprofiler/convert." }, "gene_id_mapping_file": { "type": "string", From 7293377ed044a87094d5ace5f884a36fa2bc8c10 Mon Sep 17 00:00:00 2001 From: Olivier Date: Thu, 20 Nov 2025 18:58:32 +0100 Subject: [PATCH 180/258] remove data cleansing subworkflow from workflow --- assets/multiqc_config.yml | 13 ------------- subworkflows/local/expression_normalisation/main.nf | 2 +- subworkflows/local/idmapping/main.nf | 2 +- subworkflows/local/multiqc/main.nf | 13 ------------- subworkflows/local/{ => old}/data_cleansing/main.nf | 0 workflows/stableexpression.nf | 13 +------------ 6 files changed, 3 insertions(+), 40 deletions(-) rename subworkflows/local/{ => old}/data_cleansing/main.nf (100%) diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index cb704af3..48af23d9 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -619,17 +619,6 @@ custom_data: Reasons of failure during Normalisation (DESeq2 or edgeR) plot_type: "table" - clean_count_failure_reasons: - section_name: "Failure reasons" - parent_id: cleaning - parent_name: "Cleaning" - parent_description: "Information about the cleaning" - file_format: "tsv" - no_violin: true - description: | - Reasons of failure during dataset cleaning - plot_type: "table" - #violin_downsample_after: 10000 log_filesize_limit: 10000000000 # 10GB @@ -676,5 +665,3 @@ sp: fn: "*normalisation_failure_reasons.csv" normalisation_warning_reasons: fn: "*normalisation_warning_reasons.csv" - clean_count_failure_reasons: - fn: "*clean_count_failure_reasons.csv" diff --git a/subworkflows/local/expression_normalisation/main.nf b/subworkflows/local/expression_normalisation/main.nf index 67dca582..a74955c8 100644 --- a/subworkflows/local/expression_normalisation/main.nf +++ b/subworkflows/local/expression_normalisation/main.nf @@ -58,6 +58,6 @@ workflow EXPRESSION_NORMALISATION { emit: - normalised_counts = QUANTILE_NORMALISATION.out.counts + counts = QUANTILE_NORMALISATION.out.counts } diff --git a/subworkflows/local/idmapping/main.nf b/subworkflows/local/idmapping/main.nf index b2a8e48f..d60b56c1 100644 --- a/subworkflows/local/idmapping/main.nf +++ b/subworkflows/local/idmapping/main.nf @@ -20,7 +20,7 @@ workflow ID_MAPPING { main: - + ch_counts.view { a -> "counts ${a}"} ch_gene_id_mapping = Channel.empty() if ( !params.skip_id_mapping ) { diff --git a/subworkflows/local/multiqc/main.nf b/subworkflows/local/multiqc/main.nf index ad59e234..8ae90061 100644 --- a/subworkflows/local/multiqc/main.nf +++ b/subworkflows/local/multiqc/main.nf @@ -120,18 +120,6 @@ workflow MULTIQC_WORKFLOW { } .set { ch_normalisation_warning_reasons } - Channel.topic('clean_count_failure_reason') - .map { accession, file -> [ accession, file.readLines()[0] ] } - .collectFile( - name: 'clean_count_failure_reasons.tsv', - seed: "Dataset\tReason", - newLine: true, - storeDir: "${params.outdir}/errors/" - ) { - item -> "${item[0]}\t${item[1]}" - } - .set { ch_clean_count_failure_reasons } - // ------------------------------------------------------------------------------------ // MULTIQC FILES @@ -150,7 +138,6 @@ workflow MULTIQC_WORKFLOW { .mix( ch_id_mapping_failure_reasons ) .mix( ch_normalisation_failure_reasons ) .mix( ch_normalisation_warning_reasons ) - .mix( ch_clean_count_failure_reasons ) .set { ch_multiqc_files } // ------------------------------------------------------------------------------------ diff --git a/subworkflows/local/data_cleansing/main.nf b/subworkflows/local/old/data_cleansing/main.nf similarity index 100% rename from subworkflows/local/data_cleansing/main.nf rename to subworkflows/local/old/data_cleansing/main.nf diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index aa515e63..5e8b4c38 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -8,7 +8,6 @@ include { EXPRESSIONATLAS_FETCHDATA } from '../subworkflows/local/e include { GEO_FETCHDATA } from '../subworkflows/local/geo_fetchdata' include { ID_MAPPING } from '../subworkflows/local/idmapping' include { EXPRESSION_NORMALISATION } from '../subworkflows/local/expression_normalisation' -include { DATA_CLEANSING } from '../subworkflows/local/data_cleansing' include { MERGE_DATA } from '../subworkflows/local/merge_data' include { BASE_STATISTICS } from '../subworkflows/local/base_statistics' include { STABILITY_SCORING } from '../subworkflows/local/stability_scoring' @@ -117,22 +116,12 @@ workflow STABLEEXPRESSION { params.quantile_norm_target_distrib ) - // ----------------------------------------------------------------- - // GET STATISTICS DATASET BY DATASET AND PERFORM SOME CLEANING OPERATIONS - // ----------------------------------------------------------------- - - DATA_CLEANSING( - EXPRESSION_NORMALISATION.out.normalised_counts, - params.quantile_norm_target_distrib, - params.ks_pvalue_threshold - ) - // ----------------------------------------------------------------- // MERGE DATA // ----------------------------------------------------------------- MERGE_DATA ( - DATA_CLEANSING.out.cleaned_counts, + EXPRESSION_NORMALISATION.out.counts, ch_gene_id_mapping, ch_gene_metadata ) From e78dce65ac9a34205fa8aef59af979b4aaed3fb5 Mon Sep 17 00:00:00 2001 From: Olivier Date: Thu, 20 Nov 2025 19:27:09 +0100 Subject: [PATCH 181/258] fix issue in aggregate_results.py when gene ids are integers --- bin/aggregate_results.py | 155 +++++++++++++++++---------------------- 1 file changed, 66 insertions(+), 89 deletions(-) diff --git a/bin/aggregate_results.py b/bin/aggregate_results.py index 9767ead6..9fd70c79 100755 --- a/bin/aggregate_results.py +++ b/bin/aggregate_results.py @@ -70,95 +70,73 @@ def parse_args(): return parser.parse_args() -def is_valid_lf(lf: pl.LazyFrame, file: Path) -> bool: - """Check if a LazyFrame is valid. - - A LazyFrame is considered valid if it contains at least one row. - """ - try: - return not lf.limit(1).collect().is_empty() - except FileNotFoundError: - # strangely enough we get this error for some files existing but empty - logger.error(f"Could not find file {str(file)}") - return False - except pl.exceptions.NoDataError as err: - logger.error(f"File {str(file)} is empty: {err}") - return False - +def parse_stat_file(file: Path) -> pl.DataFrame: + return pl.read_csv(file).with_columns( + pl.col(config.GENE_ID_COLNAME).cast(pl.String()) + ) -def get_valid_lazy_lfs(files: list[Path]) -> list[pl.LazyFrame]: - """Get a list of valid LazyFrames from a list of files. - A LazyFrame is considered valid if it contains at least one row. - """ - lf_dict = {file: pl.scan_csv(file) for file in files} - return [lf for file, lf in lf_dict.items() if is_valid_lf(lf, file)] +def get_non_empty_dataframes(files: list[Path]) -> list[pl.DataFrame]: + dfs = [pl.read_csv(file) for file in files] + return [df for df in dfs if not df.is_empty()] -def cast_cols_to_string(lf: pl.LazyFrame) -> pl.LazyFrame: - return lf.select( - [pl.col(column).cast(pl.String) for column in lf.collect_schema().names()] +def cast_cols_to_string(df: pl.DataFrame) -> pl.DataFrame: + return df.select( + [pl.col(column).cast(pl.String) for column in df.collect_schema().names()] ) -def concat_cast_to_string_and_drop_duplicates(files: list[Path]) -> pl.LazyFrame: - """Concatenate LazyFrames, cast all columns to String, and drop duplicates. +def concat_cast_to_string_and_drop_duplicates(files: list[Path]) -> pl.DataFrame: + """Concatenate DataFrames, cast all columns to String, and drop duplicates. - The first step is to concatenate the LazyFrames. Then, the dataframe is cast + The first step is to concatenate the DataFrames. Then, the dataframe is cast to String to ensure that all columns have the same data type. Finally, duplicate rows are dropped. """ - lfs = get_valid_lazy_lfs(files) - lfs = [cast_cols_to_string(lf) for lf in lfs] - concat_lf = pl.concat(lfs) + dfs = get_non_empty_dataframes(files) + dfs = [cast_cols_to_string(df) for df in dfs] + concat_df = pl.concat(dfs) # dropping duplicates # casting all columns to String - return concat_lf.unique() - + return concat_df.unique() -def get_count_columns(lf: pl.LazyFrame) -> list[str]: - """Get all column names except the GENE_ID column. - The GENE_ID column contains only gene IDs. - """ - return lf.select(pl.exclude(config.GENE_ID_COLNAME)).collect_schema().names() - - -def cast_count_columns_to_float32(lf: pl.LazyFrame) -> pl.LazyFrame: - return lf.select( - [pl.col(config.GENE_ID_COLNAME)] - + [pl.col(column).cast(pl.Float32) for column in get_count_columns(lf)] +def cast_count_columns_to_float(df: pl.DataFrame) -> pl.DataFrame: + return df.select( + pl.col(config.GENE_ID_COLNAME), + pl.exclude(config.GENE_ID_COLNAME).cast(pl.Float64), ) -def join_data_on_gene_id(stat_lf: pl.LazyFrame, *lfs: pl.LazyFrame) -> pl.LazyFrame: +def join_data_on_gene_id(stat_df: pl.DataFrame, *dfs: pl.DataFrame) -> pl.DataFrame: """Merge the statistics dataframe with the metadata dataframe and the mapping dataframe.""" - # we need to ensure that the index of stat_lf are strings - for lf in lfs: - stat_lf = stat_lf.join(lf, on=config.GENE_ID_COLNAME, how="left") - return stat_lf + # we need to ensure that the index of stat_df are strings + for df in dfs: + stat_df = stat_df.join(df, on=config.GENE_ID_COLNAME, how="left") + return stat_df -def get_counts(file: Path) -> pl.LazyFrame: +def get_counts(file: Path) -> pl.DataFrame: # sorting dataframe (necessary to get consistent output) - return pl.scan_parquet(file).sort(config.GENE_ID_COLNAME, descending=False) + return pl.read_parquet(file).sort(config.GENE_ID_COLNAME, descending=False) -def get_metadata(metadata_files: list[Path]) -> pl.LazyFrame | None: +def get_metadata(metadata_files: list[Path]) -> pl.DataFrame | None: """Retrieve and concatenate metadata from a list of metadata files.""" if not metadata_files: return None return concat_cast_to_string_and_drop_duplicates(metadata_files) -def get_mappings(mapping_files: list[Path]) -> pl.LazyFrame | None: +def get_mappings(mapping_files: list[Path]) -> pl.DataFrame | None: if not mapping_files: return None - concat_lf = concat_cast_to_string_and_drop_duplicates(mapping_files) + concat_df = concat_cast_to_string_and_drop_duplicates(mapping_files) # group by new gene IDs and gets the lis # convert the list column to a string representation # separate the original gene IDs with a semicolon - return concat_lf.group_by(config.GENE_ID_COLNAME).agg( + return concat_df.group_by(config.GENE_ID_COLNAME).agg( pl.col(config.ORIGINAL_GENE_ID_COLNAME) .unique() .sort() @@ -181,13 +159,13 @@ def get_status(quantile_interval: int) -> str: return "Medium range" -def add_expression_level_status(lf: pl.LazyFrame) -> pl.LazyFrame: +def add_expression_level_status(df: pl.DataFrame) -> pl.DataFrame: logger.info("Adding expression level status") mapping_dict = { quantile_interval: get_status(quantile_interval) for quantile_interval in range(NB_QUANTILES) } - return lf.with_columns( + return df.with_columns( pl.col(config.EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME) .replace_strict(mapping_dict) .alias(config.EXPRESSION_LEVEL_STATUS_COLNAME) @@ -195,19 +173,19 @@ def add_expression_level_status(lf: pl.LazyFrame) -> pl.LazyFrame: def get_all_genes_summary( - stat_summary_lf: pl.LazyFrame, *lfs: pl.LazyFrame -) -> pl.LazyFrame: + stat_summary_df: pl.DataFrame, *dfs: pl.DataFrame +) -> pl.DataFrame: """ Extract the most stable genes from the statistics dataframe. """ # add gene name, description and original gene IDs to statistics summary - stat_summary_lf = join_data_on_gene_id(stat_summary_lf, *lfs) - stat_summary_lf = add_expression_level_status(stat_summary_lf) - return stat_summary_lf + stat_summary_df = join_data_on_gene_id(stat_summary_df, *dfs) + stat_summary_df = add_expression_level_status(stat_summary_df) + return stat_summary_df def get_top_stable_genes_counts( - log_count_lf: pl.LazyFrame, stat_summary_df: pl.LazyFrame + log_count_df: pl.DataFrame, stat_summary_df: pl.DataFrame ) -> pl.DataFrame: # getting list of top stable genes with their order top_genes_with_order = ( @@ -217,11 +195,9 @@ def get_top_stable_genes_counts( ) # join to get only existing genes and maintain order - sorted_transposed_counts_df = ( - log_count_lf.join( - top_genes_with_order, on=config.GENE_ID_COLNAME, how="inner" - ).sort("sort_order", descending=False) - ).collect() + sorted_transposed_counts_df = log_count_df.join( + top_genes_with_order, on=config.GENE_ID_COLNAME, how="inner" + ).sort("sort_order", descending=False) # get the actual gene names that were found (in order) actual_gene_names = ( @@ -233,26 +209,26 @@ def get_top_stable_genes_counts( def export_data( - all_genes_summary_lf: pl.LazyFrame, - top_stable_genes_summary_lf: pl.LazyFrame, - all_counts_lf: pl.LazyFrame, + all_genes_summary_df: pl.DataFrame, + top_stable_genes_summary_df: pl.DataFrame, + all_counts_df: pl.DataFrame, top_stable_genes_counts_df: pl.DataFrame, ): """Export gene expression data to CSV files.""" logger.info(f"Exporting statistics of all genes to: {ALL_GENE_SUMMARY_OUTFILENAME}") - all_genes_summary_lf.collect().write_csv( + all_genes_summary_df.write_csv( ALL_GENE_SUMMARY_OUTFILENAME, float_precision=config.CSV_FLOAT_PRECISION ) logger.info( f"Exporting statistics of the top stable genes to: {TOP_STABLE_GENE_SUMMARY_OUTFILENAME}" ) - top_stable_genes_summary_lf.collect().write_csv( + top_stable_genes_summary_df.write_csv( TOP_STABLE_GENE_SUMMARY_OUTFILENAME, float_precision=config.CSV_FLOAT_PRECISION ) logger.info(f"Exporting all counts to: {ALL_COUNTS_FILTERED_PARQUET_OUTFILENAME}") - all_counts_lf.collect().write_parquet(ALL_COUNTS_FILTERED_PARQUET_OUTFILENAME) + all_counts_df.write_parquet(ALL_COUNTS_FILTERED_PARQUET_OUTFILENAME) logger.info( f"Exporting counts of the top stable genes to: {TOP_STABLE_GENES_COUNTS_OUTFILENAME}" @@ -285,38 +261,39 @@ def main(): else [] ) - count_lf = get_counts(args.count_file) + count_df = get_counts(args.count_file) # getting data, including metadata and mappings - all_genes_stat_summary_lf = pl.scan_csv(args.stat_file) + all_genes_stat_summary_df = parse_stat_file(args.stat_file) - platform_datasets_stat_lfs = [ - pl.scan_csv(file) + platform_datasets_stat_dfs = [ + parse_stat_file(file) for file in [args.rnaseq_dataset_stat_file, args.microarray_dataset_stat_file] if file is not None ] - metadata_lf = get_metadata(metadata_files) - mapping_lf = get_mappings(mapping_files) - optional_lfs = [lf for lf in [metadata_lf, mapping_lf] if lf is not None] - additional_data_lfs = optional_lfs + platform_datasets_stat_lfs - all_genes_summary_lf = get_all_genes_summary( - all_genes_stat_summary_lf, *additional_data_lfs + metadata_df = get_metadata(metadata_files) + mapping_df = get_mappings(mapping_files) + optional_dfs = [df for df in [metadata_df, mapping_df] if df is not None] + + additional_data_dfs = optional_dfs + platform_datasets_stat_dfs + all_genes_summary_df = get_all_genes_summary( + all_genes_stat_summary_df, *additional_data_dfs ) - top_stable_stat_summary_lf = all_genes_summary_lf.head(NB_TOP_STABLE_GENES) + top_stable_stat_summary_df = all_genes_summary_df.head(NB_TOP_STABLE_GENES) # reducing dataframe size (it is only used for plotting by MultiQC) - count_lf = cast_count_columns_to_float32(count_lf) + count_df = cast_count_columns_to_float(count_df) top_stable_genes_counts_df = get_top_stable_genes_counts( - count_lf, top_stable_stat_summary_lf + count_df, top_stable_stat_summary_df ) # exporting computed data export_data( - all_genes_summary_lf, - top_stable_stat_summary_lf, - count_lf, + all_genes_summary_df, + top_stable_stat_summary_df, + count_df, top_stable_genes_counts_df, ) From bf29940682b1905efe02898331ad1664bb72a2df Mon Sep 17 00:00:00 2001 From: Olivier Date: Thu, 20 Nov 2025 21:29:07 +0100 Subject: [PATCH 182/258] fix issues with id mapping reformating --- conf/modules/id_mapping.config | 16 +- modules/local/aggregate_results/main.nf | 8 +- nextflow.config | 4 +- nextflow_schema.json | 2 +- subworkflows/local/idmapping/main.nf | 57 ++- tests/default.nf.test | 25 + tests/default.nf.test.snap | 606 ++++++++---------------- workflows/stableexpression.nf | 8 +- 8 files changed, 292 insertions(+), 434 deletions(-) diff --git a/conf/modules/id_mapping.config b/conf/modules/id_mapping.config index 4317fb79..022316fe 100644 --- a/conf/modules/id_mapping.config +++ b/conf/modules/id_mapping.config @@ -1,8 +1,22 @@ process { + withName: COLLECT_GENE_IDS { + publishDir = [ + path: { "${params.outdir}/idmapping/collected_gene_ids" }, + mode: params.publish_dir_mode + ] + } + withName: GPROFILER_IDMAPPING { publishDir = [ - path: { "${params.outdir}/idmapping/" }, + path: { "${params.outdir}/idmapping/gprofiler" }, + mode: params.publish_dir_mode + ] + } + + withName: RENAME_GENE_IDS { + publishDir = [ + path: { "${params.outdir}/idmapping/renamed" }, mode: params.publish_dir_mode ] } diff --git a/modules/local/aggregate_results/main.nf b/modules/local/aggregate_results/main.nf index c10bacd2..229a99f0 100644 --- a/modules/local/aggregate_results/main.nf +++ b/modules/local/aggregate_results/main.nf @@ -10,10 +10,10 @@ process AGGREGATE_RESULTS { input: path count_file path stat_file - path rnaseq_dataset_stat_file, stageAs: "*/*" - path microarray_dataset_stat_file, stageAs: "*/*" - path metadata_files, stageAs: "*/*" - path mapping_files, stageAs: "*/*" + path rnaseq_dataset_stat_file + path microarray_dataset_stat_file + path metadata_files + path mapping_files output: path 'all_genes_summary.csv', emit: all_genes_summary diff --git a/nextflow.config b/nextflow.config index cbc454f6..48537e4d 100644 --- a/nextflow.config +++ b/nextflow.config @@ -37,9 +37,9 @@ params { exclude_geo_accessions_file = null // ID mapping - gprofiler_target_db = "ENTREZGENE" + gprofiler_target_db = "ENSG" gene_metadata = null - gene_id_mapping_file = null + gene_id_mapping = null skip_id_mapping = false // statistics diff --git a/nextflow_schema.json b/nextflow_schema.json index 7e5aa942..e9c6d43e 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -191,7 +191,7 @@ "default": "ENSG", "help_text": "Target database for g:Profiler. You can see the full list of available target databases at https://biit.cs.ut.ee/gprofiler/convert." }, - "gene_id_mapping_file": { + "gene_id_mapping": { "type": "string", "format": "file-path", "exists": true, diff --git a/subworkflows/local/idmapping/main.nf b/subworkflows/local/idmapping/main.nf index d60b56c1..da62a6b4 100644 --- a/subworkflows/local/idmapping/main.nf +++ b/subworkflows/local/idmapping/main.nf @@ -17,35 +17,86 @@ workflow ID_MAPPING { gprofiler_target_db ch_custom_gene_id_mapping ch_custom_gene_metadata + outdir main: - ch_counts.view { a -> "counts ${a}"} + ch_gene_id_mapping = Channel.empty() + ch_gene_metadata = Channel.empty() if ( !params.skip_id_mapping ) { + // ----------------------------------------------------------------- + // COLLECTING ALL GENE IDS FROm ALL DATASETS + // ----------------------------------------------------------------- + COLLECT_GENE_IDS( ch_counts.map{ meta, file -> file }.collect() ) + // ----------------------------------------------------------------- + // MAPPING THESE GENE IDS TO THE CHOSEN TARGET DB + // ----------------------------------------------------------------- + GPROFILER_IDMAPPING( COLLECT_GENE_IDS.out.gene_ids, species, gprofiler_target_db ) GPROFILER_IDMAPPING.out.mapping.set { ch_gene_id_mapping } + GPROFILER_IDMAPPING.out.metadata.set { ch_gene_metadata } } + // ----------------------------------------------------------------- + // RENAMING GENE IDS IN ALL COUNT DATASETS + // ----------------------------------------------------------------- + RENAME_GENE_IDS( ch_counts, ch_gene_id_mapping, ch_custom_gene_id_mapping ) + // ----------------------------------------------------------------- + // COLLECTING GLOBAL GENE ID MAPPING AND METADATA + // ----------------------------------------------------------------- + + ch_gene_id_mapping + .mix( ch_custom_gene_id_mapping ) + .filter { it != [] } // handle no custom mappings + .splitCsv( header: true ) + .unique() + .collectFile( + name: 'global_gene_id_mapping.csv', + seed: "original_gene_id,gene_id", + newLine: true, + storeDir: "${params.outdir}/idmapping/" + ) { + item -> "${item["original_gene_id"]},${item["gene_id"]}" + } + .ifEmpty([]) + .set { ch_global_gene_id_mapping } + + ch_gene_metadata + .mix( ch_custom_gene_metadata ) + .filter { it != [] } // handle no custom metadata + .splitCsv( header: true ) + .unique() + .collectFile( + name: 'global_gene_metadata.csv', + seed: "gene_id,name,description", + newLine: true, + storeDir: "${params.outdir}/idmapping/" + ) { + item -> "${item["gene_id"]},${item["name"]},${item["description"]}" + } + .ifEmpty([]) + .set { ch_global_gene_metadata } + emit: counts = RENAME_GENE_IDS.out.counts - mapping = ch_gene_id_mapping - metadata = GPROFILER_IDMAPPING.out.metadata + mapping = ch_global_gene_id_mapping + metadata = ch_global_gene_metadata } diff --git a/tests/default.nf.test b/tests/default.nf.test index 51729294..9eb232e4 100644 --- a/tests/default.nf.test +++ b/tests/default.nf.test @@ -261,6 +261,31 @@ nextflow_pipeline { } } + test("-profile test_gprofiler_target_database_entrez") { + + tag "test" + + when { + params { + species = 'beta vulgaris' + gprofiler_target_database = 'ENTREZGENE' + outdir = "$outputDir" + } + } + + then { + def stable_name = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}']) + def stable_path = getAllFilesFromDir(params.outdir, ignoreFile: 'tests/.nftignore') + assertAll( + { assert workflow.success}, + { assert snapshot( + stable_name, + stable_path + ).match() } + ) + } + } + test("-profile test_full") { tag "test_full" diff --git a/tests/default.nf.test.snap b/tests/default.nf.test.snap index bb9c291d..ffa5d8e4 100644 --- a/tests/default.nf.test.snap +++ b/tests/default.nf.test.snap @@ -8,8 +8,8 @@ "aggregate_results/all_genes_summary.csv", "aggregate_results/top_stable_genes_summary.csv", "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", - "clean_count_data", - "clean_count_data/cleaned_counts_filtered.parquet", + "collect_gene_ids", + "collect_gene_ids/all_gene_ids.txt", "compute_base_statistics", "compute_base_statistics/stats_all_genes.csv", "compute_base_statistics_for_rnaseq", @@ -48,16 +48,13 @@ "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", "dash_app/versions.yml", - "dataset_statistics", - "dataset_statistics/SRP254919.salmon.merged.gene_counts.top1000cov.assay.dataset_stats.csv", "errors", "geo", "get_candidate_genes", "get_candidate_genes/candidate_counts.parquet", "idmapping", - "idmapping/SRP254919.salmon.merged.gene_counts.top1000cov.assay.mapping.csv", - "idmapping/SRP254919.salmon.merged.gene_counts.top1000cov.assay.metadata.csv", - "idmapping/SRP254919.salmon.merged.gene_counts.top1000cov.assay.renamed.csv", + "idmapping/gene_metadata.csv", + "idmapping/mapped_gene_ids.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", "merge_all_counts", @@ -104,23 +101,28 @@ "quantile_normalised", "quantile_normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay", "quantile_normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay/SRP254919.salmon.merged.gene_counts.top1000cov.assay.renamed.cpm.quant_norm.parquet", - "warnings" + "rename_gene_ids", + "rename_gene_ids/SRP254919.salmon.merged.gene_counts.top1000cov.assay.renamed.csv", + "rename_gene_ids/warning_reason.txt", + "warnings", + "warnings/renaming_warning_reasons.tsv" ], [ - "all_genes_summary.csv:md5,5f51fb8a0383a9cc6e5f68f038b2824b", - "top_stable_genes_summary.csv:md5,5f51fb8a0383a9cc6e5f68f038b2824b", - "top_stable_genes_transposed_counts_filtered.csv:md5,981651a618f221898767331191517a0b", - "stats_all_genes.csv:md5,7c8675eea31265f9ef3e9c807e4ade42", - "rnaseq.stats_all_genes.csv:md5,d3a3334d0a2fd7312f2576a6240997f2", - "stats_with_scores.csv:md5,be0e4235981e652d9ddf84fccb879267", + "all_genes_summary.csv:md5,3e247792dc392db19887cf7423569495", + "top_stable_genes_summary.csv:md5,3e247792dc392db19887cf7423569495", + "top_stable_genes_transposed_counts_filtered.csv:md5,5ec0c8fa780b269c163d962eb01adea8", + "all_gene_ids.txt:md5,8ad5bcdc524384021376a78bc2565a99", + "stats_all_genes.csv:md5,8fc34fdea55993cad7ef2db2faf651c0", + "rnaseq.stats_all_genes.csv:md5,faf7993cd157c40141ddb323aa7ac36c", + "stats_with_scores.csv:md5,60b002a9be7769de2a90d246ed57b7f5", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_genes_summary.csv:md5,5f51fb8a0383a9cc6e5f68f038b2824b", + "all_genes_summary.csv:md5,3e247792dc392db19887cf7423569495", "whole_design.csv:md5,f29515bc2c783e593fb9028127342593", "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", - "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", + "genes.py:md5,680cb5f4e107a3b091821917d72a555c", "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", @@ -128,29 +130,30 @@ "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", - "tables.py:md5,84d17ad7f43458412e550f74a24560e5", + "tables.py:md5,92e8cc03fd8c4192af9201a7ff3cd979", "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", - "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", + "config.py:md5,b629adc64e497622f000945ee6e6aed2", + "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", "style.py:md5,b9f1207f06464e43c05e6e3912f12731", "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "SRP254919.salmon.merged.gene_counts.top1000cov.assay.dataset_stats.csv:md5,b1eade259446b4134a7e357aee92911a", - "SRP254919.salmon.merged.gene_counts.top1000cov.assay.mapping.csv:md5,7858e0e480ead97ecdfceea91be68b0f", - "SRP254919.salmon.merged.gene_counts.top1000cov.assay.metadata.csv:md5,7477308e2615ced44692cacb4ace178e", - "SRP254919.salmon.merged.gene_counts.top1000cov.assay.renamed.csv:md5,797b5e4f3a3f83f6c8d9aa7b35ddc3c6", - "whole_gene_id_mapping.csv:md5,9923ab8dfbe4bdbee956684dd7a1ec92", - "whole_gene_metadata.csv:md5,e1c71f34a81565a15aaf6db420d56c4a", + "gene_metadata.csv:md5,e7b851264bc7df54a125c94c716ccea8", + "mapped_gene_ids.csv:md5,5177a0ac44d712aac833f780edbf8503", + "whole_gene_id_mapping.csv:md5,c938897a22f53321c60dd91e8919632e", + "whole_gene_metadata.csv:md5,9e6318fedc235ee80109daccc483fdf6", "whole_design.csv:md5,f29515bc2c783e593fb9028127342593", - "SRP254919.salmon.merged.gene_counts.top1000cov.assay.renamed.cpm.csv:md5,45db1f53c79ff459ad5689bbb4e0a1ca", - "stability_values.normfinder.csv:md5,68f8cb62b8ba78ff6ea2dd11e9e988e4" + "SRP254919.salmon.merged.gene_counts.top1000cov.assay.renamed.cpm.csv:md5,e2ab50b0ae172252c167f9417522f299", + "stability_values.normfinder.csv:md5,0cd79a0ea440668c1b953a14f7b1d9cd", + "SRP254919.salmon.merged.gene_counts.top1000cov.assay.renamed.csv:md5,dccda08cf7401f15b822f4a476c240c8", + "warning_reason.txt:md5,938cbf4fb283483dd717106f3bfdee07", + "renaming_warning_reasons.tsv:md5,404d8e09a572a5e6aa94ec75d4c4a1dd" ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-19T15:09:38.05737019" + "timestamp": "2025-11-20T19:30:06.162229336" }, "-profile test_eatlas_only_with_keywords": { "content": [ @@ -161,8 +164,8 @@ "aggregate_results/all_genes_summary.csv", "aggregate_results/top_stable_genes_summary.csv", "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", - "clean_count_data", - "clean_count_data/cleaned_counts_filtered.parquet", + "collect_gene_ids", + "collect_gene_ids/all_gene_ids.txt", "compute_base_statistics", "compute_base_statistics/stats_all_genes.csv", "compute_base_statistics_for_rnaseq", @@ -201,8 +204,6 @@ "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", "dash_app/versions.yml", - "dataset_statistics", - "dataset_statistics/E_MTAB_8187_rnaseq.dataset_stats.csv", "errors", "expression_atlas", "expression_atlas/accessions", @@ -216,9 +217,8 @@ "get_candidate_genes", "get_candidate_genes/candidate_counts.parquet", "idmapping", - "idmapping/E_MTAB_8187_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/E_MTAB_8187_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/gene_metadata.csv", + "idmapping/mapped_gene_ids.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", "merge_all_counts", @@ -236,7 +236,6 @@ "multiqc/multiqc_data/multiqc_data.json", "multiqc/multiqc_data/multiqc_eatlas_all_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", "multiqc/multiqc_data/multiqc_gene_statistics.txt", "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", @@ -245,19 +244,16 @@ "multiqc/multiqc_plots/pdf", "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", "multiqc/multiqc_plots/pdf/gene_statistics.pdf", "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", "multiqc/multiqc_plots/png", "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", - "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", "multiqc/multiqc_plots/png/gene_statistics.png", "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", "multiqc/multiqc_plots/svg", "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", "multiqc/multiqc_plots/svg/gene_statistics.svg", "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", "multiqc/multiqc_report.html", @@ -273,23 +269,28 @@ "quantile_normalised", "quantile_normalised/E_MTAB_8187_rnaseq", "quantile_normalised/E_MTAB_8187_rnaseq/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", - "warnings" + "rename_gene_ids", + "rename_gene_ids/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv", + "rename_gene_ids/warning_reason.txt", + "warnings", + "warnings/renaming_warning_reasons.tsv" ], [ - "all_genes_summary.csv:md5,b20bf9df3ba0d7d82510fa632f5531df", - "top_stable_genes_summary.csv:md5,3f6ad9a7f2f184b7a4a4651935b1da42", - "top_stable_genes_transposed_counts_filtered.csv:md5,b9e7e88055bd0ff844bce2787c795683", - "stats_all_genes.csv:md5,0262d1ed114708648080d917cf6d735d", - "rnaseq.stats_all_genes.csv:md5,f898b1e455ab27d1c4b3642ec0680b98", - "stats_with_scores.csv:md5,b2e3634ccae3e031d32feff0bd3dfd1f", + "all_genes_summary.csv:md5,94feecb8ce32bcdee6c3e51de6e0dc75", + "top_stable_genes_summary.csv:md5,f3fc83cec676b75472d30e54c5ca9b6a", + "top_stable_genes_transposed_counts_filtered.csv:md5,86b247718f957d3a9364cf1a08189dc1", + "all_gene_ids.txt:md5,e4652fea77f29d9f31bf9353d6dfa9c2", + "stats_all_genes.csv:md5,f6699c8549e7a4cb9105f021d53590e1", + "rnaseq.stats_all_genes.csv:md5,9750eb04fe96150c411aa673a8dad3e2", + "stats_with_scores.csv:md5,d24a8d1ac6c7258346b664d132cf866a", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_genes_summary.csv:md5,b20bf9df3ba0d7d82510fa632f5531df", + "all_genes_summary.csv:md5,94feecb8ce32bcdee6c3e51de6e0dc75", "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", - "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", + "genes.py:md5,680cb5f4e107a3b091821917d72a555c", "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", @@ -297,101 +298,43 @@ "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", - "tables.py:md5,84d17ad7f43458412e550f74a24560e5", + "tables.py:md5,92e8cc03fd8c4192af9201a7ff3cd979", "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", - "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", + "config.py:md5,b629adc64e497622f000945ee6e6aed2", + "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", "style.py:md5,b9f1207f06464e43c05e6e3912f12731", "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "E_MTAB_8187_rnaseq.dataset_stats.csv:md5,cfe732af026c75716331b4c043f0828c", "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.mapping.csv:md5,d95581c2b10beed3ba4a4556c1afc44f", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.metadata.csv:md5,faf9789296e5578fd31efee8d0c40ca9", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv:md5,92d1acd8e9d5e5c8f44b25e1ffce66c2", - "whole_gene_id_mapping.csv:md5,d95581c2b10beed3ba4a4556c1afc44f", - "whole_gene_metadata.csv:md5,faf9789296e5578fd31efee8d0c40ca9", + "gene_metadata.csv:md5,79edd662afe94aad5125708c2d93abc5", + "mapped_gene_ids.csv:md5,5cb327ac5eec9ad4926c2745e67c621e", + "whole_gene_id_mapping.csv:md5,b061b542a0c3eb0ca5712ed3c4837bc4", + "whole_gene_metadata.csv:md5,727820034d9c2fdd54be98b55848aee0", "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,e8dfabbc566f7c096c1a850084d4768b", - "stability_values.normfinder.csv:md5,64082412b0d0c0903b3677ebfc4131af" + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,73780e145259992a172ffa7ebdc8ddbd", + "stability_values.normfinder.csv:md5,78acf60ce7914a64bb1a1b80fc223170", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv:md5,d3efe7dc241425b9f3c190fe36c6e595", + "warning_reason.txt:md5,0042ef8923246d99e5ef2a9068caa1a8", + "renaming_warning_reasons.tsv:md5,794de71c61302d26675414b20f0ddf11" ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-19T15:15:16.561840879" + "timestamp": "2025-11-20T19:37:24.912985842" }, "-profile test_skip_id_mapping": { "content": [ [ - "aggregate_results", - "aggregate_results/all_counts_filtered.parquet", - "aggregate_results/all_genes_summary.csv", - "aggregate_results/top_stable_genes_summary.csv", - "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", - "clean_count_data", - "clean_count_data/cleaned_counts_filtered.parquet", - "compute_base_statistics", - "compute_base_statistics/stats_all_genes.csv", - "compute_base_statistics_for_microarray", - "compute_base_statistics_for_microarray/microarray.stats_all_genes.csv", - "compute_base_statistics_for_rnaseq", - "compute_base_statistics_for_rnaseq/rnaseq.stats_all_genes.csv", - "compute_stability_scores", - "compute_stability_scores/stats_with_scores.csv", - "dash_app", - "dash_app/app.py", - "dash_app/assets", - "dash_app/assets/style.css", - "dash_app/data", - "dash_app/data/all_counts.parquet", - "dash_app/data/all_genes_summary.csv", - "dash_app/data/whole_design.csv", - "dash_app/file_system_backend", - "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", - "dash_app/spec-file.txt", - "dash_app/src", - "dash_app/src/callbacks", - "dash_app/src/callbacks/common.py", - "dash_app/src/callbacks/genes.py", - "dash_app/src/callbacks/samples.py", - "dash_app/src/components", - "dash_app/src/components/graphs.py", - "dash_app/src/components/icons.py", - "dash_app/src/components/right_sidebar.py", - "dash_app/src/components/settings", - "dash_app/src/components/settings/genes.py", - "dash_app/src/components/settings/samples.py", - "dash_app/src/components/stores.py", - "dash_app/src/components/tables.py", - "dash_app/src/components/tooltips.py", - "dash_app/src/components/top.py", - "dash_app/src/utils", - "dash_app/src/utils/config.py", - "dash_app/src/utils/data_management.py", - "dash_app/src/utils/style.py", - "dash_app/versions.yml", - "dataset_statistics", - "dataset_statistics/microarray.normalised.dataset_stats.csv", - "dataset_statistics/rnaseq.raw.dataset_stats.csv", "errors", "geo", - "get_candidate_genes", - "get_candidate_genes/candidate_counts.parquet", "idmapping", - "merge_all_counts", - "merge_all_counts/all_counts.parquet", - "merge_microarray_counts", - "merge_microarray_counts/all_counts.parquet", - "merge_rnaseq_counts", - "merge_rnaseq_counts/all_counts.parquet", "merged_datasets", - "merged_datasets/whole_design.csv", "multiqc", "multiqc/multiqc_data", "multiqc/multiqc_data/llms-full.txt", @@ -399,83 +342,23 @@ "multiqc/multiqc_data/multiqc.parquet", "multiqc/multiqc_data/multiqc_citations.txt", "multiqc/multiqc_data/multiqc_data.json", - "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", - "multiqc/multiqc_data/multiqc_gene_statistics.txt", - "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", - "multiqc/multiqc_plots", - "multiqc/multiqc_plots/pdf", - "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", - "multiqc/multiqc_plots/pdf/gene_statistics.pdf", - "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", - "multiqc/multiqc_plots/png", - "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", - "multiqc/multiqc_plots/png/gene_statistics.png", - "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", - "multiqc/multiqc_plots/svg", - "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", - "multiqc/multiqc_plots/svg/gene_statistics.svg", - "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", - "normalised", - "normalised/rnaseq.raw", - "normalised/rnaseq.raw/normalisation_deseq2", - "normalised/rnaseq.raw/normalisation_deseq2/rnaseq.raw.cpm.csv", - "normfinder", - "normfinder/stability_values.normfinder.csv", "pipeline_info", "pipeline_info/software_mqc_versions.yml", - "quantile_normalised", - "quantile_normalised/microarray.normalised", - "quantile_normalised/microarray.normalised/microarray.normalised.quant_norm.parquet", - "quantile_normalised/rnaseq.raw", - "quantile_normalised/rnaseq.raw/rnaseq.raw.cpm.quant_norm.parquet", "warnings" ], [ - "all_genes_summary.csv:md5,14fb1614a449f7162987cd9195d0cb1c", - "top_stable_genes_summary.csv:md5,14fb1614a449f7162987cd9195d0cb1c", - "top_stable_genes_transposed_counts_filtered.csv:md5,20d963b92dcf1b4d85c6e878cff17253", - "stats_all_genes.csv:md5,08b6baca5dd63d9e57b9b1c080a901c6", - "microarray.stats_all_genes.csv:md5,69c6a3908fa708e8f3670a9e96321f7d", - "rnaseq.stats_all_genes.csv:md5,427f59e5c2b5920f83bab591268e1fcb", - "stats_with_scores.csv:md5,f38a32ae4520ec843b076502b2d80074", - "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", - "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_genes_summary.csv:md5,14fb1614a449f7162987cd9195d0cb1c", - "whole_design.csv:md5,70d6c2673e619ca52d2774fb3e368382", - "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", - "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", - "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", - "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", - "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", - "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", - "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", - "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", - "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", - "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", - "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", - "tables.py:md5,84d17ad7f43458412e550f74a24560e5", - "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", - "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", - "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", - "style.py:md5,b9f1207f06464e43c05e6e3912f12731", - "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "microarray.normalised.dataset_stats.csv:md5,4993a895b6561b9a4f939906bae582b4", - "rnaseq.raw.dataset_stats.csv:md5,c407ed714414e689c760fe1b450d5f58", - "whole_design.csv:md5,70d6c2673e619ca52d2774fb3e368382", - "rnaseq.raw.cpm.csv:md5,59f604e6266fbc46948287f384c3639b", - "stability_values.normfinder.csv:md5,4e37152d1f0a8a4376b86cbf64b9f73c" + ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-19T14:43:26.609074576" + "timestamp": "2025-11-20T20:56:47.368093375" }, "-profile test": { "content": [ @@ -485,8 +368,8 @@ "aggregate_results/all_genes_summary.csv", "aggregate_results/top_stable_genes_summary.csv", "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", - "clean_count_data", - "clean_count_data/cleaned_counts_filtered.parquet", + "collect_gene_ids", + "collect_gene_ids/all_gene_ids.txt", "compute_base_statistics", "compute_base_statistics/stats_all_genes.csv", "compute_base_statistics_for_rnaseq", @@ -525,8 +408,6 @@ "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", "dash_app/versions.yml", - "dataset_statistics", - "dataset_statistics/E_MTAB_8187_rnaseq.dataset_stats.csv", "errors", "errors/normalisation_failure_reasons.tsv", "expression_atlas", @@ -548,12 +429,8 @@ "get_candidate_genes", "get_candidate_genes/candidate_counts.parquet", "idmapping", - "idmapping/E_MTAB_8187_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/E_MTAB_8187_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv", - "idmapping/beta_vulgaris.rnaseq.raw.counts.mapping.csv", - "idmapping/beta_vulgaris.rnaseq.raw.counts.metadata.csv", - "idmapping/beta_vulgaris.rnaseq.raw.counts.renamed.csv", + "idmapping/gene_metadata.csv", + "idmapping/mapped_gene_ids.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", "merge_all_counts", @@ -571,7 +448,6 @@ "multiqc/multiqc_data/multiqc_data.json", "multiqc/multiqc_data/multiqc_eatlas_all_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", "multiqc/multiqc_data/multiqc_gene_statistics.txt", "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_geo_rejected_experiments_metadata.txt", @@ -584,7 +460,6 @@ "multiqc/multiqc_plots/pdf", "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", "multiqc/multiqc_plots/pdf/gene_statistics.pdf", "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/geo_rejected_experiments_metadata.pdf", @@ -594,7 +469,6 @@ "multiqc/multiqc_plots/png", "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", - "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", "multiqc/multiqc_plots/png/gene_statistics.png", "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", "multiqc/multiqc_plots/png/geo_rejected_experiments_metadata.png", @@ -604,7 +478,6 @@ "multiqc/multiqc_plots/svg", "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", "multiqc/multiqc_plots/svg/gene_statistics.svg", "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", "multiqc/multiqc_plots/svg/geo_rejected_experiments_metadata.svg", @@ -627,24 +500,30 @@ "quantile_normalised", "quantile_normalised/E_MTAB_8187_rnaseq", "quantile_normalised/E_MTAB_8187_rnaseq/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", + "rename_gene_ids", + "rename_gene_ids/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv", + "rename_gene_ids/beta_vulgaris.rnaseq.raw.counts.renamed.csv", + "rename_gene_ids/warning_reason.txt", "warnings", - "warnings/geo_warning_reasons.csv" + "warnings/geo_warning_reasons.csv", + "warnings/renaming_warning_reasons.tsv" ], [ - "all_genes_summary.csv:md5,b20bf9df3ba0d7d82510fa632f5531df", - "top_stable_genes_summary.csv:md5,3f6ad9a7f2f184b7a4a4651935b1da42", - "top_stable_genes_transposed_counts_filtered.csv:md5,b9e7e88055bd0ff844bce2787c795683", - "stats_all_genes.csv:md5,0262d1ed114708648080d917cf6d735d", - "rnaseq.stats_all_genes.csv:md5,f898b1e455ab27d1c4b3642ec0680b98", - "stats_with_scores.csv:md5,b2e3634ccae3e031d32feff0bd3dfd1f", + "all_genes_summary.csv:md5,c9f6ecd47c480351c7a73950fccc5985", + "top_stable_genes_summary.csv:md5,f3fc83cec676b75472d30e54c5ca9b6a", + "top_stable_genes_transposed_counts_filtered.csv:md5,86b247718f957d3a9364cf1a08189dc1", + "all_gene_ids.txt:md5,1479fc03fa73fc2f11a2f25509537992", + "stats_all_genes.csv:md5,f6699c8549e7a4cb9105f021d53590e1", + "rnaseq.stats_all_genes.csv:md5,9750eb04fe96150c411aa673a8dad3e2", + "stats_with_scores.csv:md5,d24a8d1ac6c7258346b664d132cf866a", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_genes_summary.csv:md5,b20bf9df3ba0d7d82510fa632f5531df", + "all_genes_summary.csv:md5,c9f6ecd47c480351c7a73950fccc5985", "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", - "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", + "genes.py:md5,680cb5f4e107a3b091821917d72a555c", "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", @@ -652,14 +531,13 @@ "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", - "tables.py:md5,84d17ad7f43458412e550f74a24560e5", + "tables.py:md5,92e8cc03fd8c4192af9201a7ff3cd979", "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", - "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", + "config.py:md5,b629adc64e497622f000945ee6e6aed2", + "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", "style.py:md5,b9f1207f06464e43c05e6e3912f12731", "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "E_MTAB_8187_rnaseq.dataset_stats.csv:md5,cfe732af026c75716331b4c043f0828c", "normalisation_failure_reasons.tsv:md5,365a7f147247027d6346a95d80de4e9e", "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", @@ -667,30 +545,30 @@ "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", "accessions.txt:md5,63a651d9df354aef24400cebe56dd5ec", - "geo_all_datasets.metadata.tsv:md5,0df782fb63ecb75da068d780c46e5118", - "geo_rejected_datasets.metadata.tsv:md5,915b3e94343e1ade9247125cbab5ce9c", - "geo_selected_datasets.metadata.tsv:md5,9daef6dbdb514239685095bae4fd8adc", - "warning_reason.txt:md5,a5d754439419aabc9cf3ea347a9ee238", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.mapping.csv:md5,d95581c2b10beed3ba4a4556c1afc44f", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.metadata.csv:md5,faf9789296e5578fd31efee8d0c40ca9", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv:md5,92d1acd8e9d5e5c8f44b25e1ffce66c2", - "beta_vulgaris.rnaseq.raw.counts.mapping.csv:md5,df61b884eaf7b2734361d4d262de4833", - "beta_vulgaris.rnaseq.raw.counts.metadata.csv:md5,a9eaec4261ce39f4ab85684eb03c1581", - "beta_vulgaris.rnaseq.raw.counts.renamed.csv:md5,6da8c317c6dafccbb869be5074f597b2", - "whole_gene_id_mapping.csv:md5,d95581c2b10beed3ba4a4556c1afc44f", - "whole_gene_metadata.csv:md5,faf9789296e5578fd31efee8d0c40ca9", + "geo_all_datasets.metadata.tsv:md5,83689531d044d7682413c4fa51ac4cff", + "geo_rejected_datasets.metadata.tsv:md5,0a66c9d519b4590e48b04e4c37d66416", + "geo_selected_datasets.metadata.tsv:md5,1d41ab3e480dde9aec6abd0a6270144b", + "warning_reason.txt:md5,603e30b732b7a6b501b59adf9d0e8837", + "gene_metadata.csv:md5,3cfdc44c94bab17efb2db7600a82b921", + "mapped_gene_ids.csv:md5,3bcff962493cb088786764980de4e315", + "whole_gene_id_mapping.csv:md5,b061b542a0c3eb0ca5712ed3c4837bc4", + "whole_gene_metadata.csv:md5,3cf8bd030195e36ea10bf99a322712b1", "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,e8dfabbc566f7c096c1a850084d4768b", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,73780e145259992a172ffa7ebdc8ddbd", "failure_reason.txt:md5,7f26a85e66f925b9718d543c01e06c51", - "stability_values.normfinder.csv:md5,64082412b0d0c0903b3677ebfc4131af", - "geo_warning_reasons.csv:md5,7bf9cf895848e0233e69682ed50b61f4" + "stability_values.normfinder.csv:md5,78acf60ce7914a64bb1a1b80fc223170", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv:md5,d3efe7dc241425b9f3c190fe36c6e595", + "beta_vulgaris.rnaseq.raw.counts.renamed.csv:md5,0a9c75be866ebd67c63a61a75bf25185", + "warning_reason.txt:md5,0042ef8923246d99e5ef2a9068caa1a8", + "geo_warning_reasons.csv:md5,b44f494b756cc0297f1d7b234bea1e13", + "renaming_warning_reasons.tsv:md5,41343bb944d9455d3c2a427196770f39" ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-19T14:52:40.766786924" + "timestamp": "2025-11-20T19:28:36.747967413" }, "-profile test_accessions_only": { "content": [ @@ -752,16 +630,16 @@ "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "accessions.txt:md5,a850f625a78be7b4b10ce08a5b638e23", - "geo_all_datasets.metadata.tsv:md5,965d93f4a53e07618e37eee8c4a7045a", - "geo_rejected_datasets.metadata.tsv:md5,bc079b56981e9f34e21bbc352603a6a9", - "geo_selected_datasets.metadata.tsv:md5,5adb3566a275e73dfefdb97288cb07a3" + "geo_all_datasets.metadata.tsv:md5,e9eb980e088b46aa80aa50680f129fda", + "geo_rejected_datasets.metadata.tsv:md5,4b4908f7ae40b84a1ac1bd5addfc8dd2", + "geo_selected_datasets.metadata.tsv:md5,fcbfbbaf55debe0c031d68f4139b881f" ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-19T15:10:02.908557156" + "timestamp": "2025-11-20T19:30:49.791557377" }, "-profile test_one_accession_low_gene_count": { "content": [ @@ -772,8 +650,8 @@ "aggregate_results/all_genes_summary.csv", "aggregate_results/top_stable_genes_summary.csv", "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", - "clean_count_data", - "clean_count_data/cleaned_counts_filtered.parquet", + "collect_gene_ids", + "collect_gene_ids/all_gene_ids.txt", "compute_base_statistics", "compute_base_statistics/stats_all_genes.csv", "compute_base_statistics_for_rnaseq", @@ -812,8 +690,6 @@ "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", "dash_app/versions.yml", - "dataset_statistics", - "dataset_statistics/E_GEOD_51720_rnaseq.dataset_stats.csv", "errors", "expression_atlas", "expression_atlas/datasets", @@ -824,9 +700,8 @@ "get_candidate_genes", "get_candidate_genes/candidate_counts.parquet", "idmapping", - "idmapping/E_GEOD_51720_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/E_GEOD_51720_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/gene_metadata.csv", + "idmapping/mapped_gene_ids.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", "merge_all_counts", @@ -873,23 +748,28 @@ "quantile_normalised", "quantile_normalised/E_GEOD_51720_rnaseq", "quantile_normalised/E_GEOD_51720_rnaseq/E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", - "warnings" + "rename_gene_ids", + "rename_gene_ids/E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.csv", + "rename_gene_ids/warning_reason.txt", + "warnings", + "warnings/renaming_warning_reasons.tsv" ], [ - "all_genes_summary.csv:md5,587f5144b7fa820b3754d88db9e399ce", - "top_stable_genes_summary.csv:md5,8b8d522a43d97191ed2a912d23414351", - "top_stable_genes_transposed_counts_filtered.csv:md5,0b4f76835a28815c0f4c681c8ac95cb3", - "stats_all_genes.csv:md5,9f7b7b34c3d088eb1edd33354ed11649", - "rnaseq.stats_all_genes.csv:md5,60b4a078fc13f84b0c39aa47e7158471", - "stats_with_scores.csv:md5,144fb6bd3fadebce473e7ae9adb47baf", + "all_genes_summary.csv:md5,56140489d6741194ad888f7eeb9e4658", + "top_stable_genes_summary.csv:md5,f06095d5043b6fce60841db00042311f", + "top_stable_genes_transposed_counts_filtered.csv:md5,9a73aa6c3b36b5eb6b63a6471d44e60e", + "all_gene_ids.txt:md5,62f966ba0a48f15c932dc269dc545300", + "stats_all_genes.csv:md5,292cfda45c3cdedb463f45e54b1be0fd", + "rnaseq.stats_all_genes.csv:md5,be2fee6b119c81a5916b50009317b011", + "stats_with_scores.csv:md5,f80f189d8b50173a40c9a234eae3bb90", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_genes_summary.csv:md5,587f5144b7fa820b3754d88db9e399ce", + "all_genes_summary.csv:md5,56140489d6741194ad888f7eeb9e4658", "whole_design.csv:md5,d3aa542c4ad07d0051a84482fe6cd81c", "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", - "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", + "genes.py:md5,680cb5f4e107a3b091821917d72a555c", "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", @@ -897,32 +777,33 @@ "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", - "tables.py:md5,84d17ad7f43458412e550f74a24560e5", + "tables.py:md5,92e8cc03fd8c4192af9201a7ff3cd979", "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", - "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", + "config.py:md5,b629adc64e497622f000945ee6e6aed2", + "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", "style.py:md5,b9f1207f06464e43c05e6e3912f12731", "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "E_GEOD_51720_rnaseq.dataset_stats.csv:md5,db6a65d30bd7cf403d6d8da054fad8f3", "E_GEOD_51720_rnaseq.design.csv:md5,80805afb29837b6fbb73a6aa6f3a461b", "E_GEOD_51720_rnaseq.rnaseq.raw.counts.csv:md5,07cd448196fc2fea4663bd9705da2b98", "excluded_geo_accessions.txt:md5,cdbec776e8b1d9dc7d0aa44aaf52aa50", - "E_GEOD_51720_rnaseq.rnaseq.raw.counts.mapping.csv:md5,1bbc8cddf18072309e81498806aa73ff", - "E_GEOD_51720_rnaseq.rnaseq.raw.counts.metadata.csv:md5,08861c29159a6a2fed38efe523ed9c56", - "E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.csv:md5,a8ca7cef22b4c6662cbb23cc56510ef7", - "whole_gene_id_mapping.csv:md5,1bbc8cddf18072309e81498806aa73ff", - "whole_gene_metadata.csv:md5,a08ab44a98542a8529d2a4b5a47e4408", + "gene_metadata.csv:md5,f641a146dc7f8faa4597e9dc67b20506", + "mapped_gene_ids.csv:md5,5dfac9c67e6bb8613f319cce464b3be8", + "whole_gene_id_mapping.csv:md5,f8933b4d441cdafc92b4bcee1de21bc5", + "whole_gene_metadata.csv:md5,4811b825f6fdc4b7fee3d3c8354de5d3", "whole_design.csv:md5,d3aa542c4ad07d0051a84482fe6cd81c", - "E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,5fe6d949da70f2adb7cee8da38d3adc7", - "stability_values.normfinder.csv:md5,8f161901b6e184e90cbd1a7fc96480e2" + "E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,c8da94c002d3ee224db4f2764d615ed4", + "stability_values.normfinder.csv:md5,96cba2aa1960465d3ce43c4a54e21956", + "E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.csv:md5,77d527c8a6acc65764107b438ffc6613", + "warning_reason.txt:md5,44180971030c5f396e79e8df28118231", + "renaming_warning_reasons.tsv:md5,dd423ff5f00af7273f44ba20e3ae5b5c" ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-19T15:13:20.579340051" + "timestamp": "2025-11-20T19:35:25.802480432" }, "-profile test_no_dataset_found": { "content": [ @@ -1039,20 +920,20 @@ "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", "accessions.txt:md5,a850f625a78be7b4b10ce08a5b638e23", - "geo_all_datasets.metadata.tsv:md5,965d93f4a53e07618e37eee8c4a7045a", - "geo_rejected_datasets.metadata.tsv:md5,bc079b56981e9f34e21bbc352603a6a9", - "geo_selected_datasets.metadata.tsv:md5,5adb3566a275e73dfefdb97288cb07a3", + "geo_all_datasets.metadata.tsv:md5,e9eb980e088b46aa80aa50680f129fda", + "geo_rejected_datasets.metadata.tsv:md5,4b4908f7ae40b84a1ac1bd5addfc8dd2", + "geo_selected_datasets.metadata.tsv:md5,fcbfbbaf55debe0c031d68f4139b881f", "GSE55951_GPL18429.microarray.normalised.counts.csv:md5,18fd2d728ad2ec5cb78f994f73375144", "GSE55951_GPL18429.microarray.normalised.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d", - "warning_reason.txt:md5,a5d754439419aabc9cf3ea347a9ee238", - "geo_warning_reasons.csv:md5,4c26f6aa5c37055fe76b7a58c065fa30" + "warning_reason.txt:md5,603e30b732b7a6b501b59adf9d0e8837", + "geo_warning_reasons.csv:md5,e08541568733e7eac853f73480679e15" ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-19T15:10:51.223721623" + "timestamp": "2025-11-20T19:32:11.160027332" }, "-profile test_full": { "content": [ @@ -1063,8 +944,8 @@ "aggregate_results/all_genes_summary.csv", "aggregate_results/top_stable_genes_summary.csv", "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", - "clean_count_data", - "clean_count_data/cleaned_counts_filtered.parquet", + "collect_gene_ids", + "collect_gene_ids/all_gene_ids.txt", "compute_base_statistics", "compute_base_statistics/stats_all_genes.csv", "compute_base_statistics_for_rnaseq", @@ -1259,8 +1140,6 @@ "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", "dash_app/versions.yml", - "dataset_statistics", - "dataset_statistics/E_MTAB_5072_rnaseq.dataset_stats.csv", "errors", "expression_atlas", "expression_atlas/accessions", @@ -1435,9 +1314,8 @@ "get_candidate_genes", "get_candidate_genes/candidate_counts.parquet", "idmapping", - "idmapping/E_MTAB_5072_rnaseq.rnaseq.raw.counts.mapping.csv", - "idmapping/E_MTAB_5072_rnaseq.rnaseq.raw.counts.metadata.csv", - "idmapping/E_MTAB_5072_rnaseq.rnaseq.raw.counts.renamed.csv", + "idmapping/gene_metadata.csv", + "idmapping/mapped_gene_ids.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", "make_chunks", @@ -1680,25 +1558,30 @@ "ratio_standard_variation/std.8.8.parquet", "ratio_standard_variation/std.8.9.parquet", "ratio_standard_variation/std.9.9.parquet", + "rename_gene_ids", + "rename_gene_ids/E_MTAB_5072_rnaseq.rnaseq.raw.counts.renamed.csv", + "rename_gene_ids/warning_reason.txt", "warnings", - "warnings/geo_warning_reasons.csv" + "warnings/geo_warning_reasons.csv", + "warnings/renaming_warning_reasons.tsv" ], [ - "all_genes_summary.csv:md5,51c2b151c90dd8c8304a875ef35104c9", - "top_stable_genes_summary.csv:md5,2dc7de173ad97c73e6c09ca493fcf1fa", - "top_stable_genes_transposed_counts_filtered.csv:md5,8f0ef2e7b839e502d5a3f6cc20034ab9", - "stats_all_genes.csv:md5,4af99acc51cf1bacda835e2aab1f0dd1", - "rnaseq.stats_all_genes.csv:md5,1489c8b543026b17051b8a4921427c50", - "m_measures.csv:md5,7db246dda58cba17bd2fc2bb79c70e93", - "stats_with_scores.csv:md5,e3e798790f3a0070104314d3ee00ace6", + "all_genes_summary.csv:md5,fe294b66b7a19069e9c2ec11b74935de", + "top_stable_genes_summary.csv:md5,057ca1fc76aa5e99275ecf6228794991", + "top_stable_genes_transposed_counts_filtered.csv:md5,b50d420084bc0a93a978f0a75a4b5ba7", + "all_gene_ids.txt:md5,22625a4db6439656f16c65db8fb3a884", + "stats_all_genes.csv:md5,67dfa7ff8906d28113e7631b3806d208", + "rnaseq.stats_all_genes.csv:md5,707b8461f08de27e19b097c45c064af0", + "m_measures.csv:md5,e40a66b35178d8e64678c64afa72a2e1", + "stats_with_scores.csv:md5,52eef1966f7c03ecc42cd38561bfb9f6", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_genes_summary.csv:md5,51c2b151c90dd8c8304a875ef35104c9", + "all_genes_summary.csv:md5,fe294b66b7a19069e9c2ec11b74935de", "whole_design.csv:md5,c9bfd7bc8ca365222e03e67eb24b9a76", "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", - "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", + "genes.py:md5,680cb5f4e107a3b091821917d72a555c", "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", @@ -1706,109 +1589,54 @@ "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", - "tables.py:md5,84d17ad7f43458412e550f74a24560e5", + "tables.py:md5,92e8cc03fd8c4192af9201a7ff3cd979", "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", - "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", + "config.py:md5,b629adc64e497622f000945ee6e6aed2", + "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", "style.py:md5,b9f1207f06464e43c05e6e3912f12731", "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "E_MTAB_5072_rnaseq.dataset_stats.csv:md5,e7e95cf0ac00cf2710321e9588f5f4ad", "accessions.txt:md5,561a967c16b2ef6c29fb643cd4002947", "selected_experiments.metadata.tsv:md5,385b4d7ed40829f1e764857e805d6dc4", "species_experiments.metadata.tsv:md5,385b4d7ed40829f1e764857e805d6dc4", "E_MTAB_5072_rnaseq.design.csv:md5,a1f33d126dde387a2d542381c44bc1f3", "E_MTAB_5072_rnaseq.rnaseq.raw.counts.csv:md5,c41c84899a380515d759b99eeccfe43e", "accessions.txt:md5,48a6870e7e7e7d481e9b28002e68880e", - "geo_all_datasets.metadata.tsv:md5,aae011d6e88c3b095b45caafb67fe968", - "geo_rejected_datasets.metadata.tsv:md5,8fe446cf2da9db7df8929c49fcef18b0", - "geo_selected_datasets.metadata.tsv:md5,37ee10d019b753d09b7d6d4da3ec6e9f", - "warning_reason.txt:md5,da67cfc13dc9c91064833a73039700b2", - "E_MTAB_5072_rnaseq.rnaseq.raw.counts.mapping.csv:md5,f103d18b88f8329f5fc8e7082d251deb", - "E_MTAB_5072_rnaseq.rnaseq.raw.counts.metadata.csv:md5,8ec8f92cc8f81ca44c4c737251d3bd4a", - "E_MTAB_5072_rnaseq.rnaseq.raw.counts.renamed.csv:md5,463d259147e6d4a0f80be34b3e4b8047", - "whole_gene_id_mapping.csv:md5,8824265badf80ba59db63b6dfa8672dc", - "whole_gene_metadata.csv:md5,8c89bfe0e0320d3354e78ba622764c88", + "geo_all_datasets.metadata.tsv:md5,55ab279dea14a2d364bb86a1ca66a809", + "geo_rejected_datasets.metadata.tsv:md5,02202ad9c3d49a03dbb73c64035a8bbc", + "geo_selected_datasets.metadata.tsv:md5,e1eb953da65d90fc28a778cb17942c33", + "warning_reason.txt:md5,46bd94872631702e89a304c0adb7a8c1", + "gene_metadata.csv:md5,03c8b32f0825b10cc339a5c10c784849", + "mapped_gene_ids.csv:md5,5d0b67f5bc2f925567819f3a14969c62", + "whole_gene_id_mapping.csv:md5,564c998be28353132cc83c33698e3c14", + "whole_gene_metadata.csv:md5,6498a7b61a11e805dc4d2f6c35a38219", "whole_design.csv:md5,c9bfd7bc8ca365222e03e67eb24b9a76", - "E_MTAB_5072_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,9d449d05aecf363a92ebb60ac9a16637", - "stability_values.normfinder.csv:md5,66ac05c256ad5d2a085aafcdee4d40b9", - "geo_warning_reasons.csv:md5,bd904c137f444293f64d506d156466e5" + "E_MTAB_5072_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,7e67733fed4cba812bd77b2fc9365d06", + "stability_values.normfinder.csv:md5,7c1c3a2bd591c8b6b19db49284880d75", + "E_MTAB_5072_rnaseq.rnaseq.raw.counts.renamed.csv:md5,830ad1daaeaf7c8153b4bd53ad484037", + "warning_reason.txt:md5,ffc358470d35651bee1c82f1d474277d", + "geo_warning_reasons.csv:md5,b8e73aabeebad5fcc89f58badae7abd8", + "renaming_warning_reasons.tsv:md5,71b625eda4916cec330ac03cc9447484" ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-19T15:20:29.251259083" + "timestamp": "2025-11-20T19:43:21.917886018" }, "-profile test_dataset_custom_mapping": { "content": [ null, [ - "aggregate_results", - "aggregate_results/all_counts_filtered.parquet", - "aggregate_results/all_genes_summary.csv", - "aggregate_results/top_stable_genes_summary.csv", - "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", - "clean_count_data", - "clean_count_data/cleaned_counts_filtered.parquet", - "compute_base_statistics", - "compute_base_statistics/stats_all_genes.csv", - "compute_base_statistics_for_microarray", - "compute_base_statistics_for_microarray/microarray.stats_all_genes.csv", - "compute_base_statistics_for_rnaseq", - "compute_base_statistics_for_rnaseq/rnaseq.stats_all_genes.csv", - "compute_stability_scores", - "compute_stability_scores/stats_with_scores.csv", - "dash_app", - "dash_app/app.py", - "dash_app/assets", - "dash_app/assets/style.css", - "dash_app/data", - "dash_app/data/all_counts.parquet", - "dash_app/data/all_genes_summary.csv", - "dash_app/data/whole_design.csv", - "dash_app/file_system_backend", - "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", - "dash_app/spec-file.txt", - "dash_app/src", - "dash_app/src/callbacks", - "dash_app/src/callbacks/common.py", - "dash_app/src/callbacks/genes.py", - "dash_app/src/callbacks/samples.py", - "dash_app/src/components", - "dash_app/src/components/graphs.py", - "dash_app/src/components/icons.py", - "dash_app/src/components/right_sidebar.py", - "dash_app/src/components/settings", - "dash_app/src/components/settings/genes.py", - "dash_app/src/components/settings/samples.py", - "dash_app/src/components/stores.py", - "dash_app/src/components/tables.py", - "dash_app/src/components/tooltips.py", - "dash_app/src/components/top.py", - "dash_app/src/utils", - "dash_app/src/utils/config.py", - "dash_app/src/utils/data_management.py", - "dash_app/src/utils/style.py", - "dash_app/versions.yml", - "dataset_statistics", - "dataset_statistics/microarray.normalised.dataset_stats.csv", - "dataset_statistics/rnaseq.raw.dataset_stats.csv", "errors", "geo", - "get_candidate_genes", - "get_candidate_genes/candidate_counts.parquet", "idmapping", + "idmapping/global_gene_id_mapping.csv", + "idmapping/global_gene_metadata.csv", + "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", - "merge_all_counts", - "merge_all_counts/all_counts.parquet", - "merge_microarray_counts", - "merge_microarray_counts/all_counts.parquet", - "merge_rnaseq_counts", - "merge_rnaseq_counts/all_counts.parquet", "merged_datasets", - "merged_datasets/whole_design.csv", "multiqc", "multiqc/multiqc_data", "multiqc/multiqc_data/llms-full.txt", @@ -1816,83 +1644,25 @@ "multiqc/multiqc_data/multiqc.parquet", "multiqc/multiqc_data/multiqc_citations.txt", "multiqc/multiqc_data/multiqc_data.json", - "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", - "multiqc/multiqc_data/multiqc_gene_statistics.txt", - "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", - "multiqc/multiqc_plots", - "multiqc/multiqc_plots/pdf", - "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", - "multiqc/multiqc_plots/pdf/gene_statistics.pdf", - "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", - "multiqc/multiqc_plots/png", - "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", - "multiqc/multiqc_plots/png/gene_statistics.png", - "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", - "multiqc/multiqc_plots/svg", - "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", - "multiqc/multiqc_plots/svg/gene_statistics.svg", - "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", - "normalised", - "normalised/rnaseq.raw", - "normalised/rnaseq.raw/normalisation_deseq2", - "normalised/rnaseq.raw/normalisation_deseq2/rnaseq.raw.cpm.csv", - "normfinder", - "normfinder/stability_values.normfinder.csv", "pipeline_info", "pipeline_info/software_mqc_versions.yml", - "quantile_normalised", - "quantile_normalised/microarray.normalised", - "quantile_normalised/microarray.normalised/microarray.normalised.quant_norm.parquet", - "quantile_normalised/rnaseq.raw", - "quantile_normalised/rnaseq.raw/rnaseq.raw.cpm.quant_norm.parquet", "warnings" ], [ - "all_genes_summary.csv:md5,5402ddb07c9b7b2ec39217d8170094bd", - "top_stable_genes_summary.csv:md5,5402ddb07c9b7b2ec39217d8170094bd", - "top_stable_genes_transposed_counts_filtered.csv:md5,869c72204f45b8212ce2fd2e79221a6e", - "stats_all_genes.csv:md5,08b6baca5dd63d9e57b9b1c080a901c6", - "microarray.stats_all_genes.csv:md5,69c6a3908fa708e8f3670a9e96321f7d", - "rnaseq.stats_all_genes.csv:md5,427f59e5c2b5920f83bab591268e1fcb", - "stats_with_scores.csv:md5,f38a32ae4520ec843b076502b2d80074", - "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", - "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_genes_summary.csv:md5,5402ddb07c9b7b2ec39217d8170094bd", - "whole_design.csv:md5,70d6c2673e619ca52d2774fb3e368382", - "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", - "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", - "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", - "genes.py:md5,290fc0b53cb7ad9fe1b1c18fd92a36c4", - "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", - "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", - "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", - "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", - "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", - "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", - "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", - "tables.py:md5,84d17ad7f43458412e550f74a24560e5", - "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", - "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.py:md5,d8c73fa4a5924aa9cfe56b32f04d158d", - "data_management.py:md5,bc045967dc700f83fcbb9e84fb333c65", - "style.py:md5,b9f1207f06464e43c05e6e3912f12731", - "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "microarray.normalised.dataset_stats.csv:md5,4993a895b6561b9a4f939906bae582b4", - "rnaseq.raw.dataset_stats.csv:md5,c407ed714414e689c760fe1b450d5f58", - "whole_gene_metadata.csv:md5,544307bd920abd662a14ba43772d94b7", - "whole_design.csv:md5,70d6c2673e619ca52d2774fb3e368382", - "rnaseq.raw.cpm.csv:md5,59f604e6266fbc46948287f384c3639b", - "stability_values.normfinder.csv:md5,4e37152d1f0a8a4376b86cbf64b9f73c" + "global_gene_id_mapping.csv:md5,f5b78790b2968b7eace7cde8892514a0", + "global_gene_metadata.csv:md5,34366d3e2ac26d7d2240617b381dd222", + "whole_gene_id_mapping.csv:md5,aaedcff272e322e01272d3ac5ed2e1e4", + "whole_gene_metadata.csv:md5,a19dab830757047f6283da475a22b511" ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-19T18:05:03.755948892" + "timestamp": "2025-11-20T21:05:57.16509583" } } \ No newline at end of file diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 5e8b4c38..29f7d0cc 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -88,17 +88,15 @@ workflow STABLEEXPRESSION { // IDMAPPING // ----------------------------------------------------------------- - ch_gene_id_mapping = params.gene_id_mapping_file ? Channel.fromPath( params.gene_id_mapping_file, checkIfExists: true ) : Channel.value( [] ) - ch_gene_metadata = params.gene_metadata ? Channel.fromPath( params.gene_metadata, checkIfExists: true ) : Channel.value( [] ) - // tries to map gene IDs to Ensembl IDs whenever possible ID_MAPPING( ch_counts, species, params.skip_id_mapping, params.gprofiler_target_db, - ch_gene_id_mapping, - ch_gene_metadata + params.gene_id_mapping ? Channel.fromPath( params.gene_id_mapping, checkIfExists: true ) : Channel.value( [] ), + params.gene_metadata ? Channel.fromPath( params.gene_metadata, checkIfExists: true ) : Channel.value( [] ), + params.outdir ) ID_MAPPING.out.counts.set { ch_counts } ID_MAPPING.out.mapping.set { ch_gene_id_mapping } From 2fcdb5aef6930e7a48940ed75633b3d3b3f36021 Mon Sep 17 00:00:00 2001 From: Olivier Date: Fri, 21 Nov 2025 18:06:12 +0100 Subject: [PATCH 183/258] remove E-GTEX-* Expression Atlas accesssions by default --- subworkflows/local/expressionatlas_fetchdata/main.nf | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/subworkflows/local/expressionatlas_fetchdata/main.nf b/subworkflows/local/expressionatlas_fetchdata/main.nf index 1904858d..49c93968 100644 --- a/subworkflows/local/expressionatlas_fetchdata/main.nf +++ b/subworkflows/local/expressionatlas_fetchdata/main.nf @@ -42,8 +42,11 @@ workflow EXPRESSIONATLAS_FETCHDATA { platform ) + // removing E-GTEX-* accessions by default because they are too big + // however, contrary to E-PROT- accessions, they can be added by the user EXPRESSIONATLAS_GETACCESSIONS.out.accessions .splitText() + .filter { acc -> !acc.startsWith('E-GTEX-') } .set { ch_fetched_accessions } } @@ -61,7 +64,7 @@ workflow EXPRESSIONATLAS_FETCHDATA { // appending to accessions provided by the user // ensures that no accessions is present twice (provided by the user and fetched from E. Atlas) - // removing E-PROT- accessions + // removing E-PROT- accessions because they are not supported in subsequent steps // removing excluded accessions ch_input_accessions .mix( ch_fetched_accessions ) From d0d97f7c49ea4d87bf276bdc9736f38444883b79 Mon Sep 17 00:00:00 2001 From: Olivier Date: Fri, 21 Nov 2025 18:17:42 +0100 Subject: [PATCH 184/258] fix issues with collection of gene ids --- bin/gprofiler_map_ids.py | 9 ++-- bin/rename_gene_ids.py | 29 ++-------- modules/local/collect_gene_ids/main.nf | 1 + modules/local/rename_gene_ids/main.nf | 5 +- subworkflows/local/idmapping/main.nf | 74 ++++++++++++++++---------- workflows/stableexpression.nf | 4 +- 6 files changed, 60 insertions(+), 62 deletions(-) diff --git a/bin/gprofiler_map_ids.py b/bin/gprofiler_map_ids.py index bdf435b1..c8a78455 100755 --- a/bin/gprofiler_map_ids.py +++ b/bin/gprofiler_map_ids.py @@ -78,11 +78,10 @@ def main(): ) if not mapping_dict: - msg = "NO MAPPINGS FOUND" - logger.warning(msg) - with open(FAILURE_REASON_FILE, "w") as f: - f.write(msg) - sys.exit(0) + raise ValueError( + f"No mapping found for gene IDs such as {' '.join(gene_ids[:5])} on species {args.species} " + + f"and g:Profiler target database {args.gprofiler_target_db}" + ) ############################################################# # WRITING MAPPING diff --git a/bin/rename_gene_ids.py b/bin/rename_gene_ids.py index 3921a9cc..43fc83c4 100755 --- a/bin/rename_gene_ids.py +++ b/bin/rename_gene_ids.py @@ -38,15 +38,10 @@ def parse_args(): parser.add_argument( "--mappings", type=Path, + required=True, dest="mapping_file", help="Mapping file containing gene IDs", ) - parser.add_argument( - "--custom-mappings", - type=Path, - dest="custom_mapping_file", - help="Optional file containing custom mappings", - ) return parser.parse_args() @@ -88,24 +83,10 @@ def main(): # GETTING MAPPINGS ############################################################# - mapping_dict = {} - if args.mapping_file is not None: - mapping_df = parse_table(args.mapping_file) - mapping_dict = mapping_df.set_index(config.ORIGINAL_GENE_ID_COLNAME)[ - config.GENE_ID_COLNAME - ].to_dict() - - custom_mapping_dict = {} - if args.custom_mapping_file is not None: - custom_mapping_df = parse_table(args.custom_mapping_file) - custom_mapping_dict = custom_mapping_df.set_index( - config.ORIGINAL_GENE_ID_COLNAME - )[config.GENE_ID_COLNAME].to_dict() - - mapping_dict |= custom_mapping_dict - - if not mapping_dict: - raise ValueError("No mapping found") # should not happen + mapping_df = parse_table(args.mapping_file) + mapping_dict = mapping_df.set_index(config.ORIGINAL_GENE_ID_COLNAME)[ + config.GENE_ID_COLNAME + ].to_dict() ############################################################# # MAPPING GENE IDS IN DATAFRAME diff --git a/modules/local/collect_gene_ids/main.nf b/modules/local/collect_gene_ids/main.nf index 36170583..a7cfe5ca 100644 --- a/modules/local/collect_gene_ids/main.nf +++ b/modules/local/collect_gene_ids/main.nf @@ -1,5 +1,6 @@ process COLLECT_GENE_IDS { + tag "chunk ${task.index}" label "process_high" conda "${moduleDir}/spec-file.txt" diff --git a/modules/local/rename_gene_ids/main.nf b/modules/local/rename_gene_ids/main.nf index f22af9a6..b94a9179 100644 --- a/modules/local/rename_gene_ids/main.nf +++ b/modules/local/rename_gene_ids/main.nf @@ -12,7 +12,6 @@ process RENAME_GENE_IDS { input: tuple val(meta), path(count_file) path gene_id_mapping_file - path custom_gene_id_mapping_file output: tuple val(meta), path('*.renamed.csv'), optional: true, emit: counts @@ -24,12 +23,10 @@ process RENAME_GENE_IDS { script: def mapping_arg = gene_id_mapping_file ? "--mappings $gene_id_mapping_file" : "" - def custom_mapping_arg = custom_gene_id_mapping_file ? "--custom-mappings $custom_gene_id_mapping_file" : "" """ rename_gene_ids.py \\ --count-file "$count_file" \\ - $mapping_arg \\ - $custom_mapping_arg + $mapping_arg """ diff --git a/subworkflows/local/idmapping/main.nf b/subworkflows/local/idmapping/main.nf index da62a6b4..db0c8bd1 100644 --- a/subworkflows/local/idmapping/main.nf +++ b/subworkflows/local/idmapping/main.nf @@ -15,8 +15,8 @@ workflow ID_MAPPING { species skip_id_mapping gprofiler_target_db - ch_custom_gene_id_mapping - ch_custom_gene_metadata + custom_gene_id_mapping + custom_gene_metadata outdir @@ -25,22 +25,41 @@ workflow ID_MAPPING { ch_gene_id_mapping = Channel.empty() ch_gene_metadata = Channel.empty() - if ( !params.skip_id_mapping ) { + if ( !skip_id_mapping ) { // ----------------------------------------------------------------- - // COLLECTING ALL GENE IDS FROm ALL DATASETS + // COLLECTING ALL GENE IDS FROM ALL DATASETS // ----------------------------------------------------------------- - COLLECT_GENE_IDS( - ch_counts.map{ meta, file -> file }.collect() - ) + // here we cannot use directly COLLECT_GENE_IDS for runs comprising a huge number of files (eg. human) + // so that we proceed by chunks, and perform a final merging step using the Java VM + + // TRICK: + // the buffer operator creates non-deterministic chunks + // which prevents resuming the pipeline + // so we sort the list of files before buffering them + ch_chunck_counts = ch_counts + .map{ meta, file -> file } + .collect( sort: true ) // get all files and sort them + .flatten() // needed to convert the list back to individual channel items + .buffer( size: 100, remainder: true ) + + COLLECT_GENE_IDS( ch_chunck_counts ) + + ch_gene_ids = COLLECT_GENE_IDS.out.gene_ids + .splitText() + .unique() + .collectFile( + name: 'original_gene_ids.txt', + storeDir: "${outdir}/idmapping/" + ) // ----------------------------------------------------------------- // MAPPING THESE GENE IDS TO THE CHOSEN TARGET DB // ----------------------------------------------------------------- GPROFILER_IDMAPPING( - COLLECT_GENE_IDS.out.gene_ids, + ch_gene_ids, species, gprofiler_target_db ) @@ -48,54 +67,55 @@ workflow ID_MAPPING { GPROFILER_IDMAPPING.out.metadata.set { ch_gene_metadata } } - // ----------------------------------------------------------------- - // RENAMING GENE IDS IN ALL COUNT DATASETS - // ----------------------------------------------------------------- - - RENAME_GENE_IDS( - ch_counts, - ch_gene_id_mapping, - ch_custom_gene_id_mapping - ) - // ----------------------------------------------------------------- // COLLECTING GLOBAL GENE ID MAPPING AND METADATA // ----------------------------------------------------------------- ch_gene_id_mapping - .mix( ch_custom_gene_id_mapping ) - .filter { it != [] } // handle no custom mappings + .mix( custom_gene_id_mapping ? Channel.fromPath( custom_gene_id_mapping, checkIfExists: true ) : Channel.empty() ) .splitCsv( header: true ) .unique() .collectFile( name: 'global_gene_id_mapping.csv', seed: "original_gene_id,gene_id", newLine: true, - storeDir: "${params.outdir}/idmapping/" + storeDir: "${outdir}/idmapping/" ) { item -> "${item["original_gene_id"]},${item["gene_id"]}" } - .ifEmpty([]) .set { ch_global_gene_id_mapping } ch_gene_metadata - .mix( ch_custom_gene_metadata ) - .filter { it != [] } // handle no custom metadata + .mix( custom_gene_metadata ? Channel.fromPath( custom_gene_metadata, checkIfExists: true ) : Channel.empty() ) .splitCsv( header: true ) .unique() .collectFile( name: 'global_gene_metadata.csv', seed: "gene_id,name,description", newLine: true, - storeDir: "${params.outdir}/idmapping/" + storeDir: "${outdir}/idmapping/" ) { item -> "${item["gene_id"]},${item["name"]},${item["description"]}" } - .ifEmpty([]) .set { ch_global_gene_metadata } + // ----------------------------------------------------------------- + // RENAMING GENE IDS IN ALL COUNT DATASETS (ONLY IF NECESSARY) + // ----------------------------------------------------------------- + + if ( !skip_id_mapping || custom_gene_id_mapping ) { + + RENAME_GENE_IDS( + ch_counts, + ch_global_gene_id_mapping.first() + ) + ch_counts = RENAME_GENE_IDS.out.counts + + } + + emit: - counts = RENAME_GENE_IDS.out.counts + counts = ch_counts mapping = ch_global_gene_id_mapping metadata = ch_global_gene_metadata diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 29f7d0cc..2b559d37 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -94,8 +94,8 @@ workflow STABLEEXPRESSION { species, params.skip_id_mapping, params.gprofiler_target_db, - params.gene_id_mapping ? Channel.fromPath( params.gene_id_mapping, checkIfExists: true ) : Channel.value( [] ), - params.gene_metadata ? Channel.fromPath( params.gene_metadata, checkIfExists: true ) : Channel.value( [] ), + params.gene_id_mapping, + params.gene_metadata, params.outdir ) ID_MAPPING.out.counts.set { ch_counts } From 919b8e15fb507aac54ba697b8da72cc62d9052ec Mon Sep 17 00:00:00 2001 From: Olivier Date: Sat, 22 Nov 2025 10:10:16 +0100 Subject: [PATCH 185/258] fix bug in collect_gene_ids.py when ids are integer --- bin/collect_gene_ids.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/collect_gene_ids.py b/bin/collect_gene_ids.py index 20588fbc..28aa7d66 100755 --- a/bin/collect_gene_ids.py +++ b/bin/collect_gene_ids.py @@ -55,7 +55,7 @@ def main(): all_gene_ids.update(list(df.index)) with open(ALL_GENE_IDS_OUTFILE, "w") as f: - f.write("\n".join(list(all_gene_ids))) + f.write("\n".join([str(gene_id) for gene_id in all_gene_ids])) if __name__ == "__main__": From a94ffcdae1a12b4c0400887e17ca3325005d8c58 Mon Sep 17 00:00:00 2001 From: Olivier Date: Sat, 22 Nov 2025 10:44:40 +0100 Subject: [PATCH 186/258] remove deseq2 and edger from pipeline; add new steps for computation of cpm and tpm --- bin/compute_cpm.py | 94 ++++++++++++++++ bin/{normalise_to_tpm.py => compute_tpm.py} | 68 +++++++----- bin/download_latest_annotation.py | 102 ++++++++++++++++++ bin/{ => old}/normalise_with_deseq2.R | 0 bin/{ => old}/normalise_with_edger.R | 0 .../local/normalisation/compute_cpm/main.nf | 27 +++++ .../normalisation/compute_cpm/spec-file.txt | 42 ++++++++ .../local/normalisation/compute_tpm/main.nf | 27 +++++ .../normalisation/compute_tpm/spec-file.txt | 42 ++++++++ .../local/{ => old}/clean_count_data/main.nf | 0 .../{ => old}/clean_count_data/spec-file.txt | 0 .../{normalisation => old}/deseq2/main.nf | 0 .../deseq2/spec-file.txt | 0 .../{normalisation => old}/edger/main.nf | 0 .../edger/spec-file.txt | 0 modules/local/rename_gene_ids/main.nf | 5 +- modules/local/rename_gene_ids/spec-file.txt | 71 +++++------- nextflow.config | 2 +- nextflow_schema.json | 6 +- .../local/expression_normalisation/main.nf | 18 ++-- 20 files changed, 417 insertions(+), 87 deletions(-) create mode 100755 bin/compute_cpm.py rename bin/{normalise_to_tpm.py => compute_tpm.py} (73%) create mode 100755 bin/download_latest_annotation.py rename bin/{ => old}/normalise_with_deseq2.R (100%) rename bin/{ => old}/normalise_with_edger.R (100%) create mode 100644 modules/local/normalisation/compute_cpm/main.nf create mode 100644 modules/local/normalisation/compute_cpm/spec-file.txt create mode 100644 modules/local/normalisation/compute_tpm/main.nf create mode 100644 modules/local/normalisation/compute_tpm/spec-file.txt rename modules/local/{ => old}/clean_count_data/main.nf (100%) rename modules/local/{ => old}/clean_count_data/spec-file.txt (100%) rename modules/local/{normalisation => old}/deseq2/main.nf (100%) rename modules/local/{normalisation => old}/deseq2/spec-file.txt (100%) rename modules/local/{normalisation => old}/edger/main.nf (100%) rename modules/local/{normalisation => old}/edger/spec-file.txt (100%) diff --git a/bin/compute_cpm.py b/bin/compute_cpm.py new file mode 100755 index 00000000..635da0b2 --- /dev/null +++ b/bin/compute_cpm.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 + +# Written by Olivier Coen. Released under the MIT license. + +import argparse +import logging +from pathlib import Path + +import config +import pandas as pd + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +CPM_NORM_SUFFIX = ".cpm.csv" + + +##################################################### +##################################################### +# FUNCTIONS +##################################################### +##################################################### + + +def parse_args(): + parser = argparse.ArgumentParser(description="Normalise data to CPM") + parser.add_argument( + "--counts", type=Path, dest="count_file", required=True, help="Count file" + ) + return parser.parse_args() + + +def parse_counts(file: Path): + if file.suffix == ".csv": + return pd.read_csv(file, header=0, index_col=0) + else: # .tsv + return pd.read_csv(file, header=0, sep="\t", index_col=0) + + +def calculate_cpm(counts_df: pd.DataFrame): + """ + Calculate CPM (Counts Per Million) from raw count data. + + Parameters: + ----------- + counts_df : pandas.DataFrame + DataFrame with genes as rows and samples as columns + + Returns: + -------- + cpm_df : pandas.DataFrame + DataFrame with CPM values + """ + # Calculate total counts per sample (column sums) + total_counts = counts_df.sum(axis=0) + + # Calculate CPM: (count / total_counts) * 1,000,000 + cpm_df = (counts_df / total_counts) * 1e6 + + return cpm_df + + +def export_normalised_data(count_df: pd.DataFrame, count_file: Path): + """Export gene expression data to CSV.""" + # replace .csv / .tsv by .tpm.csv + outfilename = ".".join(count_file.name.split(".")[:-1]) + CPM_NORM_SUFFIX + logger.info(f"Exporting CPM normalised counts to: {outfilename}") + count_df.to_csv(outfilename, index=True, header=True) + + +##################################################### +##################################################### +# MAIN +##################################################### +##################################################### + + +def main(): + args = parse_args() + + logger.info("Parsing data") + count_df = parse_counts(args.count_file) + count_df.index.name = config.GENE_ID_COLNAME + + logger.info(f"Normalising {args.count_file.name}") + + count_df = calculate_cpm(count_df) + + export_normalised_data(count_df, args.count_file) + + +if __name__ == "__main__": + main() diff --git a/bin/normalise_to_tpm.py b/bin/compute_tpm.py similarity index 73% rename from bin/normalise_to_tpm.py rename to bin/compute_tpm.py index 0f0e4302..02f55f19 100755 --- a/bin/normalise_to_tpm.py +++ b/bin/compute_tpm.py @@ -13,7 +13,7 @@ logger = logging.getLogger(__name__) -TPM_NORM_SUFFIX = ".tpm.parquet" +TPM_NORM_SUFFIX = ".tpm.csv" ##################################################### @@ -29,15 +29,41 @@ def parse_args(): "--counts", type=Path, dest="count_file", required=True, help="Count file" ) parser.add_argument( - "--lengths", + "--annotation", type=Path, - dest="lengths_file", + dest="annotation_file", required=True, - help="File containing gene lengths", + help="Gene annotation file (GFF format)", ) return parser.parse_args() +def parse_counts(file: Path): + if file.suffix == ".csv": + return pd.read_csv(file, header=0, index_col=0) + else: # .tsv + return pd.read_csv(file, header=0, sep="\t", index_col=0) + + +def parse_annotation(file: Path) -> pd.DataFrame: + pass + + +def get_cdna_lengths(annotation_df: pd.DataFrame) -> pd.DataFrame: + pass + + +def reorder_gene_lengths( + count_df: pd.DataFrame, cdna_length_df: pd.DataFrame +) -> tuple[pd.DataFrame, pd.Series]: + # merge with gene length and extracts it afterwards + # so that the genes are in the same order in df and in cdna_length_series + gene_id_df = pd.DataFrame({"gene_id": count_df.index}) + gene_id_df = pd.merge(gene_id_df, cdna_length_df, how="left", on="gene_id") + cdna_length_series = gene_id_df[config.CDNA_LENGTH_COLNAME].astype(float) + return cdna_length_series + + def is_raw_counts(df: pd.DataFrame): """Check if the data are raw counts (integers).""" return all(df.dtypes.apply(lambda x: pd.api.types.is_integer_dtype(x))) @@ -75,34 +101,12 @@ def compute_tpm(df: pd.DataFrame, cdna_length_series: pd.Series): raise ValueError("Could not determine data type.") -def parse_data( - count_file: Path, gene_length_file: Path -) -> tuple[pd.DataFrame, pd.Series]: - count_df = pd.read_csv(count_file, index_col=0) - count_df.index.name = config.GENE_ID_COLNAME - - cdna_length_df = pd.read_csv( - gene_length_file, - names=[config.GENE_ID_COLNAME, config.CDNA_LENGTH_COLNAME], - ) - # merge with gene length and extracts it afterwards - # so that the genes are in the same order in df and in cdna_length_series - count_df = pd.merge( - count_df, cdna_length_df, how="left", left_index=True, right_on="gene_id" - ) - cdna_length_series = count_df[config.CDNA_LENGTH_COLNAME].astype(float) - count_df = count_df.drop( - columns=[config.CDNA_LENGTH_COLNAME] - ) # keep only expression values - return count_df, cdna_length_series - - def export_normalised_data(count_df: pd.DataFrame, count_file: Path): """Export gene expression data to Parquet.""" # replace .csv / .tsv by .tpm.csv outfilename = ".".join(count_file.name.split(".")[:-1]) + TPM_NORM_SUFFIX logger.info(f"Exporting TPM normalised counts to: {outfilename}") - count_df.reset_index().to_parquet(outfilename) + count_df.to_csv(outfilename, index=True, header=True) ##################################################### @@ -116,7 +120,15 @@ def main(): args = parse_args() logger.info("Parsing data") - count_df, cdna_length_series = parse_data(args.count_file, args.gene_length_file) + count_df = parse_counts(args.count_file) + count_df.index.name = config.GENE_ID_COLNAME + + annotation_df = parse_annotation(args.annotation_file) + + cdna_length_df + + logger.info("Reordering gene lengths") + cdna_length_series = reorder_gene_lengths(count_df, cdna_length_df) logger.info(f"Normalising {args.count_file.name}") diff --git a/bin/download_latest_annotation.py b/bin/download_latest_annotation.py new file mode 100755 index 00000000..8c3166f3 --- /dev/null +++ b/bin/download_latest_annotation.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 + +# Written by Olivier Coen. Released under the MIT license. + +import argparse +import json +import logging +from pathlib import Path + +import config +import pandas as pd +import requests +from tenacity import ( + before_sleep_log, + retry, + stop_after_delay, + wait_exponential, +) + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +GENE_IDS_CHUNKSIZE = 50 # max allowed by Ensembl REST API + +ENSEMBL_REST_SERVER = "https://rest.ensembl.org/" +SPECIES_INFO_EXT = "info/genomes/taxonomy/{species}" +HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json", +} +STOP_RETRY_AFTER_DELAY = 600 + + +################################################################## +################################################################## +# FUNCTIONS +################################################################## +################################################################## + + +def parse_args(): + parser = argparse.ArgumentParser("Get GEO Datasets accessions") + parser.add_argument( + "--species", + type=str, + dest="species", + required=True, + help="Species name", + ) + return parser.parse_args() + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# QUERIES TO ENSEMBL +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +@retry( + stop=stop_after_delay(STOP_RETRY_AFTER_DELAY), + wait=wait_exponential(multiplier=1, min=1, max=30), + before_sleep=before_sleep_log(logger, logging.WARNING), +) +def send_get_request_to_ensembl(url: str) -> list[dict]: + logger.info(f"Sending GET request to {url}") + response = requests.get(url, headers=HEADERS) + if response.status_code == 200: + response.raise_for_status() + else: + raise RuntimeError( + f"Failed to retrieve data: encountered error {response.status_code}" + ) + return response.json() + + +def get_species_division(species: str) -> str: + url = ENSEMBL_REST_SERVER + SPECIES_INFO_EXT.format(species=species) + data = send_get_request_to_ensembl(url) + if len(data) == 0: + raise ValueError(f"No division found for {species}") + elif len(data) > 1: + logger.warning( + f"Multiple divisions found for {species}. Keeping the first one." + ) + return data[0]["division"] + + +################################################################## +################################################################## +# MAIN +################################################################## +################################################################## + + +def main(): + args = parse_args() + + species_division = get_species_division(args.species) + print(species_division) + + +if __name__ == "__main__": + main() diff --git a/bin/normalise_with_deseq2.R b/bin/old/normalise_with_deseq2.R similarity index 100% rename from bin/normalise_with_deseq2.R rename to bin/old/normalise_with_deseq2.R diff --git a/bin/normalise_with_edger.R b/bin/old/normalise_with_edger.R similarity index 100% rename from bin/normalise_with_edger.R rename to bin/old/normalise_with_edger.R diff --git a/modules/local/normalisation/compute_cpm/main.nf b/modules/local/normalisation/compute_cpm/main.nf new file mode 100644 index 00000000..34a9fc54 --- /dev/null +++ b/modules/local/normalisation/compute_cpm/main.nf @@ -0,0 +1,27 @@ +process NORMALISATION_COMPUTE_CPM { + + label 'process_single' + + tag "${meta.dataset}" + + conda "${moduleDir}/spec-file.txt" + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/3d/3d7126100b0eb7cb53dfb50291707ea8dda3b9738b76551ab73605d0acbe114b/data': + 'community.wave.seqera.io/library/pandas:2.3.3--5a902bf824a79745' }" + + input: + tuple val(meta), path(count_file) + + output: + tuple val(meta), path('*.cpm.csv'), optional: true, emit: counts + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions + tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions + + script: + """ + compute_cpm.py \\ + --counts $count_file + """ + + +} diff --git a/modules/local/normalisation/compute_cpm/spec-file.txt b/modules/local/normalisation/compute_cpm/spec-file.txt new file mode 100644 index 00000000..f79332cf --- /dev/null +++ b/modules/local/normalisation/compute_cpm/spec-file.txt @@ -0,0 +1,42 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda#f7b4d76975aac7e5d9e6ad13845f92fe +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda#c0374badb3a5d4b1372db28d19462c53 +https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda#f0991f0f84902f6b6009b4d2350a83aa +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda#280ea6eee9e2ddefde25ff799c4f0363 +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda#5b767048b1b3ee9a954b06f4084f93dc +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda#f627678cf829bd70bccf141a19c3ad3e +https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-bootstrap_ha15bf96_3.conda#3036ca5b895b7f5146c5a25486234a68 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-hcd61629_7.conda#f116940d825ffc9104400f0d7f1a4551 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_7.conda#8621a450add4e231f676646880703f49 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda#be43915efc66345cccb3c310b6ed0374 +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-1_h4a7cf45_openblas.conda#8b39e1ae950f1b54a3959c58ca2c32b8 +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-1_h0358290_openblas.conda#a670bff9eb7963ea41b4e09a4e4ab608 +https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda#8b09ae86839581147ef2e5c5e229d164 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda#35f29eec58405aaf55e01cb470d8c26a +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-1_h47877c9_openblas.conda#dee12a83aa4aca5077ea23c0605de044 +https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc +https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.0-hee844dc_0.conda#729a572a3ebb8c43933b30edcc628ceb +https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda#80c07c68d2f6870250959dcc95b209d1 +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda#9ee58d5c534af06558933af3c845a780 +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 +https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 +https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda#86bc20552bf46075e3d92b67f089172d +https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a +https://conda.anaconda.org/conda-forge/linux-64/python-3.13.9-hc97d973_101_cp313.conda#4780fe896e961722d0623fa91d0d3378 +https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.5-py313hf6604e3_0.conda#15f43bcd12c90186e78801fafc53d89b +https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda#3339e3b65d58accf4ca4fb8748ab16b3 +https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 +https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 +https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 +https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.3-py313h08cd8bf_1.conda#9e87d4bda0c2711161d765332fa38781 +https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda#bf47878473e5ab9fdb4115735230e191 diff --git a/modules/local/normalisation/compute_tpm/main.nf b/modules/local/normalisation/compute_tpm/main.nf new file mode 100644 index 00000000..78d86ec3 --- /dev/null +++ b/modules/local/normalisation/compute_tpm/main.nf @@ -0,0 +1,27 @@ +process NORMALISATION_COMPUTE_TPM { + + label 'process_single' + + tag "${meta.dataset}" + + conda "${moduleDir}/spec-file.txt" + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/3d/3d7126100b0eb7cb53dfb50291707ea8dda3b9738b76551ab73605d0acbe114b/data': + 'community.wave.seqera.io/library/pandas:2.3.3--5a902bf824a79745' }" + + input: + tuple val(meta), path(count_file) + + output: + tuple val(meta), path('*.tpm.csv'), optional: true, emit: counts + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions + tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions + + script: + """ + compute_cpm.py \\ + --counts $count_file + """ + + +} diff --git a/modules/local/normalisation/compute_tpm/spec-file.txt b/modules/local/normalisation/compute_tpm/spec-file.txt new file mode 100644 index 00000000..f79332cf --- /dev/null +++ b/modules/local/normalisation/compute_tpm/spec-file.txt @@ -0,0 +1,42 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda#f7b4d76975aac7e5d9e6ad13845f92fe +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda#c0374badb3a5d4b1372db28d19462c53 +https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda#f0991f0f84902f6b6009b4d2350a83aa +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda#280ea6eee9e2ddefde25ff799c4f0363 +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda#5b767048b1b3ee9a954b06f4084f93dc +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda#f627678cf829bd70bccf141a19c3ad3e +https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-bootstrap_ha15bf96_3.conda#3036ca5b895b7f5146c5a25486234a68 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-hcd61629_7.conda#f116940d825ffc9104400f0d7f1a4551 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_7.conda#8621a450add4e231f676646880703f49 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda#be43915efc66345cccb3c310b6ed0374 +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-1_h4a7cf45_openblas.conda#8b39e1ae950f1b54a3959c58ca2c32b8 +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-1_h0358290_openblas.conda#a670bff9eb7963ea41b4e09a4e4ab608 +https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda#8b09ae86839581147ef2e5c5e229d164 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda#35f29eec58405aaf55e01cb470d8c26a +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-1_h47877c9_openblas.conda#dee12a83aa4aca5077ea23c0605de044 +https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc +https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.0-hee844dc_0.conda#729a572a3ebb8c43933b30edcc628ceb +https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda#80c07c68d2f6870250959dcc95b209d1 +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda#9ee58d5c534af06558933af3c845a780 +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 +https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 +https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda#86bc20552bf46075e3d92b67f089172d +https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a +https://conda.anaconda.org/conda-forge/linux-64/python-3.13.9-hc97d973_101_cp313.conda#4780fe896e961722d0623fa91d0d3378 +https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.5-py313hf6604e3_0.conda#15f43bcd12c90186e78801fafc53d89b +https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda#3339e3b65d58accf4ca4fb8748ab16b3 +https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 +https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 +https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 +https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.3-py313h08cd8bf_1.conda#9e87d4bda0c2711161d765332fa38781 +https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda#bf47878473e5ab9fdb4115735230e191 diff --git a/modules/local/clean_count_data/main.nf b/modules/local/old/clean_count_data/main.nf similarity index 100% rename from modules/local/clean_count_data/main.nf rename to modules/local/old/clean_count_data/main.nf diff --git a/modules/local/clean_count_data/spec-file.txt b/modules/local/old/clean_count_data/spec-file.txt similarity index 100% rename from modules/local/clean_count_data/spec-file.txt rename to modules/local/old/clean_count_data/spec-file.txt diff --git a/modules/local/normalisation/deseq2/main.nf b/modules/local/old/deseq2/main.nf similarity index 100% rename from modules/local/normalisation/deseq2/main.nf rename to modules/local/old/deseq2/main.nf diff --git a/modules/local/normalisation/deseq2/spec-file.txt b/modules/local/old/deseq2/spec-file.txt similarity index 100% rename from modules/local/normalisation/deseq2/spec-file.txt rename to modules/local/old/deseq2/spec-file.txt diff --git a/modules/local/normalisation/edger/main.nf b/modules/local/old/edger/main.nf similarity index 100% rename from modules/local/normalisation/edger/main.nf rename to modules/local/old/edger/main.nf diff --git a/modules/local/normalisation/edger/spec-file.txt b/modules/local/old/edger/spec-file.txt similarity index 100% rename from modules/local/normalisation/edger/spec-file.txt rename to modules/local/old/edger/spec-file.txt diff --git a/modules/local/rename_gene_ids/main.nf b/modules/local/rename_gene_ids/main.nf index b94a9179..6fa7777b 100644 --- a/modules/local/rename_gene_ids/main.nf +++ b/modules/local/rename_gene_ids/main.nf @@ -6,8 +6,8 @@ process RENAME_GENE_IDS { conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? - 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/5c/5c28c8e613c062828aaee4b950029bc90a1a1aa94d5f61016a588c8ec7be8b65/data': - 'community.wave.seqera.io/library/pandas_requests_tenacity:5ba56df089a9d718' }" + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/3d/3d7126100b0eb7cb53dfb50291707ea8dda3b9738b76551ab73605d0acbe114b/data': + 'community.wave.seqera.io/library/pandas:2.3.3--5a902bf824a79745' }" input: tuple val(meta), path(count_file) @@ -19,7 +19,6 @@ process RENAME_GENE_IDS { tuple val(meta.dataset), path("warning_reason.txt"), optional: true, topic: renaming_warning_reason tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions - tuple val("${task.process}"), val('requests'), eval('python3 -c "import requests; print(requests.__version__)"'), topic: versions script: def mapping_arg = gene_id_mapping_file ? "--mappings $gene_id_mapping_file" : "" diff --git a/modules/local/rename_gene_ids/spec-file.txt b/modules/local/rename_gene_ids/spec-file.txt index 3233c10b..f79332cf 100644 --- a/modules/local/rename_gene_ids/spec-file.txt +++ b/modules/local/rename_gene_ids/spec-file.txt @@ -3,55 +3,40 @@ # platform: linux-64 @EXPLICIT https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda#3cd1a7238a0dd3d0860fdefc496cc854 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda#f7b4d76975aac7e5d9e6ad13845f92fe https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda#9e60c55e725c20d23125a5f0dd69af5d -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda#6d11a5edae89fe413c0569f16d308f5a -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda#e66f2b8ad787e7beb0f846e4bd7e8493 -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62ee74e96c5ebb0af99386de58cf9553 -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_0.conda#e31316a586cac398b1fcdb10ace786b9 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda#db0bfbe7dd197b68ad5f30333bae6ce0 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda#c0374badb3a5d4b1372db28d19462c53 +https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda#f0991f0f84902f6b6009b4d2350a83aa +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda#280ea6eee9e2ddefde25ff799c4f0363 +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda#5b767048b1b3ee9a954b06f4084f93dc +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda#f627678cf829bd70bccf141a19c3ad3e +https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-bootstrap_ha15bf96_3.conda#3036ca5b895b7f5146c5a25486234a68 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-hcd61629_7.conda#f116940d825ffc9104400f0d7f1a4551 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_7.conda#8621a450add4e231f676646880703f49 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda#be43915efc66345cccb3c310b6ed0374 +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-1_h4a7cf45_openblas.conda#8b39e1ae950f1b54a3959c58ca2c32b8 +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-1_h0358290_openblas.conda#a670bff9eb7963ea41b4e09a4e4ab608 +https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda#8b09ae86839581147ef2e5c5e229d164 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda#35f29eec58405aaf55e01cb470d8c26a +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-1_h47877c9_openblas.conda#dee12a83aa4aca5077ea23c0605de044 https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda#d864d34357c3b65a4b731f78c0801dc4 +https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.2-h6cd9bfd_0.conda#b04c7eda6d7dab1e6503135e7fad4d25 -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b -https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.0-hee844dc_0.conda#729a572a3ebb8c43933b30edcc628ceb +https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda#80c07c68d2f6870250959dcc95b209d1 https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.6.15-hbd8a1cb_0.conda#72525f07d72806e3b639ad4504c30ce5 -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda#c87df2ab1448ba69169652ab9547082d +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda#9ee58d5c534af06558933af3c845a780 +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 +https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda#86bc20552bf46075e3d92b67f089172d https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.12.8-h9e4cc4f_1_cpython.conda#7fd2fd79436d9b473812f14e86746844 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda#0dfcdc155cf23812a0c9deada86fb723 -https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h2ec8cdc_3.conda#a32e0c069f6c3dcac635f7b0b0dac67e -https://conda.anaconda.org/conda-forge/noarch/certifi-2025.6.15-pyhd8ed1ab_0.conda#781d068df0cc2407d4db0ecfbb29225b -https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda#12c566707c80111f9799308d9e265aef -https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py312h06ac9bb_0.conda#a861504bbea4161a9170b85d4d2be840 -https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.2-pyhd8ed1ab_0.conda#40fe4284b8b5835a9073a645139f35af -https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda#0a802cb9888dd14eeefc611f05c40b6e -https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda#8e6923fc12f1fe8f8c4e5c9f343256ac -https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda#b4754fb1bdcb70c8fd54f918301582c6 -https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda#39a4f67be3286c86d696df570b1201b7 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda#530566b68c3b8ce7eec4cd047eae19fe -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda#bfbca721fd33188ef923dfe9ba172f29 -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda#323dc8f259224d13078aaf7ce96c3efe -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-32_h59b9bed_openblas.conda#2af9f3d5c2e39f417ce040f5a35c40c6 -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-32_he106b2a_openblas.conda#3d3f9355e52f269cd8bc2c440d8a5263 -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-32_h7ac8fdf_openblas.conda#6c3f04ccb6c578138e9f9899da0bd714 -https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.1-py312h6cf2f7f_0.conda#7e086a30150af2536a1059885368dcf0 -https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda#a451d576819089b0d672f18768be0f65 +https://conda.anaconda.org/conda-forge/linux-64/python-3.13.9-hc97d973_101_cp313.conda#4780fe896e961722d0623fa91d0d3378 +https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.5-py313hf6604e3_0.conda#15f43bcd12c90186e78801fafc53d89b +https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda#3339e3b65d58accf4ca4fb8748ab16b3 https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 -https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.3-py312hf9745cd_3.conda#2979458c23c7755683a0598fb33e7666 -https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e -https://conda.anaconda.org/conda-forge/noarch/wheel-0.45.1-pyhd8ed1ab_1.conda#75cb7132eb58d97896e173ef12ac9986 -https://conda.anaconda.org/conda-forge/noarch/pip-25.1.1-pyh8b19718_0.conda#32d0781ace05105cc99af55d36cbec7c -https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda#461219d1a5bd61342293efa2c0c90eac -https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py312h66e93f0_2.conda#630db208bc7bbb96725ce9832c7423bb -https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda#436c165519e140cb08d246a4472a9d6a -https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda#a9b9368f3701a417eac9edbcae7cb737 -https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.2-pyhd8ed1ab_0.conda#5d99943f2ae3cc69e1ada12ce9d4d701 +https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.3-py313h08cd8bf_1.conda#9e87d4bda0c2711161d765332fa38781 +https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda#bf47878473e5ab9fdb4115735230e191 diff --git a/nextflow.config b/nextflow.config index 48537e4d..97a30299 100644 --- a/nextflow.config +++ b/nextflow.config @@ -43,7 +43,7 @@ params { skip_id_mapping = false // statistics - normalisation_method = 'deseq2' + normalisation_method = 'cpm' quantile_norm_target_distrib = 'uniform' nb_top_gene_candidates = 5000 ks_pvalue_threshold = -1 diff --git a/nextflow_schema.json b/nextflow_schema.json index e9c6d43e..c3c0c4b4 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -225,9 +225,9 @@ "type": "string", "description": "Count normalisation method", "fa_icon": "fas fa-divide", - "enum": ["deseq2", "edger"], - "default": "deseq2", - "help_text": "Raw RNAseq data must be normalised before further processing. You can select the R package used for normalisation." + "enum": ["tpm", "cpm"], + "default": "cpm", + "help_text": "Raw RNAseq data must be normalised before further processing. `tmp offers a more accurate representation of gene expression levels as it is unbiased toward gene length. However, you can choose `cpm` if you do not have access to a genome annotation." }, "quantile_norm_target_distrib": { "type": "string", diff --git a/subworkflows/local/expression_normalisation/main.nf b/subworkflows/local/expression_normalisation/main.nf index a74955c8..257a8317 100644 --- a/subworkflows/local/expression_normalisation/main.nf +++ b/subworkflows/local/expression_normalisation/main.nf @@ -1,6 +1,6 @@ -include { NORMALISATION_DESEQ2 } from '../../../modules/local/normalisation/deseq2' -include { NORMALISATION_EDGER } from '../../../modules/local/normalisation/edger' -include { QUANTILE_NORMALISATION } from '../../../modules/local/quantile_normalisation' +include { NORMALISATION_COMPUTE_CPM as COMPUTE_CPM } from '../../../modules/local/normalisation/compute_cpm' +include { NORMALISATION_COMPUTE_CPM as COMPUTE_TPM } from '../../../modules/local/normalisation/compute_tpm' +include { QUANTILE_NORMALISATION } from '../../../modules/local/quantile_normalisation' /* ======================================================================================== @@ -33,13 +33,13 @@ workflow EXPRESSION_NORMALISATION { .map { meta, file -> [ meta, file, meta.design ] } .set { ch_raw_rnaseq_datasets_to_normalise } - if ( normalisation_method == 'deseq2' ) { - NORMALISATION_DESEQ2( ch_raw_rnaseq_datasets_to_normalise ) - ch_raw_rnaseq_datasets_normalised = NORMALISATION_DESEQ2.out.cpm + if ( normalisation_method == 'tpm' ) { + COMPUTE_TPM( ch_raw_rnaseq_datasets_to_normalise ) + ch_raw_rnaseq_datasets_normalised = COMPUTE_TPM.out.counts - } else { // 'edger' - NORMALISATION_EDGER( ch_raw_rnaseq_datasets_to_normalise ) - ch_raw_rnaseq_datasets_normalised = NORMALISATION_EDGER.out.cpm + } else { // 'cpm' + COMPUTE_CPM( ch_raw_rnaseq_datasets_to_normalise ) + ch_raw_rnaseq_datasets_normalised = COMPUTE_CPM.out.counts } // From 1316c54ecdc4dc541961195157c4a55d09835821 Mon Sep 17 00:00:00 2001 From: Olivier Date: Sat, 22 Nov 2025 11:17:33 +0100 Subject: [PATCH 187/258] remove unecessary tag (1) from global processes --- modules/local/{ => old}/dataset_statistics/main.nf | 0 .../{ => old}/dataset_statistics/spec-file.txt | 0 subworkflows/local/base_statistics/main.nf | 6 +++--- .../local/expression_normalisation/main.nf | 3 +-- subworkflows/local/merge_data/main.nf | 6 +++--- subworkflows/local/stability_scoring/main.nf | 10 +++++----- workflows/stableexpression.nf | 14 +++++++------- 7 files changed, 19 insertions(+), 20 deletions(-) rename modules/local/{ => old}/dataset_statistics/main.nf (100%) rename modules/local/{ => old}/dataset_statistics/spec-file.txt (100%) diff --git a/modules/local/dataset_statistics/main.nf b/modules/local/old/dataset_statistics/main.nf similarity index 100% rename from modules/local/dataset_statistics/main.nf rename to modules/local/old/dataset_statistics/main.nf diff --git a/modules/local/dataset_statistics/spec-file.txt b/modules/local/old/dataset_statistics/spec-file.txt similarity index 100% rename from modules/local/dataset_statistics/spec-file.txt rename to modules/local/old/dataset_statistics/spec-file.txt diff --git a/subworkflows/local/base_statistics/main.nf b/subworkflows/local/base_statistics/main.nf index 86f6e811..091090e8 100644 --- a/subworkflows/local/base_statistics/main.nf +++ b/subworkflows/local/base_statistics/main.nf @@ -22,12 +22,12 @@ workflow BASE_STATISTICS { // ----------------------------------------------------------------- COMPUTE_BASE_STATISTICS_FOR_RNASEQ( - ch_rnaseq_counts, + ch_rnaseq_counts.collect(), "rnaseq" ) COMPUTE_BASE_STATISTICS_FOR_MICROARRAY( - ch_microarray_counts, + ch_microarray_counts.collect(), "microarray" ) @@ -36,7 +36,7 @@ workflow BASE_STATISTICS { // ----------------------------------------------------------------- COMPUTE_BASE_STATISTICS ( - ch_all_counts, + ch_all_counts.collect(), [] ) diff --git a/subworkflows/local/expression_normalisation/main.nf b/subworkflows/local/expression_normalisation/main.nf index 257a8317..3c53b8c0 100644 --- a/subworkflows/local/expression_normalisation/main.nf +++ b/subworkflows/local/expression_normalisation/main.nf @@ -1,5 +1,5 @@ include { NORMALISATION_COMPUTE_CPM as COMPUTE_CPM } from '../../../modules/local/normalisation/compute_cpm' -include { NORMALISATION_COMPUTE_CPM as COMPUTE_TPM } from '../../../modules/local/normalisation/compute_tpm' +include { NORMALISATION_COMPUTE_TPM as COMPUTE_TPM } from '../../../modules/local/normalisation/compute_tpm' include { QUANTILE_NORMALISATION } from '../../../modules/local/quantile_normalisation' /* @@ -30,7 +30,6 @@ workflow EXPRESSION_NORMALISATION { ch_datasets .raw.filter { meta, file -> meta.platform == 'rnaseq' } - .map { meta, file -> [ meta, file, meta.design ] } .set { ch_raw_rnaseq_datasets_to_normalise } if ( normalisation_method == 'tpm' ) { diff --git a/subworkflows/local/merge_data/main.nf b/subworkflows/local/merge_data/main.nf index 20a1d999..486ee5c5 100644 --- a/subworkflows/local/merge_data/main.nf +++ b/subworkflows/local/merge_data/main.nf @@ -33,7 +33,7 @@ workflow MERGE_DATA { MERGE_RNASEQ_COUNTS ( ch_normalised_rnaseq_counts.map { meta, file -> file }.collect(), - ch_whole_rnaseq_size + ch_whole_rnaseq_size.collect() ) MERGE_RNASEQ_COUNTS.out.counts.set { ch_merged_rnaseq_counts } @@ -46,7 +46,7 @@ workflow MERGE_DATA { MERGE_MICROARRAY_COUNTS ( ch_normalised_microarray_counts.map { meta, file -> file }.collect(), - ch_whole_microarray_size + ch_whole_microarray_size.collect() ) MERGE_MICROARRAY_COUNTS.out.counts.set { ch_merged_microarray_counts } @@ -65,7 +65,7 @@ workflow MERGE_DATA { MERGE_ALL_COUNTS( ch_platform_counts.collect(), - ch_whole_size + ch_whole_size.collect() ) // ----------------------------------------------------------------- diff --git a/subworkflows/local/stability_scoring/main.nf b/subworkflows/local/stability_scoring/main.nf index f902703f..08d9c439 100644 --- a/subworkflows/local/stability_scoring/main.nf +++ b/subworkflows/local/stability_scoring/main.nf @@ -28,8 +28,8 @@ workflow STABILITY_SCORING { // ----------------------------------------------------------------- GET_CANDIDATE_GENES( - ch_counts, - ch_stats, + ch_counts.collect(), + ch_stats.collect(), candidate_selection_descriptor, nb_top_gene_candidates, min_expr_threshold @@ -41,8 +41,8 @@ workflow STABILITY_SCORING { // ----------------------------------------------------------------- NORMFINDER ( - ch_candidate_gene_counts, - ch_design + ch_candidate_gene_counts.collect(), + ch_design.collect() ) NORMFINDER.out.stability_values.set { ch_normfinder_stability } @@ -62,7 +62,7 @@ workflow STABILITY_SCORING { // ----------------------------------------------------------------- COMPUTE_STABILITY_SCORES ( - ch_stats, + ch_stats.collect(), stability_score_weights, ch_normfinder_stability, ch_genorm_stability diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 2b559d37..e850db6e 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -161,12 +161,12 @@ workflow STABLEEXPRESSION { // ----------------------------------------------------------------- AGGREGATE_RESULTS ( - ch_all_counts, - ch_stats_all_genes_with_scores, + ch_all_counts.collect(), + ch_stats_all_genes_with_scores.collect(), BASE_STATISTICS.out.rnaseq_stats.ifEmpty( [] ), BASE_STATISTICS.out.microarray_stats.ifEmpty( [] ), - MERGE_DATA.out.whole_gene_metadata, - MERGE_DATA.out.whole_gene_id_mapping + MERGE_DATA.out.whole_gene_metadata.collect(), + MERGE_DATA.out.whole_gene_id_mapping.collect() ) AGGREGATE_RESULTS.out.all_genes_summary.set { ch_all_genes_summary } @@ -178,9 +178,9 @@ workflow STABLEEXPRESSION { // ----------------------------------------------------------------- DASH_APP( - ch_all_counts, - ch_whole_design, - ch_all_genes_summary + ch_all_counts.collect(), + ch_whole_design.collect(), + ch_all_genes_summary.collect() ) ch_versions = ch_versions.mix ( DASH_APP.out.versions ) From 5aa5945fe44a1bb429dd012a5f670bad6f5b220d Mon Sep 17 00:00:00 2001 From: Olivier Date: Sat, 22 Nov 2025 16:57:20 +0100 Subject: [PATCH 188/258] script to download annotation gff3 file given a species name --- bin/download_latest_annotation.py | 294 ++++++++++++++++++++++++++++-- bin/get_eatlas_accessions.py | 8 +- 2 files changed, 283 insertions(+), 19 deletions(-) diff --git a/bin/download_latest_annotation.py b/bin/download_latest_annotation.py index 8c3166f3..7d51485b 100755 --- a/bin/download_latest_annotation.py +++ b/bin/download_latest_annotation.py @@ -3,19 +3,21 @@ # Written by Olivier Coen. Released under the MIT license. import argparse -import json import logging +from datetime import datetime from pathlib import Path +from urllib.request import urlretrieve -import config import pandas as pd import requests +from bs4 import BeautifulSoup from tenacity import ( before_sleep_log, retry, stop_after_delay, wait_exponential, ) +from tqdm import tqdm logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -24,12 +26,27 @@ ENSEMBL_REST_SERVER = "https://rest.ensembl.org/" SPECIES_INFO_EXT = "info/genomes/taxonomy/{species}" -HEADERS = { +ENSEMBL_API_HEADERS = { "Content-Type": "application/json", "Accept": "application/json", } STOP_RETRY_AFTER_DELAY = 600 +NCBI_TAXONOMY_API_URL = "https://api.ncbi.nlm.nih.gov/datasets/v2/taxonomy" +NCBI_API_HEADERS = {"accept": "application/json", "content-type": "application/json"} + +ENSEMBL_DIVISION_TO_FOLDER = { + "EnsemblPlants": "plants", + "EnsemblVertebrates": "vertebrates", + "EnsemblMetazoa": "metazoa", + "EnsemblFungi": "fungi", + "EnsemblBacteria": "bacteria", + "EnsemblProtists": "protists", +} + +ENSEMBL_GENOMES_BASE_URL = "https://ftp.ebi.ac.uk/ensemblgenomes/pub/current/{}/gff3/" +ENSEMBL_VERTEBRATES_BASE_URL = "https://ftp.ensembl.org/pub/current/gff3/" + ################################################################## ################################################################## @@ -50,9 +67,35 @@ def parse_args(): return parser.parse_args() -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# QUERIES TO ENSEMBL -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +################################################################## +################################################################## +# REQUESTS +################################################################## +################################################################## + + +@retry( + stop=stop_after_delay(600), + wait=wait_exponential(multiplier=1, min=1, max=30), + before_sleep=before_sleep_log(logger, logging.WARNING), +) +def parse_page_data(url: str) -> BeautifulSoup: + page = requests.get(url) + page.raise_for_status() + return BeautifulSoup(page.content, "html.parser") + + +@retry( + stop=stop_after_delay(STOP_RETRY_AFTER_DELAY), + wait=wait_exponential(multiplier=1, min=1, max=30), + before_sleep=before_sleep_log(logger, logging.WARNING), +) +def send_request_to_ncbi_taxonomy(taxid: str | int): + taxons = [str(taxid)] + data = {"taxons": taxons} + response = requests.post(NCBI_TAXONOMY_API_URL, headers=NCBI_API_HEADERS, json=data) + response.raise_for_status() + return response.json() @retry( @@ -62,7 +105,7 @@ def parse_args(): ) def send_get_request_to_ensembl(url: str) -> list[dict]: logger.info(f"Sending GET request to {url}") - response = requests.get(url, headers=HEADERS) + response = requests.get(url, headers=ENSEMBL_API_HEADERS) if response.status_code == 200: response.raise_for_status() else: @@ -72,18 +115,227 @@ def send_get_request_to_ensembl(url: str) -> list[dict]: return response.json() -def get_species_division(species: str) -> str: - url = ENSEMBL_REST_SERVER + SPECIES_INFO_EXT.format(species=species) +@retry( + stop=stop_after_delay(STOP_RETRY_AFTER_DELAY), + wait=wait_exponential(multiplier=1, min=1, max=30), + before_sleep=before_sleep_log(logger, logging.WARNING), +) +def download_file(url: str, output_path: str): + try: + urlretrieve(url, output_path) + except Exception as e: + logger.error(f"Failed to download file from {url}: {e}") + raise + + +################################################################## +################################################################## +# PARSING +################################################################## +################################################################## + + +def get_species_taxid(species: str) -> int: + result = send_request_to_ncbi_taxonomy(species) + if len(result["taxonomy_nodes"]) > 1: + raise ValueError(f"Multiple taxids for species {species}") + metadata = result["taxonomy_nodes"][0] + if "taxonomy" not in metadata: + raise ValueError(f"Could not find taxonomy results for species {species}") + return int(metadata["taxonomy"]["tax_id"]) + + +def get_species_division(species_taxid: int) -> str: + url = ENSEMBL_REST_SERVER + SPECIES_INFO_EXT.format(species=str(species_taxid)) data = send_get_request_to_ensembl(url) if len(data) == 0: - raise ValueError(f"No division found for {species}") + raise ValueError(f"No division found for species Taxon ID {species_taxid}") elif len(data) > 1: logger.warning( - f"Multiple divisions found for {species}. Keeping the first one." + f"Multiple divisions found for species Taxon ID {species_taxid}. Keeping the first one." ) return data[0]["division"] +def get_species_category(species: str) -> str: + ncbi_formated_species_name = format_species_name_for_ncbi_taxonomy(species) + species_taxid = get_species_taxid(ncbi_formated_species_name) + division = get_species_division(species_taxid) + return ENSEMBL_DIVISION_TO_FOLDER[division] + + +def get_division_url(species: str) -> str: + category = get_species_category(species) + if category == "vertebrates": + return ENSEMBL_VERTEBRATES_BASE_URL + else: + return ENSEMBL_GENOMES_BASE_URL.format(category) + + +def format_species_name_for_ensembl(species: str) -> str: + return species.replace(" ", "_").lower() + + +def format_species_name_for_ncbi_taxonomy(species: str) -> str: + return species.replace("_", " ").lower() + + +def parse_last_modified_date(dt_string: str) -> datetime | None: + try: + return datetime.strptime(dt_string, "%Y-%m-%d %H:%M") + except ValueError: + return None + + +def get_candidate_species_folders( + species: str, url: str, first_level: bool = True +) -> list[dict]: + soup = parse_page_data(url) + species_url_records = [] + + # adding progress bar only at the first level + iterator = tqdm(soup.find_all("tr")) if first_level else soup.find_all("tr") + for item in iterator: + # all line sections + line_sections = list(item.find_all("td")) + # all folders of interest have an associated date + if len(line_sections) < 2: + continue + + folder_name_section = line_sections[1] + date_section = line_sections[2] + last_modified_date = parse_last_modified_date(date_section.text.strip()) + + for folder in folder_name_section.find_all("a"): + folder_url = f"{url}{folder.text}" + if folder.text.startswith(species): + d = { + "date": last_modified_date, + "url": folder_url, + "name": folder.text.rstrip("/"), + } + species_url_records.append(d) + print(folder.text) + elif folder.text.endswith("_collection/"): + species_url_records += get_candidate_species_folders( + species, folder_url, first_level=False + ) + else: + continue + + return species_url_records + + +def get_main_folder_url(records: list[dict], species: str) -> str | None: + main_folder_url = None + for record in records: + if record["name"] == species: + main_folder_url = record["url"] + break + return main_folder_url + + +def get_last_modified_folder_url(records: list[dict]) -> str: + df = pd.DataFrame.from_dict(records) + df.sort_values(by="date", ascending=False, inplace=True) + return df.iloc[0]["url"] + + +def get_current_annotation_folder(records: list[dict], species: str) -> str: + main_folder_url = get_main_folder_url(records, species) + if main_folder_url is not None: + return main_folder_url + + logger.info( + "Could not find a folder having the species as name. Checking for gca folders." + ) + gca_records = [ + record for record in records if record["name"].startswith(f"{species}_gca") + ] + if gca_records: + return get_last_modified_folder_url(gca_records) + + logger.info( + "Could not find a folder having the species as name. Getting the last modified one." + ) + return get_last_modified_folder_url(records) + + +def parse_size(size_str): + """ + Convert size strings like '902K', '4.1M', '5G' to bytes. + + Parameters: + ----------- + size_str : str + Size string with suffix (K, M, G, T, etc.) + + Returns: + -------- + int : size in bytes + """ + size_str = size_str.strip().upper() + + # Define multipliers + multipliers = {"K": 1024, "M": 1024**2, "G": 1024**3, "T": 1024**4, "P": 1024**5} + + # Check if last character is a unit + if size_str[-1] in multipliers: + number = float(size_str[:-1]) + multiplier = multipliers[size_str[-1]] + return int(number * multiplier) + else: + # No suffix, assume it's already in bytes + return int(float(size_str)) + + +def get_annotation_file(url: str) -> str: + soup = parse_page_data(url) + file_records = [] + + for item in soup.find_all("tr"): + # all line sections + line_sections = list(item.find_all("td")) + if len(line_sections) < 4: + continue + + file = line_sections[1].text.strip() + if not file.endswith(".gff3.gz"): + continue + + d = { + "file": file, + "date": parse_last_modified_date(line_sections[2].text.strip()), + "size": parse_size(line_sections[3].text.strip()), + } + file_records.append(d) + + if not file_records: + raise ValueError("No annotation files found") + + df = pd.DataFrame.from_dict(file_records) + + # keeping the biggest annotation + max_size_df = df.loc[ + [df["size"].idxmax()] + ] # double brackets to keep it as a DataFrame + if len(max_size_df) == 1: + return max_size_df["file"].iloc[0] + + # if multiple files with the same size, return the most recent + most_recent_df = max_size_df.loc[ + [max_size_df["date"].idxmax()] + ] # double brackets to keep it as a DataFrame + if len(most_recent_df) == 1: + return max_size_df["file"].iloc[0] + + # if still multiple files, return the first one + # remove the one ending with 'chr.gff3.gz' if it exists + if max_size_df["file"].str.endswith("chr.gff3.gz").any(): + max_size_df = max_size_df[~max_size_df["file"].str.endswith("chr.gff3.gz")] + return max_size_df["file"].iloc[0] + + ################################################################## ################################################################## # MAIN @@ -94,8 +346,24 @@ def get_species_division(species: str) -> str: def main(): args = parse_args() - species_division = get_species_division(args.species) - print(species_division) + species = format_species_name_for_ensembl(args.species) + division_url = get_division_url(species) + logger.info(f"Searching for the right folder in {division_url}") + + species_url_records = get_candidate_species_folders(species, division_url) + if not species_url_records: + raise ValueError(f"No species folder found for {species}") + + annotation_folder_url = get_current_annotation_folder(species_url_records, species) + logger.info(f"Found current annotation folder: {annotation_folder_url}") + + annotation_file = get_annotation_file(annotation_folder_url) + + annotation_full_url = annotation_folder_url + annotation_file + logger.info(f"Found annotation URL: {annotation_full_url}.\nDownloading...") + + download_file(annotation_full_url, annotation_file) + logger.info("Done") if __name__ == "__main__": diff --git a/bin/get_eatlas_accessions.py b/bin/get_eatlas_accessions.py index 8f3ec985..63a7368f 100755 --- a/bin/get_eatlas_accessions.py +++ b/bin/get_eatlas_accessions.py @@ -79,12 +79,8 @@ def get_data(url: str) -> dict: If the query fails """ response = requests.get(url) - if response.status_code == 200: - return response.json() - else: - raise RuntimeError( - f"Failed to retrieve data: encountered error {response.status_code}" - ) + response.raise_for_status() + return response.json() def get_experiment_description(exp_dict: dict): From bb60780cdec17e21cbe83538b8a92c02b61c997a Mon Sep 17 00:00:00 2001 From: Olivier Date: Sat, 22 Nov 2025 17:08:48 +0100 Subject: [PATCH 189/258] increase to process_high all modules that handle all data --- modules/local/aggregate_results/main.nf | 2 +- modules/local/compute_base_statistics/main.nf | 2 +- modules/local/compute_stability_scores/main.nf | 2 +- modules/local/get_candidate_genes/main.nf | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/local/aggregate_results/main.nf b/modules/local/aggregate_results/main.nf index 229a99f0..80a21270 100644 --- a/modules/local/aggregate_results/main.nf +++ b/modules/local/aggregate_results/main.nf @@ -1,6 +1,6 @@ process AGGREGATE_RESULTS { - label 'process_single' + label 'process_high' conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? diff --git a/modules/local/compute_base_statistics/main.nf b/modules/local/compute_base_statistics/main.nf index e5287ecd..5106ef62 100644 --- a/modules/local/compute_base_statistics/main.nf +++ b/modules/local/compute_base_statistics/main.nf @@ -1,6 +1,6 @@ process COMPUTE_BASE_STATISTICS { - label 'process_medium' + label 'process_high' conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? diff --git a/modules/local/compute_stability_scores/main.nf b/modules/local/compute_stability_scores/main.nf index 2e2a009f..715ddbbe 100644 --- a/modules/local/compute_stability_scores/main.nf +++ b/modules/local/compute_stability_scores/main.nf @@ -1,6 +1,6 @@ process COMPUTE_STABILITY_SCORES { - label 'process_single' + label 'process_high' conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? diff --git a/modules/local/get_candidate_genes/main.nf b/modules/local/get_candidate_genes/main.nf index a9ce645b..e7e1622d 100644 --- a/modules/local/get_candidate_genes/main.nf +++ b/modules/local/get_candidate_genes/main.nf @@ -1,6 +1,6 @@ process GET_CANDIDATE_GENES { - label 'process_single' + label 'process_high' conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? From 803227e8de8ed6a069c0ed1b578e1a79170ac21e Mon Sep 17 00:00:00 2001 From: Olivier Date: Sun, 23 Nov 2025 10:45:00 +0100 Subject: [PATCH 190/258] increase dragstically number of retries for OOM related erros --- conf/base.config | 20 +++++++++----------- modules/local/geo/getaccessions/main.nf | 2 +- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/conf/base.config b/conf/base.config index 7909e851..23c50a67 100644 --- a/conf/base.config +++ b/conf/base.config @@ -16,16 +16,19 @@ process { time = { 4.h * task.attempt } errorStrategy = { - if (task.exitStatus in (100..102)) { // managed errors; they should not be retried + if (task.exitStatus in (100..102)) { // managed errors; they should not be retried but ignored at once 'ignore' - } else if (task.attempt <= 5) { // all other errors should be retried with exponential backoff + } else if (task.exitStatus in ((130..145) + 104 + 175) && task.attempt <= 10) { // OOM & related errors; should be retried as long as memory does not fit sleep(Math.pow(2, task.attempt) * 200 as long) - return 'retry' - } else { // after 5 retries, ignore the error - return 'ignore' + 'retry' + } else if (task.attempt <= 3) { // all other errors should be retried with exponential backoff with max retry = 3 + sleep(Math.pow(2, task.attempt) * 200 as long) + 'retry' + } else { // after 3 retries, ignore the error + 'ignore' } } - maxRetries = 3 + maxRetries = 10 maxErrors = '-1' // Process-specific resource requirements @@ -50,11 +53,6 @@ process { memory = { 10.GB * task.attempt } time = { 4.h * task.attempt } } - withLabel:process_high_cpus { - cpus = { 12 * task.attempt } - memory = { 10.GB * task.attempt } - time = { 8.h * task.attempt } - } withLabel:process_high { cpus = { 12 * task.attempt } memory = { 20.GB * task.attempt } diff --git a/modules/local/geo/getaccessions/main.nf b/modules/local/geo/getaccessions/main.nf index 1294c0f1..b6877510 100644 --- a/modules/local/geo/getaccessions/main.nf +++ b/modules/local/geo/getaccessions/main.nf @@ -1,6 +1,6 @@ process GEO_GETACCESSIONS { - label 'process_high_cpus' + label 'process_high' tag "${species}" From 6eb77192249917a718bb9f27948d9a63df49b4ea Mon Sep 17 00:00:00 2001 From: Olivier Date: Sun, 23 Nov 2025 12:06:27 +0100 Subject: [PATCH 191/258] make script to compute max cdna length gene per gene from GGF3 file --- bin/get_gene_transcript_lengths.py | 133 ++++++++++++++++++ .../get_gene_lengths_from_ensembl_api.py} | 0 2 files changed, 133 insertions(+) create mode 100755 bin/get_gene_transcript_lengths.py rename bin/{get_gene_lengths.py => old/get_gene_lengths_from_ensembl_api.py} (100%) diff --git a/bin/get_gene_transcript_lengths.py b/bin/get_gene_transcript_lengths.py new file mode 100755 index 00000000..62cfedfe --- /dev/null +++ b/bin/get_gene_transcript_lengths.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 + +# Written by Olivier Coen. Released under the MIT license. + +import argparse +import logging +from pathlib import Path + +import pandas as pd + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +OUTFILE = "gene_lengths.csv" + +GFF_COLUMNS = [ + "chromosome", + "source", + "feature", + "start", + "end", + "score", + "strand", + "phase", + "attributes", +] + +DTYPES = { + "chromosome": str, + "source": str, + "feature": str, + "start": int, + "end": int, + "score": str, + "strand": str, + "phase": str, + "attributes": str, +} + + +################################################################## +################################################################## +# FUNCTIONS +################################################################## +################################################################## + + +def parse_args(): + parser = argparse.ArgumentParser("Get CDNA lengths from GFF3 annotation file") + parser.add_argument( + "--annotation", + type=Path, + dest="annotation_file", + required=True, + help="Annotation file in GFF3 format", + ) + return parser.parse_args() + + +def parse_gff3_file(annotation_file: Path): + return pd.read_csv( + annotation_file, + sep="\t", + names=GFF_COLUMNS, + dtype=DTYPES, + comment="#", + on_bad_lines="warn", + ) + + +def compute_transcript_lengths(df: pd.DataFrame): + exon_df = df.loc[df["feature"] == "exon"].copy() + # extract transcript ID from attributes column for each exon + exon_df["transcript_id"] = exon_df["attributes"].str.extract( + r"Parent=transcript:([^;]+)" + ) + # compute transcript length + exon_df["length"] = exon_df["end"] - exon_df["start"] + 1 + exon_df = exon_df[["transcript_id", "length"]] + return exon_df.groupby("transcript_id", as_index=False).agg({"length": "sum"}) + + +def compute_max_transcript_lengths_per_gene( + df: pd.DataFrame, transcript_lengths_df: pd.DataFrame +): + rna_cols = [ + feature + for feature in df["feature"].unique() + if "RNA" in feature and "gene" not in feature + ] + rna_df = df.loc[df["feature"].isin(rna_cols)].copy() + + # extract gene ID from attributes column for each transcript + rna_df["gene_id"] = rna_df["attributes"].str.extract(r"Parent=gene:([^;]+)") + # extract transcript ID from attributes column + rna_df["transcript_id"] = rna_df["attributes"].str.extract(r"ID=transcript:([^;]+)") + + # merge with transcript lengths dataframe to get length + merged_df = rna_df.merge(transcript_lengths_df, how="left", on="transcript_id") + logger.info( + f"Got length for {len(merged_df) / len(rna_df) * 100:.2f}% of transcripts" + ) + # compute max transcript length per gene + merged_df = merged_df[["gene_id", "length"]] + return merged_df.groupby("gene_id", as_index=False).agg({"length": "max"}) + + +################################################################## +################################################################## +# MAIN +################################################################## +################################################################## + + +def main(): + args = parse_args() + + logger.info("Parsing annotation file") + df = parse_gff3_file(args.annotation_file) + + logger.info("Computing transcript lengths") + transcript_lengths_df = compute_transcript_lengths(df) + + # keep only mRNA and exon features + logger.info("Getting max transcript length per gene") + gene_length_df = compute_max_transcript_lengths_per_gene(df, transcript_lengths_df) + + logger.info(f"Writing to {OUTFILE}") + gene_length_df.to_csv(OUTFILE, index=False, header=True) + + +if __name__ == "__main__": + main() diff --git a/bin/get_gene_lengths.py b/bin/old/get_gene_lengths_from_ensembl_api.py similarity index 100% rename from bin/get_gene_lengths.py rename to bin/old/get_gene_lengths_from_ensembl_api.py From e8a5916446861657f8255d765144892f85090728 Mon Sep 17 00:00:00 2001 From: Olivier Date: Sun, 23 Nov 2025 13:05:58 +0100 Subject: [PATCH 192/258] add computation of tpm in workflow and set it as default --- bin/compute_tpm.py | 29 ++++----- bin/config.py | 2 +- ... => download_latest_ensembl_annotation.py} | 0 bin/get_gene_transcript_lengths.py | 21 ++++-- .../compute_gene_transcript_lengths/main.nf | 38 +++++++++++ .../spec-file.txt | 42 ++++++++++++ .../local/download_ensembl_annotation/main.nf | 34 ++++++++++ .../download_ensembl_annotation/spec-file.txt | 64 +++++++++++++++++++ .../local/normalisation/compute_cpm/main.nf | 2 +- .../local/normalisation/compute_tpm/main.nf | 8 ++- nextflow.config | 2 +- nextflow_schema.json | 2 +- .../local/expression_normalisation/main.nf | 13 +++- .../local/get_transcript_lengths/main.nf | 30 +++++++++ workflows/stableexpression.nf | 1 + 15 files changed, 255 insertions(+), 33 deletions(-) rename bin/{download_latest_annotation.py => download_latest_ensembl_annotation.py} (100%) create mode 100644 modules/local/compute_gene_transcript_lengths/main.nf create mode 100644 modules/local/compute_gene_transcript_lengths/spec-file.txt create mode 100644 modules/local/download_ensembl_annotation/main.nf create mode 100644 modules/local/download_ensembl_annotation/spec-file.txt create mode 100644 subworkflows/local/get_transcript_lengths/main.nf diff --git a/bin/compute_tpm.py b/bin/compute_tpm.py index 02f55f19..678a6c49 100755 --- a/bin/compute_tpm.py +++ b/bin/compute_tpm.py @@ -29,11 +29,11 @@ def parse_args(): "--counts", type=Path, dest="count_file", required=True, help="Count file" ) parser.add_argument( - "--annotation", + "--gene-lengths", type=Path, - dest="annotation_file", + dest="gene_lengths_file", required=True, - help="Gene annotation file (GFF format)", + help="Gene lengths file (CSV format)", ) return parser.parse_args() @@ -45,22 +45,17 @@ def parse_counts(file: Path): return pd.read_csv(file, header=0, sep="\t", index_col=0) -def parse_annotation(file: Path) -> pd.DataFrame: - pass - - -def get_cdna_lengths(annotation_df: pd.DataFrame) -> pd.DataFrame: - pass - - def reorder_gene_lengths( count_df: pd.DataFrame, cdna_length_df: pd.DataFrame -) -> tuple[pd.DataFrame, pd.Series]: +) -> pd.Series: # merge with gene length and extracts it afterwards # so that the genes are in the same order in df and in cdna_length_series - gene_id_df = pd.DataFrame({"gene_id": count_df.index}) - gene_id_df = pd.merge(gene_id_df, cdna_length_df, how="left", on="gene_id") + gene_id_df = pd.DataFrame({config.GENE_ID_COLNAME: count_df.index}) + gene_id_df = pd.merge( + gene_id_df, cdna_length_df, how="left", on=config.GENE_ID_COLNAME + ) cdna_length_series = gene_id_df[config.CDNA_LENGTH_COLNAME].astype(float) + cdna_length_series.index = gene_id_df[config.GENE_ID_COLNAME] return cdna_length_series @@ -123,9 +118,7 @@ def main(): count_df = parse_counts(args.count_file) count_df.index.name = config.GENE_ID_COLNAME - annotation_df = parse_annotation(args.annotation_file) - - cdna_length_df + cdna_length_df = pd.read_csv(args.gene_lengths_file, header=0) logger.info("Reordering gene lengths") cdna_length_series = reorder_gene_lengths(count_df, cdna_length_df) @@ -133,7 +126,7 @@ def main(): logger.info(f"Normalising {args.count_file.name}") count_df = compute_tpm(count_df, cdna_length_series) - + print(count_df) export_normalised_data(count_df, args.count_file) diff --git a/bin/config.py b/bin/config.py index 4326c38d..ba32d51c 100644 --- a/bin/config.py +++ b/bin/config.py @@ -1,6 +1,6 @@ # general column names GENE_ID_COLNAME = "gene_id" -CDNA_LENGTH_COLNAME = "cdna_length" +CDNA_LENGTH_COLNAME = "length" RANK_COLNAME = "rank" # base statistics diff --git a/bin/download_latest_annotation.py b/bin/download_latest_ensembl_annotation.py similarity index 100% rename from bin/download_latest_annotation.py rename to bin/download_latest_ensembl_annotation.py diff --git a/bin/get_gene_transcript_lengths.py b/bin/get_gene_transcript_lengths.py index 62cfedfe..2b0e3b1d 100755 --- a/bin/get_gene_transcript_lengths.py +++ b/bin/get_gene_transcript_lengths.py @@ -6,12 +6,13 @@ import logging from pathlib import Path +import config import pandas as pd logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -OUTFILE = "gene_lengths.csv" +OUTFILE = "gene_transcript_lengths.csv" GFF_COLUMNS = [ "chromosome", @@ -75,9 +76,11 @@ def compute_transcript_lengths(df: pd.DataFrame): r"Parent=transcript:([^;]+)" ) # compute transcript length - exon_df["length"] = exon_df["end"] - exon_df["start"] + 1 - exon_df = exon_df[["transcript_id", "length"]] - return exon_df.groupby("transcript_id", as_index=False).agg({"length": "sum"}) + exon_df[config.CDNA_LENGTH_COLNAME] = exon_df["end"] - exon_df["start"] + 1 + exon_df = exon_df[["transcript_id", config.CDNA_LENGTH_COLNAME]] + return exon_df.groupby("transcript_id", as_index=False).agg( + {config.CDNA_LENGTH_COLNAME: "sum"} + ) def compute_max_transcript_lengths_per_gene( @@ -91,7 +94,9 @@ def compute_max_transcript_lengths_per_gene( rna_df = df.loc[df["feature"].isin(rna_cols)].copy() # extract gene ID from attributes column for each transcript - rna_df["gene_id"] = rna_df["attributes"].str.extract(r"Parent=gene:([^;]+)") + rna_df[config.GENE_ID_COLNAME] = rna_df["attributes"].str.extract( + r"Parent=gene:([^;]+)" + ) # extract transcript ID from attributes column rna_df["transcript_id"] = rna_df["attributes"].str.extract(r"ID=transcript:([^;]+)") @@ -101,8 +106,10 @@ def compute_max_transcript_lengths_per_gene( f"Got length for {len(merged_df) / len(rna_df) * 100:.2f}% of transcripts" ) # compute max transcript length per gene - merged_df = merged_df[["gene_id", "length"]] - return merged_df.groupby("gene_id", as_index=False).agg({"length": "max"}) + merged_df = merged_df[[config.GENE_ID_COLNAME, config.CDNA_LENGTH_COLNAME]] + return merged_df.groupby(config.GENE_ID_COLNAME, as_index=False).agg( + {config.CDNA_LENGTH_COLNAME: "max"} + ) ################################################################## diff --git a/modules/local/compute_gene_transcript_lengths/main.nf b/modules/local/compute_gene_transcript_lengths/main.nf new file mode 100644 index 00000000..32cf4c7e --- /dev/null +++ b/modules/local/compute_gene_transcript_lengths/main.nf @@ -0,0 +1,38 @@ +process COMPUTE_GENE_TRANSCRIPT_LENGTHS { + + label 'process_low' + + tag "${gff3.baseName}" + + conda "${moduleDir}/spec-file.txt" + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/3d/3d7126100b0eb7cb53dfb50291707ea8dda3b9738b76551ab73605d0acbe114b/data': + 'community.wave.seqera.io/library/pandas:2.3.3--5a902bf824a79745' }" + + input: + path gff3 + + output: + path('gene_transcript_lengths.csv'), emit: csv + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions + tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions + + script: + def is_compressed = gff3.getExtension() == "gz" ? true : false + def gff3_name = is_compressed ? gff3.getBaseName() : gff3 + """ + if [ "${is_compressed}" == "true" ]; then + gzip -c -d ${gff3} > ${gff3_name} + fi + + get_gene_transcript_lengths.py \\ + --annotation ${gff3_name} + """ + + + stub: + """ + touch gene_transcript_lengths.csv + """ + +} diff --git a/modules/local/compute_gene_transcript_lengths/spec-file.txt b/modules/local/compute_gene_transcript_lengths/spec-file.txt new file mode 100644 index 00000000..f79332cf --- /dev/null +++ b/modules/local/compute_gene_transcript_lengths/spec-file.txt @@ -0,0 +1,42 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda#f7b4d76975aac7e5d9e6ad13845f92fe +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda#c0374badb3a5d4b1372db28d19462c53 +https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda#f0991f0f84902f6b6009b4d2350a83aa +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda#280ea6eee9e2ddefde25ff799c4f0363 +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda#5b767048b1b3ee9a954b06f4084f93dc +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda#f627678cf829bd70bccf141a19c3ad3e +https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-bootstrap_ha15bf96_3.conda#3036ca5b895b7f5146c5a25486234a68 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-hcd61629_7.conda#f116940d825ffc9104400f0d7f1a4551 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_7.conda#8621a450add4e231f676646880703f49 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda#be43915efc66345cccb3c310b6ed0374 +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-1_h4a7cf45_openblas.conda#8b39e1ae950f1b54a3959c58ca2c32b8 +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-1_h0358290_openblas.conda#a670bff9eb7963ea41b4e09a4e4ab608 +https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda#8b09ae86839581147ef2e5c5e229d164 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda#35f29eec58405aaf55e01cb470d8c26a +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-1_h47877c9_openblas.conda#dee12a83aa4aca5077ea23c0605de044 +https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc +https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.0-hee844dc_0.conda#729a572a3ebb8c43933b30edcc628ceb +https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda#80c07c68d2f6870250959dcc95b209d1 +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda#9ee58d5c534af06558933af3c845a780 +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 +https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 +https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda#86bc20552bf46075e3d92b67f089172d +https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a +https://conda.anaconda.org/conda-forge/linux-64/python-3.13.9-hc97d973_101_cp313.conda#4780fe896e961722d0623fa91d0d3378 +https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.5-py313hf6604e3_0.conda#15f43bcd12c90186e78801fafc53d89b +https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda#3339e3b65d58accf4ca4fb8748ab16b3 +https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 +https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 +https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 +https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.3-py313h08cd8bf_1.conda#9e87d4bda0c2711161d765332fa38781 +https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda#bf47878473e5ab9fdb4115735230e191 diff --git a/modules/local/download_ensembl_annotation/main.nf b/modules/local/download_ensembl_annotation/main.nf new file mode 100644 index 00000000..d0ed5d62 --- /dev/null +++ b/modules/local/download_ensembl_annotation/main.nf @@ -0,0 +1,34 @@ +process DOWNLOAD_ENSEMBL_ANNOTATION { + + label 'process_low' + + tag "${species}" + + conda "${moduleDir}/spec-file.txt" + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/5f/5fa11d593e2f2d68c60acc6a00c812793112bff4691754c992fff6b038458604/data': + 'community.wave.seqera.io/library/bs4_pandas_requests_tenacity_tqdm:32f7387852168716' }" + + input: + val species + + output: + path "*.gff3.gz", emit: gff3 + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions + tuple val("${task.process}"), val('requests'), eval('python3 -c "import requests; print(requests.__version__)"'), topic: versions + tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions + tuple val("${task.process}"), val('bs4'), eval('python3 -c "import bs4; print(bs4.__version__)"'), topic: versions + tuple val("${task.process}"), val('tqdm'), eval('python3 -c "import tqdm; print(tqdm.__version__)"'), topic: versions + + script: + """ + download_latest_ensembl_annotation.py \\ + --species ${species} + """ + + stub: + """ + touch fake.gff3.gz.txt + """ + +} diff --git a/modules/local/download_ensembl_annotation/spec-file.txt b/modules/local/download_ensembl_annotation/spec-file.txt new file mode 100644 index 00000000..8255eae2 --- /dev/null +++ b/modules/local/download_ensembl_annotation/spec-file.txt @@ -0,0 +1,64 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda#f7b4d76975aac7e5d9e6ad13845f92fe +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda#c0374badb3a5d4b1372db28d19462c53 +https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-bootstrap_ha15bf96_3.conda#3036ca5b895b7f5146c5a25486234a68 +https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda#8b09ae86839581147ef2e5c5e229d164 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda#35f29eec58405aaf55e01cb470d8c26a +https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc +https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda#280ea6eee9e2ddefde25ff799c4f0363 +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda#5b767048b1b3ee9a954b06f4084f93dc +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda#f627678cf829bd70bccf141a19c3ad3e +https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.0-hee844dc_0.conda#729a572a3ebb8c43933b30edcc628ceb +https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda#80c07c68d2f6870250959dcc95b209d1 +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda#f0991f0f84902f6b6009b4d2350a83aa +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda#9ee58d5c534af06558933af3c845a780 +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 +https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 +https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda#86bc20552bf46075e3d92b67f089172d +https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a +https://conda.anaconda.org/conda-forge/linux-64/python-3.13.9-hc97d973_101_cp313.conda#4780fe896e961722d0623fa91d0d3378 +https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8-pyhd8ed1ab_0.conda#18c019ccf43769d211f2cf78e9ad46c2 +https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda#0caa1af407ecff61170c9437a808404d +https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda#edd329d7d3a4ab45dcf905899a7a6115 +https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.2-pyha770c72_0.conda#749ebebabc2cae99b2e5b3edd04c6ca2 +https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py313h09d1b84_0.conda#dfd94363b679c74937b3926731ee861a +https://conda.anaconda.org/conda-forge/noarch/bs4-4.14.2-hd8ed1ab_0.conda#19dbd742f9c26cfe9b89a05461aa68c3 +https://conda.anaconda.org/conda-forge/noarch/certifi-2025.11.12-pyhd8ed1ab_0.conda#96a02a5c1a65470a7e4eedb644c872fd +https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda#12c566707c80111f9799308d9e265aef +https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py313hf46b229_1.conda#d0616e7935acab407d1543b28c446f6f +https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.4-pyhd8ed1ab_0.conda#a22d1fd9bf98827e280a02875d9a007a +https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda#962b9857ee8e7018c22f2776ffa0b2d7 +https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda#0a802cb9888dd14eeefc611f05c40b6e +https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda#8e6923fc12f1fe8f8c4e5c9f343256ac +https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda#164fc43f0b53b6e3a7bc7dce5e4f1dc9 +https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda#53abe63df7e10a6ba605dc5f9f961d36 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-hcd61629_7.conda#f116940d825ffc9104400f0d7f1a4551 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_7.conda#8621a450add4e231f676646880703f49 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda#be43915efc66345cccb3c310b6ed0374 +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-2_h4a7cf45_openblas.conda#6146bf1b7f58113d54614c6ec683c14a +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-2_h0358290_openblas.conda#a84b2b7ed34206d14739fb8d29cd2799 +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-2_h47877c9_openblas.conda#9fb20e74a7436dc94dd39d9a9decddc3 +https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.5-py313hf6604e3_0.conda#15f43bcd12c90186e78801fafc53d89b +https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda#3339e3b65d58accf4ca4fb8748ab16b3 +https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 +https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 +https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 +https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.3-py313h08cd8bf_1.conda#9e87d4bda0c2711161d765332fa38781 +https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda#bf47878473e5ab9fdb4115735230e191 +https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda#461219d1a5bd61342293efa2c0c90eac +https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda#6432cb5d4ac0046c3ac0a8a0f95842f9 +https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.25.0-py313h54dd161_1.conda#710d4663806d0f72b2fb414e936223b5 +https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda#436c165519e140cb08d246a4472a9d6a +https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda#db0c6b99149880c8ba515cf4abe93ee4 +https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.2-pyhd8ed1ab_0.conda#5d99943f2ae3cc69e1ada12ce9d4d701 +https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda#9efbfdc37242619130ea42b1cc4ed861 diff --git a/modules/local/normalisation/compute_cpm/main.nf b/modules/local/normalisation/compute_cpm/main.nf index 34a9fc54..ab54cbfa 100644 --- a/modules/local/normalisation/compute_cpm/main.nf +++ b/modules/local/normalisation/compute_cpm/main.nf @@ -1,6 +1,6 @@ process NORMALISATION_COMPUTE_CPM { - label 'process_single' + label 'process_low' tag "${meta.dataset}" diff --git a/modules/local/normalisation/compute_tpm/main.nf b/modules/local/normalisation/compute_tpm/main.nf index 78d86ec3..fb9efbfc 100644 --- a/modules/local/normalisation/compute_tpm/main.nf +++ b/modules/local/normalisation/compute_tpm/main.nf @@ -1,6 +1,6 @@ process NORMALISATION_COMPUTE_TPM { - label 'process_single' + label 'process_low' tag "${meta.dataset}" @@ -11,6 +11,7 @@ process NORMALISATION_COMPUTE_TPM { input: tuple val(meta), path(count_file) + path gene_lengths_file output: tuple val(meta), path('*.tpm.csv'), optional: true, emit: counts @@ -19,8 +20,9 @@ process NORMALISATION_COMPUTE_TPM { script: """ - compute_cpm.py \\ - --counts $count_file + compute_tpm.py \\ + --counts $count_file \\ + --gene-lengths $gene_lengths_file """ diff --git a/nextflow.config b/nextflow.config index 97a30299..60b82d21 100644 --- a/nextflow.config +++ b/nextflow.config @@ -43,7 +43,7 @@ params { skip_id_mapping = false // statistics - normalisation_method = 'cpm' + normalisation_method = 'tpm' quantile_norm_target_distrib = 'uniform' nb_top_gene_candidates = 5000 ks_pvalue_threshold = -1 diff --git a/nextflow_schema.json b/nextflow_schema.json index c3c0c4b4..5e033cc3 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -226,7 +226,7 @@ "description": "Count normalisation method", "fa_icon": "fas fa-divide", "enum": ["tpm", "cpm"], - "default": "cpm", + "default": "tpm", "help_text": "Raw RNAseq data must be normalised before further processing. `tmp offers a more accurate representation of gene expression levels as it is unbiased toward gene length. However, you can choose `cpm` if you do not have access to a genome annotation." }, "quantile_norm_target_distrib": { diff --git a/subworkflows/local/expression_normalisation/main.nf b/subworkflows/local/expression_normalisation/main.nf index 3c53b8c0..70286828 100644 --- a/subworkflows/local/expression_normalisation/main.nf +++ b/subworkflows/local/expression_normalisation/main.nf @@ -2,6 +2,8 @@ include { NORMALISATION_COMPUTE_CPM as COMPUTE_CPM } from '../../../modules/lo include { NORMALISATION_COMPUTE_TPM as COMPUTE_TPM } from '../../../modules/local/normalisation/compute_tpm' include { QUANTILE_NORMALISATION } from '../../../modules/local/quantile_normalisation' +include { GET_TRANSCRIPT_LENGTHS } from '../../../subworkflows/local/get_transcript_lengths' + /* ======================================================================================== SUBWORKFLOW TO NORMALISE AND HARMONISE EXPRESSION DATASETS @@ -11,6 +13,7 @@ include { QUANTILE_NORMALISATION } from '../../../modules/lo workflow EXPRESSION_NORMALISATION { take: + species ch_datasets normalisation_method quantile_norm_target_distrib @@ -33,7 +36,15 @@ workflow EXPRESSION_NORMALISATION { .set { ch_raw_rnaseq_datasets_to_normalise } if ( normalisation_method == 'tpm' ) { - COMPUTE_TPM( ch_raw_rnaseq_datasets_to_normalise ) + + // download genome annotation + // and computing length of the longest transcript gene per gene + GET_TRANSCRIPT_LENGTHS (species) + + COMPUTE_TPM( + ch_raw_rnaseq_datasets_to_normalise, + GET_TRANSCRIPT_LENGTHS.out.csv + ) ch_raw_rnaseq_datasets_normalised = COMPUTE_TPM.out.counts } else { // 'cpm' diff --git a/subworkflows/local/get_transcript_lengths/main.nf b/subworkflows/local/get_transcript_lengths/main.nf new file mode 100644 index 00000000..16c73707 --- /dev/null +++ b/subworkflows/local/get_transcript_lengths/main.nf @@ -0,0 +1,30 @@ +include { COMPUTE_GENE_TRANSCRIPT_LENGTHS } from '../../../modules/local/compute_gene_transcript_lengths' +include { DOWNLOAD_ENSEMBL_ANNOTATION } from '../../../modules/local/download_ensembl_annotation' + + +/* +======================================================================================== + SUBWORKFLOW TO DOWNLOAD EXPRESSIONATLAS ACCESSIONS AND DATASETS +======================================================================================== +*/ + +workflow GET_TRANSCRIPT_LENGTHS { + + take: + species + + main: + + DOWNLOAD_ENSEMBL_ANNOTATION (species) + ch_annotation = DOWNLOAD_ENSEMBL_ANNOTATION.out.gff3 + + COMPUTE_GENE_TRANSCRIPT_LENGTHS (ch_annotation) + + + + emit: + csv = COMPUTE_GENE_TRANSCRIPT_LENGTHS.out.csv + + + +} diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index e850db6e..9ddfa200 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -109,6 +109,7 @@ workflow STABLEEXPRESSION { // ----------------------------------------------------------------- EXPRESSION_NORMALISATION( + species, ch_counts, params.normalisation_method, params.quantile_norm_target_distrib From c1f90eb1256827dbdb61bb074a6b07547585689c Mon Sep 17 00:00:00 2001 From: Olivier Date: Sun, 23 Nov 2025 20:45:45 +0100 Subject: [PATCH 193/258] replace quantile normalisation by linear scaling to [0,1] in stability score computation --- bin/compute_stability_scores.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/bin/compute_stability_scores.py b/bin/compute_stability_scores.py index d26e9b60..6558967d 100755 --- a/bin/compute_stability_scores.py +++ b/bin/compute_stability_scores.py @@ -46,18 +46,28 @@ def parse_stability_score_weights(self): ): self.weights[weight_field] = float(weight) + """ @staticmethod def quantile_normalise(data: pl.Series, new_name: str) -> pl.Series: - """ + ''' Quantile normalize a series - """ + ''' array = data.to_numpy().reshape(-1, 1) transformer = QuantileTransformer(output_distribution="uniform", subsample=None) normalised_array = transformer.fit_transform(array) return pl.Series(new_name, normalised_array.ravel()) + """ + + def linear_normalise(self, data: pl.Series, new_name: str) -> pl.Series: + """ + Linearly normalise a series + """ + min_val = data.min() + max_val = data.max() + return pl.Series(new_name, (data - min_val) / (max_val - min_val)) @staticmethod - def get_normalsed_col(col: str) -> str: + def get_normalised_col(col: str) -> str: return f"{col}_normalised" def compute_stability_score(self): @@ -79,10 +89,8 @@ def compute_stability_score(self): data = candidate_df.select(col).to_series() # for each column present, we quantile normalise the data to have values between 0 and 1 # and put these normalised data in another column suffixed with "_normalised" - normalised_col = self.get_normalsed_col(col) - normalised_data[col] = self.quantile_normalise( - data, new_name=normalised_col - ) + normalised_col = self.get_normalised_col(col) + normalised_data[col] = self.linear_normalise(data, new_name=normalised_col) # creating a null column with same name null_data[col] = pl.Series(normalised_col, [None] * len(non_candidate_df)) # counting the sum of weights corresponding to the columns present @@ -115,7 +123,7 @@ def compute_stability_score(self): if col not in self.df.columns: logger.warning(f"Column {col} not found in dataframe") continue - normalised_col = self.get_normalsed_col(col) + normalised_col = self.get_normalised_col(col) # we do not want to include null / nan values in the stability score calculation # because this would result in a total null / nan value for the stability score stability_scoring_expr += ( From ea8798676ec32aa1f586fbb1e2545f60e884efb3 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 24 Nov 2025 13:45:04 +0100 Subject: [PATCH 194/258] make script and module to download latest annotation from ncbi --- ....py => download_latest_ncbi_annotation.py} | 103 ++++++++++++++---- .../local/download_ncbi_annotation/main.nf | 33 ++++++ .../download_ncbi_annotation/spec-file.txt | 57 ++++++++++ 3 files changed, 170 insertions(+), 23 deletions(-) rename bin/{old/get_annotation_accession.py => download_latest_ncbi_annotation.py} (58%) create mode 100644 modules/local/download_ncbi_annotation/main.nf create mode 100644 modules/local/download_ncbi_annotation/spec-file.txt diff --git a/bin/old/get_annotation_accession.py b/bin/download_latest_ncbi_annotation.py similarity index 58% rename from bin/old/get_annotation_accession.py rename to bin/download_latest_ncbi_annotation.py index d48d9d16..609a4e54 100755 --- a/bin/old/get_annotation_accession.py +++ b/bin/download_latest_ncbi_annotation.py @@ -4,7 +4,10 @@ import argparse import logging +import shutil import sys +import zipfile +from pathlib import Path import requests from tenacity import ( @@ -20,13 +23,20 @@ logger = logging.getLogger(__name__) # Modern NCBI API -NCBI_TAXONOMY_API_URL = "https://api.ncbi.nlm.nih.gov/datasets/v2/taxonomy" -NCBI_GENOME_DATASET_REPORT_API_URL = ( - "https://api.ncbi.nlm.nih.gov/datasets/v2/genome/taxon/{taxid}/dataset_report" -) -NCBI_GENOME_DATASET_REPORT_API_PARAMS = "filters.has_annotation=true&page_size=1000" +NCBI_DATASET_API_URL = "https://api.ncbi.nlm.nih.gov/datasets/v2/" + +NCBI_TAXONOMY_ENDPOINT = "taxonomy" +NCBI_GENOME_DATASET_REPORT_BASE_ENDPOINT = "genome/taxon/{taxid}/dataset_report" +NCBI_DOWNLOAD_ENDPOINT = "genome/download" + + +NCBI_GENOME_DATASET_REPORT_API_PARAMS = { + "filters.has_annotation": True, + "page_size": 1000, +} NCBI_API_HEADERS = {"accept": "application/json", "content-type": "application/json"} +DOWNLOADED_FILENAME = "ncbi_dataset.zip" ACCESSION_FILE = "accession.txt" @@ -57,10 +67,9 @@ def parse_args(): wait=wait_exponential(multiplier=1, min=1, max=30), before_sleep=before_sleep_log(logger, logging.WARNING), ) -def send_request_to_ncbi_taxonomy(taxid: str | int): - taxons = [str(taxid)] - data = {"taxons": taxons} - response = requests.post(NCBI_TAXONOMY_API_URL, headers=NCBI_API_HEADERS, json=data) +def send_post_request_to_ncbi_dataset(endpoint: str, data: dict, params: dict = {}): + url = NCBI_DATASET_API_URL + endpoint + response = requests.post(url, headers=NCBI_API_HEADERS, json=data, params=params) response.raise_for_status() return response.json() @@ -70,10 +79,9 @@ def send_request_to_ncbi_taxonomy(taxid: str | int): wait=wait_exponential(multiplier=1, min=1, max=30), before_sleep=before_sleep_log(logger, logging.WARNING), ) -def send_request_to_ncbi_genome_dataset_report_api(taxid: int): - url = NCBI_GENOME_DATASET_REPORT_API_URL.format(taxid=taxid) - url += f"?{NCBI_GENOME_DATASET_REPORT_API_PARAMS}" - response = requests.get(url, headers=NCBI_API_HEADERS) +def send_get_request_to_ncbi_dataset(endpoint: str, params: dict = {}): + url = NCBI_DATASET_API_URL + endpoint + response = requests.get(url, headers=NCBI_API_HEADERS, params=params) response.raise_for_status() return response.json() @@ -86,7 +94,8 @@ def send_request_to_ncbi_genome_dataset_report_api(taxid: int): def get_species_taxid(species: str) -> int: - result = send_request_to_ncbi_taxonomy(species) + data = {"taxons": [species]} + result = send_post_request_to_ncbi_dataset(NCBI_TAXONOMY_ENDPOINT, data) if len(result["taxonomy_nodes"]) > 1: raise ValueError(f"Multiple taxids for species {species}") @@ -101,6 +110,14 @@ def get_species_taxid(species: str) -> int: return int(metadata["taxonomy"]["tax_id"]) +def get_assembly_reports(taxid: int): + result = send_get_request_to_ncbi_dataset( + endpoint=NCBI_GENOME_DATASET_REPORT_BASE_ENDPOINT.format(taxid=taxid), + params=NCBI_GENOME_DATASET_REPORT_API_PARAMS, + ) + return result.get("reports", []) + + def get_assembly_with_best_stats(reports: list[dict]): sorted_reports = sorted( reports, @@ -146,6 +163,30 @@ def format_species_name(species: str): return species.replace("_", " ").lower() +def download_genome_annotation(genome_accession: str) -> str: + data = {"accessions": [genome_accession], "include_annotation_type": ["GENOME_GFF"]} + params = {"filename": DOWNLOADED_FILENAME} + send_post_request_to_ncbi_dataset(NCBI_TAXONOMY_ENDPOINT, data, params) + + +def extract_annotation_file_from_archive(): + with zipfile.ZipFile(DOWNLOADED_FILENAME, "r") as zip_ref: + zip_ref.extractall() + + valid_files = list(Path().cwd().glob(f"ncbi_dataset/data/{accession}/*.gff")) + + if not valid_files: + raise ValueError(f"No annotation file found for accession {accession}") + + if len(valid_files) > 1: + logger.warning( + f"Multiple annotation files found for accession {accession}. Taking the first one" + ) + + annotation_file = valid_files[0] + shutil.move(annotation_file, f"{accession}.gff") + + ##################################################### ##################################################### # MAIN @@ -160,17 +201,33 @@ def format_species_name(species: str): logger.info(f"Species taxid: {species_taxid}") logger.info(f"Getting best NCBI assembly for taxid: {species_taxid}") - result = send_request_to_ncbi_genome_dataset_report_api(species_taxid) + reports = get_assembly_reports(species_taxid) - try: - reports = result["reports"] - best_assembly_report = get_reference_assembly(reports) - logger.info(f"Best assembly: {best_assembly_report['accession']}") - except Exception as e: - logger.error(f"Could not get any assembly for taxid {species_taxid}: {e}") + if not reports: + logger.error(f"No assembly reports found for taxid {species_taxid}") sys.exit(100) - with open(ACCESSION_FILE, "w") as fout: - fout.write(best_assembly_report["accession"]) + # looping while we can get an annotation file + annotation_found = False + while not annotation_found: + best_assembly_report = get_reference_assembly(reports) + logger.info( + f"Best assembly: {best_assembly_report['accession']}. Trying to download annotation" + ) + accession = best_assembly_report["accession"] + try: + download_genome_annotation(accession) + extract_annotation_file_from_archive() + annotation_found = True + except Exception as e: + logger.error(f"Error downloading annotation for accession {accession}: {e}") + + if not annotation_found: + # Remove the best assembly report from the list of reports + reports = [report for report in reports if report["accession"] != accession] + + if not annotation_found: + logger.error(f"No annotation found for taxid {species_taxid}") + sys.exit(100) logger.info("Done") diff --git a/modules/local/download_ncbi_annotation/main.nf b/modules/local/download_ncbi_annotation/main.nf new file mode 100644 index 00000000..33b9a525 --- /dev/null +++ b/modules/local/download_ncbi_annotation/main.nf @@ -0,0 +1,33 @@ +process DOWNLOAD_NCBI_ANNOTATION { + + label 'process_low' + + tag "${species}" + + conda "${moduleDir}/spec-file.txt" + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/5c/5c28c8e613c062828aaee4b950029bc90a1a1aa94d5f61016a588c8ec7be8b65/data': + 'community.wave.seqera.io/library/pandas_requests_tenacity:5ba56df089a9d718' }" + + input: + val species + + output: + path "*.gff.gz", emit: gff + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions + tuple val("${task.process}"), val('requests'), eval('python3 -c "import requests; print(requests.__version__)"'), topic: versions + + script: + """ + download_latest_ncbi_annotation.py \\ + --species ${species} + + gzip -n *.gff + """ + + stub: + """ + touch fake.gff3.gz.txt + """ + +} diff --git a/modules/local/download_ncbi_annotation/spec-file.txt b/modules/local/download_ncbi_annotation/spec-file.txt new file mode 100644 index 00000000..3233c10b --- /dev/null +++ b/modules/local/download_ncbi_annotation/spec-file.txt @@ -0,0 +1,57 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda#3cd1a7238a0dd3d0860fdefc496cc854 +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda#9e60c55e725c20d23125a5f0dd69af5d +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda#6d11a5edae89fe413c0569f16d308f5a +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda#e66f2b8ad787e7beb0f846e4bd7e8493 +https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62ee74e96c5ebb0af99386de58cf9553 +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_0.conda#e31316a586cac398b1fcdb10ace786b9 +https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda#db0bfbe7dd197b68ad5f30333bae6ce0 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 +https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc +https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda#d864d34357c3b65a4b731f78c0801dc4 +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.2-h6cd9bfd_0.conda#b04c7eda6d7dab1e6503135e7fad4d25 +https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b +https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.6.15-hbd8a1cb_0.conda#72525f07d72806e3b639ad4504c30ce5 +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda#c87df2ab1448ba69169652ab9547082d +https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 +https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 +https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a +https://conda.anaconda.org/conda-forge/linux-64/python-3.12.8-h9e4cc4f_1_cpython.conda#7fd2fd79436d9b473812f14e86746844 +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda#0dfcdc155cf23812a0c9deada86fb723 +https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h2ec8cdc_3.conda#a32e0c069f6c3dcac635f7b0b0dac67e +https://conda.anaconda.org/conda-forge/noarch/certifi-2025.6.15-pyhd8ed1ab_0.conda#781d068df0cc2407d4db0ecfbb29225b +https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda#12c566707c80111f9799308d9e265aef +https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py312h06ac9bb_0.conda#a861504bbea4161a9170b85d4d2be840 +https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.2-pyhd8ed1ab_0.conda#40fe4284b8b5835a9073a645139f35af +https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda#0a802cb9888dd14eeefc611f05c40b6e +https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda#8e6923fc12f1fe8f8c4e5c9f343256ac +https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda#b4754fb1bdcb70c8fd54f918301582c6 +https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda#39a4f67be3286c86d696df570b1201b7 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda#530566b68c3b8ce7eec4cd047eae19fe +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda#bfbca721fd33188ef923dfe9ba172f29 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda#323dc8f259224d13078aaf7ce96c3efe +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-32_h59b9bed_openblas.conda#2af9f3d5c2e39f417ce040f5a35c40c6 +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-32_he106b2a_openblas.conda#3d3f9355e52f269cd8bc2c440d8a5263 +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-32_h7ac8fdf_openblas.conda#6c3f04ccb6c578138e9f9899da0bd714 +https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.1-py312h6cf2f7f_0.conda#7e086a30150af2536a1059885368dcf0 +https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda#a451d576819089b0d672f18768be0f65 +https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 +https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 +https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 +https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.3-py312hf9745cd_3.conda#2979458c23c7755683a0598fb33e7666 +https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e +https://conda.anaconda.org/conda-forge/noarch/wheel-0.45.1-pyhd8ed1ab_1.conda#75cb7132eb58d97896e173ef12ac9986 +https://conda.anaconda.org/conda-forge/noarch/pip-25.1.1-pyh8b19718_0.conda#32d0781ace05105cc99af55d36cbec7c +https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda#461219d1a5bd61342293efa2c0c90eac +https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py312h66e93f0_2.conda#630db208bc7bbb96725ce9832c7423bb +https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda#436c165519e140cb08d246a4472a9d6a +https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda#a9b9368f3701a417eac9edbcae7cb737 +https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.2-pyhd8ed1ab_0.conda#5d99943f2ae3cc69e1ada12ce9d4d701 From 1ae9906ce10031cacb44c3d27bbfdbf116dc7886 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 24 Nov 2025 16:23:21 +0100 Subject: [PATCH 195/258] big reformating of the steps for fetching accessions and download datasets; addition of two parameters for subsampling randomly the accessions in order to reduce computational overhead --- conf/modules.config | 3 +- conf/modules/expression_atlas.config | 19 --- conf/modules/geo.config | 17 --- conf/modules/public_data.config | 31 ++++ conf/test_dataset_eatlas.config | 2 +- .../tool/nf_core_stableexpression.xml | 24 +-- modules/local/geo/getaccessions/main.nf | 4 - nextflow.config | 34 ++--- nextflow_schema.json | 113 ++++++-------- .../local/download_public_datasets/main.nf | 66 ++++++++ .../local/expressionatlas_fetchdata/main.nf | 101 ------------ subworkflows/local/geo_fetchdata/main.nf | 144 ------------------ .../local/get_public_accessions/main.nf | 142 +++++++++++++++++ .../main.nf | 28 +--- .../expressionatlas_fetchdata/main.nf.test | 8 +- .../local/geo_fetchdata/main.nf.test | 16 +- tests/workflows/stableexpression.nf.test | 4 +- workflows/stableexpression.nf | 54 ++++--- 18 files changed, 365 insertions(+), 445 deletions(-) delete mode 100644 conf/modules/expression_atlas.config delete mode 100644 conf/modules/geo.config create mode 100644 conf/modules/public_data.config create mode 100644 subworkflows/local/download_public_datasets/main.nf delete mode 100644 subworkflows/local/expressionatlas_fetchdata/main.nf delete mode 100644 subworkflows/local/geo_fetchdata/main.nf create mode 100644 subworkflows/local/get_public_accessions/main.nf diff --git a/conf/modules.config b/conf/modules.config index 6248bbb3..11705c20 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -19,8 +19,7 @@ process { } -includeConfig 'modules/expression_atlas.config' -includeConfig 'modules/geo.config' +includeConfig 'modules/public_data.config' includeConfig 'modules/id_mapping.config' includeConfig 'modules/normalisation.config' includeConfig 'modules/qc.config' diff --git a/conf/modules/expression_atlas.config b/conf/modules/expression_atlas.config deleted file mode 100644 index 454de041..00000000 --- a/conf/modules/expression_atlas.config +++ /dev/null @@ -1,19 +0,0 @@ -process { - - withName: EXPRESSIONATLAS_GETACCESSIONS { - publishDir = [ - path: { "${params.outdir}/expression_atlas/accessions/" }, - mode: params.publish_dir_mode - ] - } - - withName: EXPRESSIONATLAS_GETDATA { - - publishDir = [ - path: { "${params.outdir}/expression_atlas/datasets/" }, - mode: params.publish_dir_mode - ] - - } - -} diff --git a/conf/modules/geo.config b/conf/modules/geo.config deleted file mode 100644 index 17971c46..00000000 --- a/conf/modules/geo.config +++ /dev/null @@ -1,17 +0,0 @@ -process { - - withName: GEO_GETACCESSIONS { - publishDir = [ - path: { "${params.outdir}/geo/accessions/" }, - mode: params.publish_dir_mode - ] - } - - withName: GEO_GETDATA { - publishDir = [ - path: { "${params.outdir}/geo/datasets/" }, - mode: params.publish_dir_mode - ] - } - -} diff --git a/conf/modules/public_data.config b/conf/modules/public_data.config new file mode 100644 index 00000000..fb8b521b --- /dev/null +++ b/conf/modules/public_data.config @@ -0,0 +1,31 @@ +process { + + withName: 'STABLEEXPRESSION:GET_PUBLIC_ACCESSIONS:EXPRESSION_ATLAS' { + publishDir = [ + path: { "${params.outdir}/accessions/expression_atlas/" }, + mode: params.publish_dir_mode + ] + } + + withName: 'STABLEEXPRESSION:GET_PUBLIC_ACCESSIONS:GEO' { + publishDir = [ + path: { "${params.outdir}/accessions/geo/" }, + mode: params.publish_dir_mode + ] + } + + withName: 'STABLEEXPRESSION:DOWNLOAD_PUBLIC_DATASETS:EXPRESSION_ATLAS' { + publishDir = [ + path: { "${params.outdir}/downloaded/expression_atlas/" }, + mode: params.publish_dir_mode + ] + } + + withName: 'STABLEEXPRESSION:DOWNLOAD_PUBLIC_DATASETS:GEO' { + publishDir = [ + path: { "${params.outdir}/downloaded/geo/" }, + mode: params.publish_dir_mode + ] + } + +} diff --git a/conf/test_dataset_eatlas.config b/conf/test_dataset_eatlas.config index e4a0553d..6d9ae3a2 100644 --- a/conf/test_dataset_eatlas.config +++ b/conf/test_dataset_eatlas.config @@ -17,7 +17,7 @@ params { // Input data species = 'mus_musculus' - eatlas_accessions = "E-MTAB-2262" + accessions = "E-MTAB-2262" skip_fetch_eatlas_accessions = true skip_fetch_geo_accessions = true datasets = 'https://raw.githubusercontent.com/nf-core/test-datasets/stableexpression/test_data/input_datasets/input_big.yaml' diff --git a/galaxy/tool_shed/tool/nf_core_stableexpression.xml b/galaxy/tool_shed/tool/nf_core_stableexpression.xml index 6764ec86..6172d00e 100644 --- a/galaxy/tool_shed/tool/nf_core_stableexpression.xml +++ b/galaxy/tool_shed/tool/nf_core_stableexpression.xml @@ -75,11 +75,11 @@ VERSION="1.0dev"; echo "$VERSION" #if $expression_atlas_options.eatlas_accessions_file --eatlas_accessions_file "$expression_atlas_options.eatlas_accessions_file" #end if - #if $expression_atlas_options.exclude_eatlas_accessions - --exclude_eatlas_accessions "$expression_atlas_options.exclude_eatlas_accessions" + #if $expression_atlas_options.excluded_eatlas_accessions + --excluded_eatlas_accessions "$expression_atlas_options.excluded_eatlas_accessions" #end if - #if $expression_atlas_options.exclude_eatlas_accessions_file - --exclude_eatlas_accessions_file "$expression_atlas_options.exclude_eatlas_accessions_file" + #if $expression_atlas_options.excluded_eatlas_accessions_file + --excluded_eatlas_accessions_file "$expression_atlas_options.excluded_eatlas_accessions_file" #end if #if $geo_dataset_options.skip_fetch_geo_accessions --skip_fetch_geo_accessions $geo_dataset_options.skip_fetch_geo_accessions @@ -90,11 +90,11 @@ VERSION="1.0dev"; echo "$VERSION" #if $geo_dataset_options.geo_accessions_file --geo_accessions_file "$geo_dataset_options.geo_accessions_file" #end if - #if $geo_dataset_options.exclude_geo_accessions - --exclude_geo_accessions "$geo_dataset_options.exclude_geo_accessions" + #if $geo_dataset_options.excluded_geo_accessions + --excluded_geo_accessions "$geo_dataset_options.excluded_geo_accessions" #end if - #if $geo_dataset_options.exclude_geo_accessions_file - --exclude_geo_accessions_file "$geo_dataset_options.exclude_geo_accessions_file" + #if $geo_dataset_options.excluded_geo_accessions_file + --excluded_geo_accessions_file "$geo_dataset_options.excluded_geo_accessions_file" #end if #if $idmapping_options.skip_id_mapping --skip_id_mapping $idmapping_options.skip_id_mapping @@ -162,10 +162,10 @@ VERSION="1.0dev"; echo "$VERSION" ([A-Z0-9-]+,?)+ - + ([A-Z0-9-]+,?)+ - +
    @@ -173,10 +173,10 @@ VERSION="1.0dev"; echo "$VERSION" ([A-Z0-9-]+,?)+ - + ([A-Z0-9-]+,?)+ - +
    diff --git a/modules/local/geo/getaccessions/main.nf b/modules/local/geo/getaccessions/main.nf index b6877510..86c95c37 100644 --- a/modules/local/geo/getaccessions/main.nf +++ b/modules/local/geo/getaccessions/main.nf @@ -14,7 +14,6 @@ process GEO_GETACCESSIONS { val keywords val platform path excluded_accessions_file - val accessions output: path "accessions.txt", optional: true, emit: accessions @@ -42,9 +41,6 @@ process GEO_GETACCESSIONS { if ( excluded_accessions_file != [] ) { args += " --exclude-accessions-in $excluded_accessions_file" } - if ( accessions != 'none' ) { - args += " --accessions $accessions" - } // the folder where nltk will download data needs to be writable (necessary for singularity) """ # the Entrez module from biopython automatically stores temp results in /.config diff --git a/nextflow.config b/nextflow.config index 60b82d21..efd6f000 100644 --- a/nextflow.config +++ b/nextflow.config @@ -22,25 +22,19 @@ params { datasets = null // Expression atlas + skip_fetch_public_accessions = false skip_fetch_eatlas_accessions = false - eatlas_accessions = "" - exclude_eatlas_accessions = "" - eatlas_accessions_file = null - exclude_eatlas_accessions_file = null - - // GEO - min_nb_eatlas_datasets_auto_skip_geo = 1500 - skip_fetch_geo_accessions = false - geo_accessions = "" - exclude_geo_accessions = "" - geo_accessions_file = null - exclude_geo_accessions_file = null + skip_fetch_geo_accessions = false + accessions = "" + excluded_accessions = "" + accessions_file = null + excluded_accessions_file = null // ID mapping gprofiler_target_db = "ENSG" gene_metadata = null gene_id_mapping = null - skip_id_mapping = false + skip_id_mapping = false // statistics normalisation_method = 'tpm' @@ -54,12 +48,16 @@ params { candidate_selection_descriptor = "cv" stability_score_weights = "0.8,0.1,0.1,0" + // random sampling + random_sampling_seed = 42 + random_sampling_size = null + // MultiQC options - multiqc_config = null - multiqc_title = null - multiqc_logo = null - max_multiqc_email_size = '25.MB' - multiqc_methods_description = null + multiqc_config = null + multiqc_title = null + multiqc_logo = null + max_multiqc_email_size = '25.MB' + multiqc_methods_description = null // Boilerplate options outdir = null diff --git a/nextflow_schema.json b/nextflow_schema.json index 5e033cc3..120f54d4 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -76,98 +76,59 @@ } } }, - "expression_atlas_options": { - "title": "Expression Atlas options", + "public_data_options": { + "title": "Public data options", "type": "object", "fa_icon": "fas fa-book-atlas", - "description": "Options for fetching experiment data from Expression Atlas.", + "description": "Options for fetching experiment data from Expression Atlas / GEO.", "properties": { + "skip_fetch_public_accessions": { + "type": "boolean", + "fa_icon": "fas fa-cloud-arrow-down", + "description": "Skip fetching public accessions", + "help_text": "Expression Atlas / GEO accessions are automatically fetched by default. Set this parameter to skip this step. Equivalent to `--skip_fetch_eatlas_accessions --skip_fetch_geo_accessions`" + }, "skip_fetch_eatlas_accessions": { "type": "boolean", "fa_icon": "fas fa-cloud-arrow-down", "description": "Skip fetching Expression Atlas accessions", "help_text": "Expression Atlas accessions are automatically fetched by default. Set this parameter to skip this step." }, - "eatlas_accessions": { - "type": "string", - "pattern": "([A-Z0-9-]+,?)+", - "description": "Expression Atlas accession(s) to include", - "fa_icon": "fas fa-address-card", - "help_text": "Provide Expression Atlas accession(s) that you want to download. The accessions should be comma-separated. Example: `--eatlas_accessions E-MTAB-552,E-GEOD-61690`. Combine with --skip_fetch_eatlas_accessions if you want only these accessions to be used." - }, - "eatlas_accessions_file": { - "type": "string", - "format": "file-path", - "exists": true, - "description": "File containing Expression Atlas accession(s) to download", - "fa_icon": "fas fa-file", - "help_text": "File containing Expression Atlas accession(s) that you want to download. One accession per line. Example: `--eatlas_accessions_file included_accessions.txt`. Combine with --skip_fetch_eatlas_accessions if you want only these accessions to be used." - }, - "exclude_eatlas_accessions": { - "type": "string", - "pattern": "([A-Z0-9-]+,?)+", - "description": "Expression Atlas accession(s) to exclude", - "fa_icon": "fas fa-id-card", - "help_text": "Provide Expression Atlas accession(s) that you want to exclude. The accessions should be comma-separated. Example: `--exclude_eatlas_accessions E-MTAB-552,E-GEOD-61690`" - }, - "exclude_eatlas_accessions_file": { - "type": "string", - "format": "file-path", - "exists": true, - "description": "File containing Expression Atlas accession(s) to exclude", - "fa_icon": "fas fa-file", - "help_text": "File containing Expression Atlas accession(s) that you want to exclude. One accession per line. Example: `--exclude_eatlas_accessions_file excluded_accessions.txt`." - } - } - }, - "geo_dataset_options": { - "title": "GEO dataset options", - "type": "object", - "fa_icon": "fas fa-book-atlas", - "description": "Options for fetching datasets from NCBI GEO.", - "properties": { - "min_nb_eatlas_datasets_auto_skip_geo": { - "type": "integer", - "minimum": 0, - "default": 1500, - "description": "Automatically skip fetching GEO datasets when the number of downloaded Expression Atlas datasets exceeds this threshold.", - "help_text": "By default, both Expression Atlas and GEO datasets are fetched. If enough Expression Atlas datasets are downloaded, fetching GEO accessing and downloading datasets will be skipped automatically. As of November 2025, this feature was made specifically for `homo sapiens`, for which the overall number of datasets largely exceeds usual needs." - }, "skip_fetch_geo_accessions": { "type": "boolean", "fa_icon": "fas fa-cloud-arrow-down", "description": "Skip fetching GEO accessions", "help_text": "GEO accessions are automatically fetched by default. Set this parameter to skip this step." }, - "geo_accessions": { + "accessions": { "type": "string", "pattern": "([A-Z0-9-]+,?)+", - "description": "GEO accession(s) to include", - "fa_icon": "fas fa-id-card", - "help_text": "Provide GEO accessions that you want to download. The accessions should be comma-separated. Example: `--geo_accessions GSE8165,GSE8161`. Combine with --skip_fetch_geo_accessions if you want only these accessions to be used." + "description": "Expression Atlas / GEO accession(s) to include", + "fa_icon": "fas fa-address-card", + "help_text": "Provide Expression Atlas / GEO accession(s) that you want to download. The accessions should be comma-separated. Example: `--accessions E-MTAB-552,E-GEOD-61690,GSE8165,GSE8161`. Combine with --skip_fetch_accessions if you want only these accessions to be used. User provided accessions are prioritised over excluded accessions." }, - "geo_accessions_file": { + "accessions_file": { "type": "string", "format": "file-path", "exists": true, - "description": "File containing GEO accession(s) to download", + "description": "File containing Expression Atlas / GEO accession(s) to download", "fa_icon": "fas fa-file", - "help_text": "File containing GEO series accession(s) that you want to download. One accession per line. Example: `--geo_accessions_file included_accessions.txt`. Combine with --skip_fetch_geo_accessions if you want only these accessions to be used." + "help_text": "File containing Expression Atlas / GEO accession(s) that you want to download. One accession per line. Example: `--accessions_file included_accessions.txt`. Combine with --skip_fetch_accessions if you want only these accessions to be used. User provided accessions are prioritised over excluded accessions." }, - "exclude_geo_accessions": { + "excluded_accessions": { "type": "string", "pattern": "([A-Z0-9-]+,?)+", - "description": "GEO accession(s) to exclude", + "description": "Expression Atlas accession(s) to exclude", "fa_icon": "fas fa-id-card", - "help_text": "Provide GEO accessions that you want to exclude. The accessions should be comma-separated. Example: `--exclude_geo_accessions GSE8165,GSE8161`" + "help_text": "Provide Expression Atlas / GEO accession(s) that you want to exclude. The accessions should be comma-separated. Example: `--excluded_accessions E-MTAB-552,E-GEOD-61690`" }, - "exclude_geo_accessions_file": { + "excluded_accessions_file": { "type": "string", "format": "file-path", "exists": true, - "description": "File containing GEO accession(s) to exclude", + "description": "File containing Expression Atlas accession(s) to exclude", "fa_icon": "fas fa-file", - "help_text": "File containing GEO series accession(s) that you want to exclude. One accession per line. Example: `--exclude_geo_accessions_file excluded_accessions.txt`." + "help_text": "File containing Expression Atlas / GEO accession(s) that you want to exclude. One accession per line. Example: `--excluded_accessions_file excluded_accessions.txt`." } } }, @@ -292,6 +253,28 @@ } } }, + "scalability_options": { + "title": "Scalability options", + "type": "object", + "fa_icon": "fas fa-terminal", + "description": "Options to improve pipeline scalability and robustness", + "properties": { + "random_sampling_size": { + "type": "integer", + "description": "Number of public dataset accessions (Expression Atlas + GEO) to sample randomly before downloading.", + "fa_icon": "fas fa-sort-numeric-up-alt", + "minimum": 1, + "help_text": "When dealing with species for which there is a large number (eg. >1000) of public datasets, users may encounter RAM issues (eg. errors with `137` exit codes). In such cases, it is recommended to sample a random subset of these datasets to reduce the computational load. The subsampling is performed after getting proper accessions from Expression Atlas and GEO, AFTER excluding unwanted accessions (obtained through `--excluded_accessions` or `--excluded_accession_file`) but BEFORE including user provided accessions (obtained through `--accessions` or `--accession_file`)." + }, + "random_sampling_seed": { + "type": "integer", + "description": "Seed for dataset random sampling.", + "fa_icon": "fas fa-sort-numeric-up-alt", + "minimum": 0, + "help_text": "Seed for dataset random sampling. This ensures reproducibility of the random sampling process. Changing the seed will result in a different random sample being selected." + } + } + }, "institutional_config_options": { "title": "Institutional config options", "type": "object", @@ -455,10 +438,7 @@ "$ref": "#/$defs/input_output_options" }, { - "$ref": "#/$defs/expression_atlas_options" - }, - { - "$ref": "#/$defs/geo_dataset_options" + "$ref": "#/$defs/public_data_options" }, { "$ref": "#/$defs/idmapping_options" @@ -469,6 +449,9 @@ { "$ref": "#/$defs/stability_scoring_options" }, + { + "$ref": "#/$defs/scalability_options" + }, { "$ref": "#/$defs/institutional_config_options" }, diff --git a/subworkflows/local/download_public_datasets/main.nf b/subworkflows/local/download_public_datasets/main.nf new file mode 100644 index 00000000..9008a0ef --- /dev/null +++ b/subworkflows/local/download_public_datasets/main.nf @@ -0,0 +1,66 @@ +include { EXPRESSIONATLAS_GETDATA as EXPRESSION_ATLAS } from '../../../modules/local/expressionatlas/getdata' +include { GEO_GETDATA as GEO } from '../../../modules/local/geo/getdata' + +include { addDatasetIdToMetadata } from '../utils_nfcore_stableexpression_pipeline' +include { groupFilesByDatasetId } from '../utils_nfcore_stableexpression_pipeline' +include { augmentMetadata } from '../utils_nfcore_stableexpression_pipeline' + +/* +======================================================================================== + SUBWORKFLOW TO DOWNLOAD GEO ACCESSIONS AND DATASETS +======================================================================================== +*/ + +workflow DOWNLOAD_PUBLIC_DATASETS { + + take: + species + ch_accessions + + + main: + + ch_datasets = Channel.empty() + ch_fetched_accessions = Channel.empty() + + ch_accessions = ch_accessions + .branch { acc -> + eatlas: acc.startsWith('E-') + geo: acc.startsWith('GSE') + } + + // ------------------------------------------------------------------------------------ + // DOWNLOAD EXPRESSION ATLAS DATASETS + // ------------------------------------------------------------------------------------ + + // Downloading Expression Atlas data for each accession in ch_accessions + EXPRESSION_ATLAS( ch_accessions.eatlas ) + + // ------------------------------------------------------------------------------------ + // DOWNLOAD GEO DATASETS + // ------------------------------------------------------------------------------------ + + // Downloading GEO datasets for each accession in ch_accessions + GEO( + ch_accessions.geo, + species + ) + + ch_downloaded_counts = EXPRESSION_ATLAS.out.counts.mix ( GEO.out.counts ) + ch_downloaded_design = EXPRESSION_ATLAS.out.design.mix ( GEO.out.design ) + + // adding dataset id (accession + data_type) in the file meta + // flattening in case multiple files are returned at once + ch_counts = addDatasetIdToMetadata( ch_downloaded_counts.flatten() ) + ch_design = addDatasetIdToMetadata( ch_downloaded_design.flatten() ) + + // adding design files to the meta of their respective count files + ch_datasets = groupFilesByDatasetId( ch_design, ch_counts ) + + // adding normalisation state in the meta + augmentMetadata( ch_datasets ) + + emit: + datasets = ch_datasets + +} diff --git a/subworkflows/local/expressionatlas_fetchdata/main.nf b/subworkflows/local/expressionatlas_fetchdata/main.nf deleted file mode 100644 index 49c93968..00000000 --- a/subworkflows/local/expressionatlas_fetchdata/main.nf +++ /dev/null @@ -1,101 +0,0 @@ -include { EXPRESSIONATLAS_GETACCESSIONS } from '../../../modules/local/expressionatlas/getaccessions' -include { EXPRESSIONATLAS_GETDATA } from '../../../modules/local/expressionatlas/getdata' - -include { addDatasetIdToMetadata } from '../utils_nfcore_stableexpression_pipeline' -include { groupFilesByDatasetId } from '../utils_nfcore_stableexpression_pipeline' -include { augmentMetadata } from '../utils_nfcore_stableexpression_pipeline' - -/* -======================================================================================== - SUBWORKFLOW TO DOWNLOAD EXPRESSIONATLAS ACCESSIONS AND DATASETS -======================================================================================== -*/ - -workflow EXPRESSIONATLAS_FETCHDATA { - - take: - ch_species - - - main: - - ch_eatlas_datasets = Channel.empty() - ch_fetched_accessions = Channel.empty() - - ch_eatlas_accessions_file = params.eatlas_accessions_file ? Channel.fromPath(params.eatlas_accessions_file, checkIfExists: true) : Channel.empty() - - Channel.fromList( params.eatlas_accessions.tokenize(',') ) - .mix( ch_eatlas_accessions_file.splitText() ) - .unique() - .map { acc -> acc.trim() } - .set { ch_input_accessions } - - // fetching Expression Atlas accessions if applicable - if ( !params.skip_fetch_eatlas_accessions ) { - - // getting Expression Atlas accessions given a species name and keywords - // keywords can be an empty string - def platform = params.platform?: 'none' - EXPRESSIONATLAS_GETACCESSIONS( - ch_species, - params.keywords, - platform - ) - - // removing E-GTEX-* accessions by default because they are too big - // however, contrary to E-PROT- accessions, they can be added by the user - EXPRESSIONATLAS_GETACCESSIONS.out.accessions - .splitText() - .filter { acc -> !acc.startsWith('E-GTEX-') } - .set { ch_fetched_accessions } - - } - - ch_exclude_eatlas_accessions_file = params.exclude_eatlas_accessions_file ? Channel.fromPath(params.exclude_eatlas_accessions_file, checkIfExists: true) : Channel.empty() - - // getting accessions to exclude and preparing in the right format - Channel.fromList( params.exclude_eatlas_accessions.tokenize(',') ) - .mix( ch_exclude_eatlas_accessions_file.splitText() ) - .unique() - .map { acc -> acc.trim() } - .toList() - .map { lst -> [lst] } // list of lists : mandatory when combining in the next step - .set { ch_excluded_accessions } - - // appending to accessions provided by the user - // ensures that no accessions is present twice (provided by the user and fetched from E. Atlas) - // removing E-PROT- accessions because they are not supported in subsequent steps - // removing excluded accessions - ch_input_accessions - .mix( ch_fetched_accessions ) - .unique() - .map { acc -> acc.trim() } - .filter { acc -> acc.startsWith('E-') && !acc.startsWith('E-PROT-') } - .combine ( ch_excluded_accessions ) - .filter { accession, excluded_accessions -> !(accession in excluded_accessions) } - .map { accession, excluded_accessions -> accession } - .set { ch_accessions } - - if ( !params.accessions_only ) { - - // Downloading Expression Atlas data for each accession in ch_accessions - EXPRESSIONATLAS_GETDATA( ch_accessions ) - - // adding dataset id (accession + data_type) in the file meta - // flattening in case multiple files are returned at once - ch_design = addDatasetIdToMetadata( EXPRESSIONATLAS_GETDATA.out.design.flatten() ) - ch_counts = addDatasetIdToMetadata( EXPRESSIONATLAS_GETDATA.out.counts.flatten() ) - - // adding design files to the meta of their respective count files - ch_eatlas_datasets = groupFilesByDatasetId( ch_design, ch_counts ) - - // adding normalisation state in the meta - augmentMetadata( ch_eatlas_datasets ) - - } - - emit: - downloaded_datasets = ch_eatlas_datasets - accessions = ch_accessions - -} diff --git a/subworkflows/local/geo_fetchdata/main.nf b/subworkflows/local/geo_fetchdata/main.nf deleted file mode 100644 index 6eaa1de1..00000000 --- a/subworkflows/local/geo_fetchdata/main.nf +++ /dev/null @@ -1,144 +0,0 @@ -include { GEO_GETACCESSIONS } from '../../../modules/local/geo/getaccessions' -include { GEO_GETDATA } from '../../../modules/local/geo/getdata' -include { addDatasetIdToMetadata } from '../utils_nfcore_stableexpression_pipeline' -include { groupFilesByDatasetId } from '../utils_nfcore_stableexpression_pipeline' -include { augmentMetadata } from '../utils_nfcore_stableexpression_pipeline' -include { geoDatasetsToFetch } from '../utils_nfcore_stableexpression_pipeline' - -/* -======================================================================================== - SUBWORKFLOW TO DOWNLOAD GEO ACCESSIONS AND DATASETS -======================================================================================== -*/ - -workflow GEO_FETCHDATA { - - take: - species - skip_fetch_geo_accessions - accessions_only - platform - keywords - geo_accessions - geo_accessions_file - exclude_geo_accessions - exclude_geo_accessions_file - ch_eatlas_excluded_accessions - ch_nb_downloaded_eatlas_datasets - min_nb_eatlas_datasets_auto_skip_geo - outdir - - - main: - - ch_datasets = Channel.empty() - ch_fetched_accessions = Channel.empty() - - // ------------------------------------------------------------------------------------ - // PREPARE EXCLUDED ACCESSIONS - // ------------------------------------------------------------------------------------ - - // getting accessions to exclude from GEO - ch_eatlas_excluded_accessions - .filter { accession -> accession.startsWith("E-GEOD-") } - .map { accession -> accession.replace("E-GEOD-", "GSE") } - .set { ch_excluded_eatlas_accessions } - - // parsing file listing excluded accessions - ch_exclude_geo_accessions_file = exclude_geo_accessions_file ? Channel.fromPath(exclude_geo_accessions_file, checkIfExists: true) : Channel.empty() - - // getting accessions to exclude and preparing in the right format - Channel.fromList( exclude_geo_accessions.tokenize(',') ) - .mix( ch_excluded_eatlas_accessions ) - .mix( ch_exclude_geo_accessions_file.splitText() ) - .unique() - .map { acc -> acc.trim() } // removing spaces - .set { ch_excluded_accessions } - - ch_excluded_accessions - .collectFile( - name: 'excluded_geo_accessions.txt', - storeDir: "${outdir}/geo/", - sort: true, - newLine: true - ) - .ifEmpty( [] ) - .set { ch_excluded_accessions_file } - - // ------------------------------------------------------------------------------------ - // GET GEO ACCESSIONS - // ------------------------------------------------------------------------------------ - - // fetching GEO accessions if applicable - if ( !skip_fetch_geo_accessions ) { - - // checking the number of Expression Atlas datasets downloaded - // and storing whether to skip fetching GEO accessions - ch_geo_to_fetch = geoDatasetsToFetch( ch_nb_downloaded_eatlas_datasets, min_nb_eatlas_datasets_auto_skip_geo ) - - // trick to decide whether to fetch GEO accessions or not depending on ch_geo_to_fetch - Channel.value(species) - .combine( ch_geo_to_fetch ) - .filter{ species_name, to_fetch -> to_fetch } // kept only when to_fetch is true - .map { species_name, to_fetch -> species_name } - .set { ch_species } - - // getting GEO accessions given a species name and keywords - // keywords can be an empty string - GEO_GETACCESSIONS( - ch_species, - keywords, - platform ?: 'none', - ch_excluded_accessions_file, - "none" - ) - - GEO_GETACCESSIONS.out.accessions - .splitText() - .set { ch_fetched_accessions } - - } - - // ------------------------------------------------------------------------------------ - // PREPARE ACCESSIONS PROVIDED BY THE USER - // ------------------------------------------------------------------------------------ - - ch_geo_accessions_file = geo_accessions_file ? Channel.fromPath(geo_accessions_file, checkIfExists: true) : Channel.empty() - - Channel.fromList( geo_accessions.tokenize(',') ) - .mix( ch_geo_accessions_file.splitText() ) - .mix( ch_fetched_accessions ) - .unique() - .filter { acc -> acc.startsWith('GSE') } - .map { acc -> acc.trim() } - .set { ch_accessions } - - // ------------------------------------------------------------------------------------ - // DOWNLOAD GEO DATASETS - // ------------------------------------------------------------------------------------ - - if ( !accessions_only ) { - - // Downloading GEO datasets for each accession in ch_accessions - GEO_GETDATA( - ch_accessions, - species - ) - - // adding dataset id (accession + data_type) in the file meta - // flattening in case multiple files are returned at once - ch_design = addDatasetIdToMetadata( GEO_GETDATA.out.design.flatten() ) - ch_counts = addDatasetIdToMetadata( GEO_GETDATA.out.counts.flatten() ) - - // adding design files to the meta of their respective count files - ch_datasets = groupFilesByDatasetId( ch_design, ch_counts ) - - // adding normalisation state in the meta - augmentMetadata( ch_datasets ) - - } - - emit: - downloaded_datasets = ch_datasets - -} diff --git a/subworkflows/local/get_public_accessions/main.nf b/subworkflows/local/get_public_accessions/main.nf new file mode 100644 index 00000000..bbc24db3 --- /dev/null +++ b/subworkflows/local/get_public_accessions/main.nf @@ -0,0 +1,142 @@ +include { EXPRESSIONATLAS_GETACCESSIONS as EXPRESSION_ATLAS } from '../../../modules/local/expressionatlas/getaccessions' +include { GEO_GETACCESSIONS as GEO } from '../../../modules/local/geo/getaccessions' + +/* +======================================================================================== + SUBWORKFLOW TO DOWNLOAD EXPRESSIONATLAS ACCESSIONS AND DATASETS +======================================================================================== +*/ + +workflow GET_PUBLIC_ACCESSIONS { + + take: + species + skip_fetch_public_accessions + skip_fetch_eatlas_accessions + skip_fetch_geo_accessions + platform + keywords + ch_accessions + ch_accessions_file + ch_excluded_accessions + ch_excluded_accessions_file + random_sampling_size + random_sampling_seed + outdir + + main: + + ch_fetched_eatlas_accessions = Channel.empty() + ch_fetched_geo_accessions = Channel.empty() + + // ----------------------------------------------------------------- + // GET EATLAS ACCESSIONS + // ----------------------------------------------------------------- + + // fetching Expression Atlas accessions if applicable + if ( !skip_fetch_public_accessions && !skip_fetch_eatlas_accessions ) { + + // getting Expression Atlas accessions given a species name and keywords + // keywords can be an empty string + EXPRESSION_ATLAS( + species, + keywords, + platform?: 'none' + ) + + // removing E-GTEX-* accessions by default because they are too big + // however, contrary to E-PROT- accessions, they can be added by the user + ch_fetched_eatlas_accessions = EXPRESSION_ATLAS.out.accessions + .splitText() + .filter { acc -> !acc.startsWith('E-GTEX-') } + } + + // ------------------------------------------------------------------------------------ + // GET GEO ACCESSIONS + // ------------------------------------------------------------------------------------ + + // fetching GEO accessions if applicable + if ( !skip_fetch_public_accessions && !skip_fetch_geo_accessions ) { + + // all Expression Atlas accessions starting with E-GEOD- are imported from GEO + // we do not want to collect these GEO data if we already get them from Expression Atlas + ch_excluded_eatlas_accessions_file = ch_fetched_eatlas_accessions + .filter { accession -> accession.startsWith("E-GEOD-") } + .map { accession -> accession.replace("E-GEOD-", "GSE") } + .collectFile( + name: 'excluded_geo_accessions.txt', + storeDir: "${outdir}/geo/", + sort: true, + newLine: true + ) + .ifEmpty( [] ) + + // getting GEO accessions given a species name and keywords + // keywords can be an empty string + GEO( + species, + keywords, + platform ?: 'none', + ch_excluded_eatlas_accessions_file + ) + + ch_fetched_geo_accessions = GEO.out.accessions.splitText() + } + + // ----------------------------------------------------------------- + // MERGING AND EXCLUDING UNWANTED ACCESSIONS + // ----------------------------------------------------------------- + + // getting accessions to exclude and preparing in the right format + ch_excluded_accessions = ch_excluded_accessions + .mix( ch_excluded_accessions_file.splitText() ) + .unique() + .map { acc -> acc.trim() } + .toList() + .map { lst -> [lst] } // list of lists : mandatory when combining in the next step + + ch_fetched_public_accessions = ch_fetched_eatlas_accessions + .mix( ch_fetched_geo_accessions ) + .map { acc -> acc.trim() } + .combine ( ch_excluded_accessions ) + .filter { accession, excluded_accessions -> !(accession in excluded_accessions) } + .map { accession, excluded_accessions -> accession } + + // ----------------------------------------------------------------- + // IF NECESSARY, SUBSAMPLE RANDOMLY THE ACCESSIONS + // ----------------------------------------------------------------- + + if ( random_sampling_size != null ) { + if ( random_sampling_seed != null ) { + ch_fetched_public_accessions = ch_fetched_public_accessions.randomSample( random_sampling_size, random_sampling_seed ) + } else { + ch_fetched_public_accessions = ch_fetched_public_accessions.randomSample( random_sampling_size ) + } + } + ch_fetched_public_accessions.view() + // ----------------------------------------------------------------- + // ADDING USER PROVIDED ACCESSIONS + // ----------------------------------------------------------------- + + ch_input_accessions = ch_accessions + .mix( ch_accessions_file.splitText() ) + .unique() + .map { acc -> acc.trim() } + + // appending to accessions provided by the user + // ensures that no accessions is present twice (provided by the user and fetched from E. Atlas) + // removing E-PROT- accessions because they are not supported in subsequent steps + // removing excluded accessions + ch_accessions = ch_input_accessions + .mix( ch_fetched_public_accessions ) + .unique() + .map { acc -> acc.trim() } + .filter { acc -> + (acc.startsWith('E-') || acc.startsWith('GSE')) && !acc.startsWith('E-PROT-') + } + + + emit: + accessions = ch_accessions + +} diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index 77627407..fc8283f2 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -176,15 +176,15 @@ def validateInputParameters(params) { } // if expression atlas accessions are provided, checking that they are well formated - if ( params.eatlas_accessions ) { - params.eatlas_accessions.tokenize(',').each { accession -> - if ( !accession.startsWith('E-') ) { - error('Expression Atlas accession ' + accession + ' is not well formated. All accessions should start with "E-".') + if ( params.accessions ) { + params.accessions.tokenize(',').each { accession -> + if ( !accession.startsWith('E-') || !accession.startsWith('GSE') ) { + error('Accession ' + accession + ' is not well formated. All accessions should start with "E-" or "GSE".') } } } - if ( params.keywords && params.skip_fetch_eatlas_accessions && params.skip_fetch_geo_accessions ) { + if ( params.keywords && ( params.skip_fetch_public_accessions || ( params.skip_fetch_eatlas_accessions && params.skip_fetch_geo_accessions ) ) ) { log.warn "Ignoring keywords as accessions will not be fetched from Expression Atlas or GEO" } @@ -432,21 +432,3 @@ def checkCounts(ch_counts) { } } } - -def geoDatasetsToFetch(ch_nb_downloaded_eatlas_datasets, threshold) { - // display a warning if no datasets are found - def msg = [ - "More than ${threshold} Expression Atlas datasets found. ", - "Will skip fetching GEO dataset accessions" - ].join("\n").trim() - - return ch_nb_downloaded_eatlas_datasets - .map { n -> - if( n >= threshold ) { - log.warn(msg) - return false - } else { - return true - } - } -} diff --git a/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test b/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test index 1e5ad130..2ace20e9 100644 --- a/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test +++ b/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test @@ -13,7 +13,7 @@ nextflow_workflow { params { eatlas_accessions = "E-MTAB-552,E-GEOD-61690 ,E-PROT-138" eatlas_accessions_file = null - exclude_eatlas_accessions_file = null + excluded_eatlas_accessions_file = null keywords = "potato,stress" skip_fetch_eatlas_accessions = false accessions_only = false @@ -43,7 +43,7 @@ nextflow_workflow { params { eatlas_accessions = "E-MTAB-552,E-GEOD-61690 ,E-PROT-138" eatlas_accessions_file = null - exclude_eatlas_accessions_file = null + excluded_eatlas_accessions_file = null keywords = "potato,stress" skip_fetch_eatlas_accessions = false accessions_only = true @@ -72,7 +72,7 @@ nextflow_workflow { params { eatlas_accessions = "" eatlas_accessions_file = null - exclude_eatlas_accessions_file = null + excluded_eatlas_accessions_file = null keywords = "" skip_fetch_eatlas_accessions = false accessions_only = false @@ -101,7 +101,7 @@ nextflow_workflow { params { eatlas_accessions = "E-MTAB-552,E-GEOD-61690 ,E-PROT-138" eatlas_accessions_file = null - exclude_eatlas_accessions_file = null + excluded_eatlas_accessions_file = null keywords = "" skip_fetch_eatlas_accessions = false accessions_only = false diff --git a/tests/subworkflows/local/geo_fetchdata/main.nf.test b/tests/subworkflows/local/geo_fetchdata/main.nf.test index 42625540..4d025ded 100644 --- a/tests/subworkflows/local/geo_fetchdata/main.nf.test +++ b/tests/subworkflows/local/geo_fetchdata/main.nf.test @@ -19,8 +19,8 @@ nextflow_workflow { input[4] = "" // keywords input[5] = "" // geo_accessions input[6] = null // geo_accessions_file - input[7] = "" // exclude_geo_accessions - input[8] = null // exclude_geo_accessions_file + input[7] = "" // excluded_geo_accessions + input[8] = null // excluded_geo_accessions_file input[9] = Channel.empty() // ch_eatlas_excluded_accessions input[10] = Channel.value(1) // ch_nb_downloaded_eatlas_datasets input[11] = 1500 // min_nb_eatlas_datasets_auto_skip_geo @@ -51,8 +51,8 @@ nextflow_workflow { input[4] = "" // keywords input[5] = "" // geo_accessions input[6] = null // geo_accessions_file - input[7] = "" // exclude_geo_accessions - input[8] = null // exclude_geo_accessions_file + input[7] = "" // excluded_geo_accessions + input[8] = null // excluded_geo_accessions_file input[9] = Channel.empty() // ch_eatlas_excluded_accessions input[10] = Channel.value(1) // ch_nb_downloaded_eatlas_datasets input[11] = 1500 // min_nb_eatlas_datasets_auto_skip_geo @@ -84,8 +84,8 @@ nextflow_workflow { input[4] = "leaf" // keywords input[5] = "GSE55951" // geo_accessions input[6] = null // geo_accessions_file - input[7] = "GSE79526" // exclude_geo_accessions - input[8] = file( '$projectDir/tests/test_data/geo/get_accessions/exclude_two_accessions.txt', checkIfExists: true ) // exclude_geo_accessions_file + input[7] = "GSE79526" // excluded_geo_accessions + input[8] = file( '$projectDir/tests/test_data/geo/get_accessions/exclude_two_accessions.txt', checkIfExists: true ) // excluded_geo_accessions_file input[9] = Channel.empty() // ch_eatlas_excluded_accessions input[10] = Channel.value(1) // ch_nb_downloaded_eatlas_datasets input[11] = 1500 // min_nb_eatlas_datasets_auto_skip_geo @@ -117,8 +117,8 @@ nextflow_workflow { input[4] = "" // keywords input[5] = "GSE55951" // geo_accessions input[6] = null // geo_accessions_file - input[7] = "" // exclude_geo_accessions - input[8] = null // exclude_geo_accessions_file + input[7] = "" // excluded_geo_accessions + input[8] = null // excluded_geo_accessions_file input[9] = Channel.empty() // ch_eatlas_excluded_accessions input[10] = Channel.value(10) // ch_nb_downloaded_eatlas_datasets input[11] = 10 // min_nb_eatlas_datasets_auto_skip_geo diff --git a/tests/workflows/stableexpression.nf.test b/tests/workflows/stableexpression.nf.test index d803bb76..46d4df2a 100644 --- a/tests/workflows/stableexpression.nf.test +++ b/tests/workflows/stableexpression.nf.test @@ -173,9 +173,9 @@ nextflow_workflow { params { species = "solanum tuberosum" eatlas_accessions = "E-MTAB-552,E-GEOD-61690" - exclude_eatlas_accessions = "E-MTAB-4251" + excluded_eatlas_accessions = "E-MTAB-4251" eatlas_accessions_file = file( '$projectDir/tests/test_data/misc/accessions_to_include.txt.txt', checkIfExists: true) - exclude_eatlas_accessions_file = file( '$projectDir/tests/test_data/misc/excluded_accessions.txt', checkIfExists: true) + excluded_eatlas_accessions_file = file( '$projectDir/tests/test_data/misc/excluded_accessions.txt', checkIfExists: true) } workflow { """ diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 9ddfa200..bb44f0e0 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -4,8 +4,8 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -include { EXPRESSIONATLAS_FETCHDATA } from '../subworkflows/local/expressionatlas_fetchdata' -include { GEO_FETCHDATA } from '../subworkflows/local/geo_fetchdata' +include { GET_PUBLIC_ACCESSIONS } from '../subworkflows/local/get_public_accessions' +include { DOWNLOAD_PUBLIC_DATASETS } from '../subworkflows/local/download_public_datasets' include { ID_MAPPING } from '../subworkflows/local/idmapping' include { EXPRESSION_NORMALISATION } from '../subworkflows/local/expression_normalisation' include { MERGE_DATA } from '../subworkflows/local/merge_data' @@ -33,6 +33,9 @@ workflow STABLEEXPRESSION { main: + ch_accessions = Channel.empty() + ch_counts = Channel.empty() + ch_versions = Channel.empty() ch_multiqc_files = Channel.empty() @@ -43,38 +46,39 @@ workflow STABLEEXPRESSION { def species = params.species.split(' ').join('_').toLowerCase() // ----------------------------------------------------------------- - // FETCH AND DOWNLOAD EXPRESSION ATLAS DATASETS IF NEEDED - // ----------------------------------------------------------------- - - EXPRESSIONATLAS_FETCHDATA( species ) - EXPRESSIONATLAS_FETCHDATA.out.downloaded_datasets.set { ch_eatlas_downloaded_datasets } - - // ----------------------------------------------------------------- - // FETCH AND DOWNLOAD GEO DATASETS IF NEEDED + // FETCH PUBLIC ACCESSIONS // ----------------------------------------------------------------- - GEO_FETCHDATA ( + GET_PUBLIC_ACCESSIONS( species, + params.skip_fetch_public_accessions, + params.skip_fetch_eatlas_accessions, params.skip_fetch_geo_accessions, - params.accessions_only, params.platform, params.keywords, - params.geo_accessions, - params.geo_accessions_file, - params.exclude_geo_accessions, - params.exclude_geo_accessions_file, - EXPRESSIONATLAS_FETCHDATA.out.accessions, - ch_eatlas_downloaded_datasets.count(), - params.min_nb_eatlas_datasets_auto_skip_geo, + Channel.fromList( params.accessions.tokenize(',') ), + params.accessions_file ? Channel.fromPath(params.accessions_file, checkIfExists: true) : Channel.empty(), + Channel.fromList( params.excluded_accessions.tokenize(',') ), + params.excluded_accessions_file ? Channel.fromPath(params.excluded_accessions_file, checkIfExists: true) : Channel.empty(), + params.random_sampling_size, + params.random_sampling_seed, params.outdir - ) + ) + ch_accessions = GET_PUBLIC_ACCESSIONS.out.accessions + // ----------------------------------------------------------------- + // DOWNLOAD GEO DATASETS IF NEEDED + // ----------------------------------------------------------------- - // putting all datasets together (local datasets + Expression Atlas datasets) - ch_input_datasets - .concat( ch_eatlas_downloaded_datasets ) - .concat( GEO_FETCHDATA.out.downloaded_datasets ) - .set { ch_counts } + if ( !params.accessions_only) { + + DOWNLOAD_PUBLIC_DATASETS ( + species, + ch_accessions + ) + ch_counts = DOWNLOAD_PUBLIC_DATASETS.out.datasets + + } // store nb of genes and nb f samples at this stage in the meta maps ch_counts = storeDatasetSize( ch_counts, "nb_genes", "nb_samples" ) From 11d36d4eb044016357e9a68b51b3cd2e8cbc95a7 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 24 Nov 2025 16:39:42 +0100 Subject: [PATCH 196/258] replace request to NCBI taxonomy to get species taxid in download_latest_ensembl_annotation.py; keep call to NCBI API in case of REST ENSEMBL API failure --- bin/download_latest_ensembl_annotation.py | 43 ++++++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/bin/download_latest_ensembl_annotation.py b/bin/download_latest_ensembl_annotation.py index 7d51485b..cce6c48f 100755 --- a/bin/download_latest_ensembl_annotation.py +++ b/bin/download_latest_ensembl_annotation.py @@ -25,12 +25,13 @@ GENE_IDS_CHUNKSIZE = 50 # max allowed by Ensembl REST API ENSEMBL_REST_SERVER = "https://rest.ensembl.org/" -SPECIES_INFO_EXT = "info/genomes/taxonomy/{species}" +SPECIES_INFO_BASE_ENDPOINT = "info/genomes/taxonomy/{species}" +TAXONOMY_NAME_ENDPOINT = "taxonomy/name/{species}" ENSEMBL_API_HEADERS = { "Content-Type": "application/json", "Accept": "application/json", } -STOP_RETRY_AFTER_DELAY = 600 +STOP_RETRY_AFTER_DELAY = 120 NCBI_TAXONOMY_API_URL = "https://api.ncbi.nlm.nih.gov/datasets/v2/taxonomy" NCBI_API_HEADERS = {"accept": "application/json", "content-type": "application/json"} @@ -91,6 +92,7 @@ def parse_page_data(url: str) -> BeautifulSoup: before_sleep=before_sleep_log(logger, logging.WARNING), ) def send_request_to_ncbi_taxonomy(taxid: str | int): + logger.info(f"Sending POST request to {NCBI_TAXONOMY_API_URL}") taxons = [str(taxid)] data = {"taxons": taxons} response = requests.post(NCBI_TAXONOMY_API_URL, headers=NCBI_API_HEADERS, json=data) @@ -136,6 +138,34 @@ def download_file(url: str, output_path: str): def get_species_taxid(species: str) -> int: + try: + return get_species_taxid_from_ensembl(species) + except Exception as e: + logger.error( + f"Could not get species taxid for species {species} using the Ensembl REST API: {e}.\nTrying NCBI taxonomy." + ) + ncbi_formated_species_name = format_species_name_for_ncbi_taxonomy(species) + return get_species_taxid_from_ncbi(ncbi_formated_species_name) + + +def get_species_taxid_from_ensembl(species: str) -> int: + url = ENSEMBL_REST_SERVER + TAXONOMY_NAME_ENDPOINT.format(species=species) + data = send_get_request_to_ensembl(url) + if len(data) == 0: + raise ValueError(f"No species found for species {species}") + elif len(data) > 1: + logger.warning( + f"Multiple species found for species {species}. Keeping the first one." + ) + species_data = data[0] + if "id" not in species_data: + raise ValueError( + f"Could not find taxid for species {species}. Data collected: {species_data}" + ) + return species_data["id"] + + +def get_species_taxid_from_ncbi(species: str) -> int: result = send_request_to_ncbi_taxonomy(species) if len(result["taxonomy_nodes"]) > 1: raise ValueError(f"Multiple taxids for species {species}") @@ -146,7 +176,9 @@ def get_species_taxid(species: str) -> int: def get_species_division(species_taxid: int) -> str: - url = ENSEMBL_REST_SERVER + SPECIES_INFO_EXT.format(species=str(species_taxid)) + url = ENSEMBL_REST_SERVER + SPECIES_INFO_BASE_ENDPOINT.format( + species=str(species_taxid) + ) data = send_get_request_to_ensembl(url) if len(data) == 0: raise ValueError(f"No division found for species Taxon ID {species_taxid}") @@ -158,9 +190,10 @@ def get_species_division(species_taxid: int) -> str: def get_species_category(species: str) -> str: - ncbi_formated_species_name = format_species_name_for_ncbi_taxonomy(species) - species_taxid = get_species_taxid(ncbi_formated_species_name) + species_taxid = get_species_taxid(species) + logger.info(f"Got species taxid: {species_taxid}") division = get_species_division(species_taxid) + logger.info(f"Got division: {division}") return ENSEMBL_DIVISION_TO_FOLDER[division] From 583af6a6e681838e22b40d04d01a094f31094293 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 24 Nov 2025 16:53:48 +0100 Subject: [PATCH 197/258] improved logging of warning and failures in multiqc --- bin/compute_cpm.py | 24 ++++++++++--- bin/compute_tpm.py | 33 ++++++++++++------ bin/rename_gene_ids.py | 4 +++ conf/modules/public_data.config | 34 ++++++++++--------- .../local/normalisation/compute_cpm/main.nf | 2 ++ .../local/normalisation/compute_tpm/main.nf | 2 ++ .../local/get_public_accessions/main.nf | 9 +++-- subworkflows/local/multiqc/main.nf | 19 ++++++----- 8 files changed, 82 insertions(+), 45 deletions(-) diff --git a/bin/compute_cpm.py b/bin/compute_cpm.py index 635da0b2..2b97237b 100755 --- a/bin/compute_cpm.py +++ b/bin/compute_cpm.py @@ -4,6 +4,7 @@ import argparse import logging +import sys from pathlib import Path import config @@ -15,6 +16,9 @@ CPM_NORM_SUFFIX = ".cpm.csv" +WARNING_REASON_FILE = "warning_reason.txt" +FAILURE_REASON_FILE = "failure_reason.txt" + ##################################################### ##################################################### @@ -80,14 +84,24 @@ def main(): args = parse_args() logger.info("Parsing data") - count_df = parse_counts(args.count_file) - count_df.index.name = config.GENE_ID_COLNAME - logger.info(f"Normalising {args.count_file.name}") + try: + count_df = parse_counts(args.count_file) + count_df.index.name = config.GENE_ID_COLNAME + + logger.info(f"Normalising {args.count_file.name}") + + count_df = calculate_cpm(count_df) - count_df = calculate_cpm(count_df) + export_normalised_data(count_df, args.count_file) - export_normalised_data(count_df, args.count_file) + except Exception as e: + logger.error(f"Error occurred while normalising data: {e}") + msg = "UNEXPECTED ERROR" + logger.error(msg) + with open(FAILURE_REASON_FILE, "w") as f: + f.write(msg) + sys.exit(0) if __name__ == "__main__": diff --git a/bin/compute_tpm.py b/bin/compute_tpm.py index 678a6c49..7139e714 100755 --- a/bin/compute_tpm.py +++ b/bin/compute_tpm.py @@ -4,6 +4,7 @@ import argparse import logging +import sys from pathlib import Path import config @@ -15,6 +16,9 @@ TPM_NORM_SUFFIX = ".tpm.csv" +WARNING_REASON_FILE = "warning_reason.txt" +FAILURE_REASON_FILE = "failure_reason.txt" + ##################################################### ##################################################### @@ -114,20 +118,29 @@ def export_normalised_data(count_df: pd.DataFrame, count_file: Path): def main(): args = parse_args() - logger.info("Parsing data") - count_df = parse_counts(args.count_file) - count_df.index.name = config.GENE_ID_COLNAME + try: + logger.info("Parsing data") + count_df = parse_counts(args.count_file) + count_df.index.name = config.GENE_ID_COLNAME + + cdna_length_df = pd.read_csv(args.gene_lengths_file, header=0) + + logger.info("Reordering gene lengths") + cdna_length_series = reorder_gene_lengths(count_df, cdna_length_df) - cdna_length_df = pd.read_csv(args.gene_lengths_file, header=0) + logger.info(f"Normalising {args.count_file.name}") - logger.info("Reordering gene lengths") - cdna_length_series = reorder_gene_lengths(count_df, cdna_length_df) + count_df = compute_tpm(count_df, cdna_length_series) - logger.info(f"Normalising {args.count_file.name}") + export_normalised_data(count_df, args.count_file) - count_df = compute_tpm(count_df, cdna_length_series) - print(count_df) - export_normalised_data(count_df, args.count_file) + except Exception as e: + logger.error(f"Error occurred while normalising data: {e}") + msg = "UNEXPECTED ERROR" + logger.error(msg) + with open(FAILURE_REASON_FILE, "w") as f: + f.write(msg) + sys.exit(0) if __name__ == "__main__": diff --git a/bin/rename_gene_ids.py b/bin/rename_gene_ids.py index 43fc83c4..17a31e63 100755 --- a/bin/rename_gene_ids.py +++ b/bin/rename_gene_ids.py @@ -112,6 +112,7 @@ def main(): else: logger.info(f"All genes were mapped ({len(df)} out of {original_nb_genes})") + logger.info("Renaming gene names") # renaming gene names to mapped ids using mapping dict df.index = df.index.map(mapping_dict) df.reset_index(inplace=True) @@ -127,6 +128,7 @@ def main(): # handling cases where multiple genes have the same Gene ID # since subsequent steps in the pipeline require integer values, # we need to ensure that the resulting DataFrame has integer values + logger.info("Computing mean counts for genes with duplicate IDs") df = df.groupby(config.GENE_ID_COLNAME, as_index=False, sort=False).agg( lambda x: x.mean().astype(int) ) @@ -135,6 +137,8 @@ def main(): # WRITING OUTFILES ############################################################# # writing to output file + + logger.info("Writing output file") outfile = args.count_file.with_name(args.count_file.stem + RENAMED_FILE_SUFFIX) df.to_csv(outfile, index=False, header=True) diff --git a/conf/modules/public_data.config b/conf/modules/public_data.config index fb8b521b..b54271bc 100644 --- a/conf/modules/public_data.config +++ b/conf/modules/public_data.config @@ -1,31 +1,33 @@ process { - withName: 'STABLEEXPRESSION:GET_PUBLIC_ACCESSIONS:EXPRESSION_ATLAS' { + withName: EXPRESSIONATLAS_GETACCESSIONS { publishDir = [ - path: { "${params.outdir}/accessions/expression_atlas/" }, + path: { "${params.outdir}/expression_atlas/accessions/" }, mode: params.publish_dir_mode ] } - withName: 'STABLEEXPRESSION:GET_PUBLIC_ACCESSIONS:GEO' { - publishDir = [ - path: { "${params.outdir}/accessions/geo/" }, - mode: params.publish_dir_mode - ] - } + withName: EXPRESSIONATLAS_GETDATA { - withName: 'STABLEEXPRESSION:DOWNLOAD_PUBLIC_DATASETS:EXPRESSION_ATLAS' { publishDir = [ - path: { "${params.outdir}/downloaded/expression_atlas/" }, + path: { "${params.outdir}/expression_atlas/datasets/" }, mode: params.publish_dir_mode ] - } - withName: 'STABLEEXPRESSION:DOWNLOAD_PUBLIC_DATASETS:GEO' { - publishDir = [ - path: { "${params.outdir}/downloaded/geo/" }, - mode: params.publish_dir_mode - ] } + withName: GEO_GETACCESSIONS { + publishDir = [ + path: { "${params.outdir}/geo/accessions/" }, + mode: params.publish_dir_mode + ] + } + + withName: GEO_GETDATA { + publishDir = [ + path: { "${params.outdir}/geo/datasets/" }, + mode: params.publish_dir_mode + ] + } + } diff --git a/modules/local/normalisation/compute_cpm/main.nf b/modules/local/normalisation/compute_cpm/main.nf index ab54cbfa..c02c69fb 100644 --- a/modules/local/normalisation/compute_cpm/main.nf +++ b/modules/local/normalisation/compute_cpm/main.nf @@ -14,6 +14,8 @@ process NORMALISATION_COMPUTE_CPM { output: tuple val(meta), path('*.cpm.csv'), optional: true, emit: counts + tuple val(meta.dataset), path("failure_reason.txt"), optional: true, topic: normalisation_failure_reason + tuple val(meta.dataset), path("warning_reason.txt"), optional: true, topic: normalisation_warning_reason tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions diff --git a/modules/local/normalisation/compute_tpm/main.nf b/modules/local/normalisation/compute_tpm/main.nf index fb9efbfc..a11700a4 100644 --- a/modules/local/normalisation/compute_tpm/main.nf +++ b/modules/local/normalisation/compute_tpm/main.nf @@ -15,6 +15,8 @@ process NORMALISATION_COMPUTE_TPM { output: tuple val(meta), path('*.tpm.csv'), optional: true, emit: counts + tuple val(meta.dataset), path("failure_reason.txt"), optional: true, topic: normalisation_failure_reason + tuple val(meta.dataset), path("warning_reason.txt"), optional: true, topic: normalisation_warning_reason tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions diff --git a/subworkflows/local/get_public_accessions/main.nf b/subworkflows/local/get_public_accessions/main.nf index bbc24db3..e00a6d19 100644 --- a/subworkflows/local/get_public_accessions/main.nf +++ b/subworkflows/local/get_public_accessions/main.nf @@ -98,6 +98,9 @@ workflow GET_PUBLIC_ACCESSIONS { ch_fetched_public_accessions = ch_fetched_eatlas_accessions .mix( ch_fetched_geo_accessions ) .map { acc -> acc.trim() } + .filter { acc -> + (acc.startsWith('E-') || acc.startsWith('GSE')) && !acc.startsWith('E-PROT-') + } .combine ( ch_excluded_accessions ) .filter { accession, excluded_accessions -> !(accession in excluded_accessions) } .map { accession, excluded_accessions -> accession } @@ -113,7 +116,7 @@ workflow GET_PUBLIC_ACCESSIONS { ch_fetched_public_accessions = ch_fetched_public_accessions.randomSample( random_sampling_size ) } } - ch_fetched_public_accessions.view() + // ----------------------------------------------------------------- // ADDING USER PROVIDED ACCESSIONS // ----------------------------------------------------------------- @@ -131,10 +134,6 @@ workflow GET_PUBLIC_ACCESSIONS { .mix( ch_fetched_public_accessions ) .unique() .map { acc -> acc.trim() } - .filter { acc -> - (acc.startsWith('E-') || acc.startsWith('GSE')) && !acc.startsWith('E-PROT-') - } - emit: accessions = ch_accessions diff --git a/subworkflows/local/multiqc/main.nf b/subworkflows/local/multiqc/main.nf index 8ae90061..41fbec8b 100644 --- a/subworkflows/local/multiqc/main.nf +++ b/subworkflows/local/multiqc/main.nf @@ -82,7 +82,7 @@ workflow MULTIQC_WORKFLOW { ) { item -> "${item[0]}\t${item[1]}" } - .set { ch_id_mapping_failure_reasons } + .set { ch_id_mapping_warning_reasons } Channel.topic('renaming_failure_reason') .map { accession, file -> [ accession, file.readLines()[0] ] } @@ -96,29 +96,29 @@ workflow MULTIQC_WORKFLOW { } .set { ch_id_mapping_failure_reasons } - Channel.topic('normalisation_failure_reason') + Channel.topic('normalisation_warning_reason') .map { accession, file -> [ accession, file.readLines()[0] ] } .collectFile( - name: 'normalisation_failure_reasons.tsv', + name: 'normalisation_warning_reasons.tsv', seed: "Dataset\tReason", newLine: true, - storeDir: "${params.outdir}/errors/" + storeDir: "${params.outdir}/warnings/" ) { item -> "${item[0]}\t${item[1]}" } - .set { ch_normalisation_failure_reasons } + .set { ch_normalisation_warning_reasons } - Channel.topic('normalisation_warning_reason') + Channel.topic('normalisation_failure_reason') .map { accession, file -> [ accession, file.readLines()[0] ] } .collectFile( - name: 'normalisation_warning_reasons.tsv', + name: 'normalisation_failure_reasons.tsv', seed: "Dataset\tReason", newLine: true, - storeDir: "${params.outdir}/warnings/" + storeDir: "${params.outdir}/errors/" ) { item -> "${item[0]}\t${item[1]}" } - .set { ch_normalisation_warning_reasons } + .set { ch_normalisation_failure_reasons } // ------------------------------------------------------------------------------------ @@ -135,6 +135,7 @@ workflow MULTIQC_WORKFLOW { .mix( Channel.topic('geo_rejected_datasets').collect() ) .mix( ch_geo_failure_reasons ) .mix( ch_geo_warning_reasons ) + .mix( ch_id_mapping_warning_reasons ) .mix( ch_id_mapping_failure_reasons ) .mix( ch_normalisation_failure_reasons ) .mix( ch_normalisation_warning_reasons ) From c80eba64a557d85bf77cf6c19fc4ab2f9da9b7d6 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 24 Nov 2025 21:07:44 +0100 Subject: [PATCH 198/258] improve speed of rename_gene_ids.py --- bin/rename_gene_ids.py | 37 +++++++++++++-------- modules/local/rename_gene_ids/main.nf | 4 +-- modules/local/rename_gene_ids/spec-file.txt | 30 ++++++++++------- 3 files changed, 44 insertions(+), 27 deletions(-) diff --git a/bin/rename_gene_ids.py b/bin/rename_gene_ids.py index 17a31e63..28e1fd66 100755 --- a/bin/rename_gene_ids.py +++ b/bin/rename_gene_ids.py @@ -9,6 +9,7 @@ import config import pandas as pd +import polars as pl logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -52,6 +53,15 @@ def parse_table(file: Path, **kwargs): return pd.read_csv(file, header=0, sep="\t", **kwargs) +def parse_count_table(file: Path): + # transitting to pandas dataframe helps to avoid parsing errors + df = parse_table(file, index_col=0) + # whatever the name of the first col, rename it to "gene_id" + df.index.rename(config.GENE_ID_COLNAME, inplace=True) + df.index = df.index.astype(str) + return pl.from_pandas(df.reset_index()) + + ################################################################## # MAIN ################################################################## @@ -66,19 +76,15 @@ def main(): # PARSING FILES ############################################################# - # whatever the name of the first col, rename it to "gene_id" - df = parse_table(args.count_file, index_col=0) - df.index.rename(config.GENE_ID_COLNAME, inplace=True) + df = parse_count_table(args.count_file) - if df.empty: + if df.is_empty(): msg = "COUNT FILE IS EMPTY" logger.warning(msg) with open(FAILURE_REASON_FILE, "w") as f: f.write(msg) sys.exit(0) - df.index = df.index.astype(str) - ############################################################# # GETTING MAPPINGS ############################################################# @@ -96,8 +102,10 @@ def main(): # filtering the DataFrame to keep only the rows where the index can be mapped original_nb_genes = len(df) - df = df.loc[df.index.isin(mapping_dict)] - if df.empty: + # df = df.loc[df.index.isin(mapping_dict)] + df = df.filter(pl.col(config.GENE_ID_COLNAME).is_in(mapping_dict.keys())) + + if df.is_empty(): msg = "NO GENES WERE MAPPED" logger.error(msg) with open(FAILURE_REASON_FILE, "w") as f: @@ -114,8 +122,11 @@ def main(): logger.info("Renaming gene names") # renaming gene names to mapped ids using mapping dict - df.index = df.index.map(mapping_dict) - df.reset_index(inplace=True) + df = df.with_columns( + pl.col(config.GENE_ID_COLNAME) + .replace(mapping_dict) + .alias(config.GENE_ID_COLNAME) + ) # TODO: check is there is another way to avoid duplicate gene names # sometimes different gene names have the same Gene ID @@ -129,8 +140,8 @@ def main(): # since subsequent steps in the pipeline require integer values, # we need to ensure that the resulting DataFrame has integer values logger.info("Computing mean counts for genes with duplicate IDs") - df = df.groupby(config.GENE_ID_COLNAME, as_index=False, sort=False).agg( - lambda x: x.mean().astype(int) + df = df.group_by(config.GENE_ID_COLNAME, maintain_order=True).agg( + pl.exclude(config.GENE_ID_COLNAME).mean() ) ############################################################# @@ -140,7 +151,7 @@ def main(): logger.info("Writing output file") outfile = args.count_file.with_name(args.count_file.stem + RENAMED_FILE_SUFFIX) - df.to_csv(outfile, index=False, header=True) + df.write_csv(outfile) # making dataframe for mapping (only two columns: original and new) mapping_df = ( diff --git a/modules/local/rename_gene_ids/main.nf b/modules/local/rename_gene_ids/main.nf index 6fa7777b..d6ecd06c 100644 --- a/modules/local/rename_gene_ids/main.nf +++ b/modules/local/rename_gene_ids/main.nf @@ -6,8 +6,8 @@ process RENAME_GENE_IDS { conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? - 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/3d/3d7126100b0eb7cb53dfb50291707ea8dda3b9738b76551ab73605d0acbe114b/data': - 'community.wave.seqera.io/library/pandas:2.3.3--5a902bf824a79745' }" + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/c9/c9b43e446f2c3b794644fd4c1c86ab09ba0afafc0c02e3fcdf45509ffc89fc4d/data': + 'community.wave.seqera.io/library/pandas_polars:29ea1468b5490a67' }" input: tuple val(meta), path(count_file) diff --git a/modules/local/rename_gene_ids/spec-file.txt b/modules/local/rename_gene_ids/spec-file.txt index f79332cf..d850c144 100644 --- a/modules/local/rename_gene_ids/spec-file.txt +++ b/modules/local/rename_gene_ids/spec-file.txt @@ -7,32 +7,36 @@ https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda# https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda#c0374badb3a5d4b1372db28d19462c53 https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda#f0991f0f84902f6b6009b4d2350a83aa -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda#280ea6eee9e2ddefde25ff799c4f0363 https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda#5b767048b1b3ee9a954b06f4084f93dc -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda#f627678cf829bd70bccf141a19c3ad3e -https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-bootstrap_ha15bf96_3.conda#3036ca5b895b7f5146c5a25486234a68 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-hcd61629_7.conda#f116940d825ffc9104400f0d7f1a4551 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_7.conda#8621a450add4e231f676646880703f49 -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda#be43915efc66345cccb3c310b6ed0374 -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-1_h4a7cf45_openblas.conda#8b39e1ae950f1b54a3959c58ca2c32b8 -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-1_h0358290_openblas.conda#a670bff9eb7963ea41b4e09a4e4ab608 +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 +https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda#6432cb5d4ac0046c3ac0a8a0f95842f9 +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_104.conda#a6abd2796fc332536735f68ba23f7901 https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda#8b09ae86839581147ef2e5c5e229d164 https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda#35f29eec58405aaf55e01cb470d8c26a -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-1_h47877c9_openblas.conda#dee12a83aa4aca5077ea23c0605de044 https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda#280ea6eee9e2ddefde25ff799c4f0363 +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda#f627678cf829bd70bccf141a19c3ad3e +https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.0-hee844dc_0.conda#729a572a3ebb8c43933b30edcc628ceb https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda#80c07c68d2f6870250959dcc95b209d1 https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda#f0991f0f84902f6b6009b4d2350a83aa https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda#9ee58d5c534af06558933af3c845a780 https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda#86bc20552bf46075e3d92b67f089172d https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a https://conda.anaconda.org/conda-forge/linux-64/python-3.13.9-hc97d973_101_cp313.conda#4780fe896e961722d0623fa91d0d3378 +https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.9-py313hd8ed1ab_101.conda#367133808e89325690562099851529c8 +https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.9-h4df99d1_101.conda#f41e3c1125e292e6bfcea8392a3de3d8 +https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda#aaa2a381ccc56eac91d63b6c1240312f +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-hcd61629_7.conda#f116940d825ffc9104400f0d7f1a4551 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_7.conda#8621a450add4e231f676646880703f49 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda#be43915efc66345cccb3c310b6ed0374 +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-2_h4a7cf45_openblas.conda#6146bf1b7f58113d54614c6ec683c14a +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-2_h0358290_openblas.conda#a84b2b7ed34206d14739fb8d29cd2799 +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-2_h47877c9_openblas.conda#9fb20e74a7436dc94dd39d9a9decddc3 https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.5-py313hf6604e3_0.conda#15f43bcd12c90186e78801fafc53d89b https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda#3339e3b65d58accf4ca4fb8748ab16b3 https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 @@ -40,3 +44,5 @@ https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0. https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.3-py313h08cd8bf_1.conda#9e87d4bda0c2711161d765332fa38781 https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda#bf47878473e5ab9fdb4115735230e191 +https://conda.anaconda.org/conda-forge/linux-64/polars-runtime-32-1.35.2-py310hffdcd12_0.conda#2b90c3aaf73a5b6028b068cf3c76e0b7 +https://conda.anaconda.org/conda-forge/noarch/polars-1.35.2-pyh6a1acc5_0.conda#24e8f78d79881b3c035f89f4b83c565c From 84c9417cc82b36e7bdb09688136eb05b91ac16c6 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Tue, 25 Nov 2025 08:15:22 +0100 Subject: [PATCH 199/258] control nb of cpus in eatlas get accessions --- bin/get_eatlas_accessions.py | 11 +++++++++-- modules/local/expressionatlas/getaccessions/main.nf | 10 +++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/bin/get_eatlas_accessions.py b/bin/get_eatlas_accessions.py index 63a7368f..c65ed950 100755 --- a/bin/get_eatlas_accessions.py +++ b/bin/get_eatlas_accessions.py @@ -44,6 +44,13 @@ def parse_args(): required=True, help="Search Expression Atlas for this specific species", ) + parser.add_argument( + "--cpus", + dest="nb_cpus", + type=int, + required=True, + help="Number of CPUs to use", + ) parser.add_argument( "--keywords", type=str, @@ -340,13 +347,13 @@ def main(): ) logger.info("Parsing experiments") - with Pool() as pool: + with Pool(processes=args.nb_cpu) as pool: results = pool.map(parse_experiment, species_experiments) if keywords: logger.info(f"Filtering experiments with keywords {keywords}") func = partial(filter_experiment_with_keywords, keywords=keywords) - with Pool() as pool: + with Pool(processes=args.nb_cpu) as pool: results = [res for res in pool.map(func, results) if res is not None] if results: diff --git a/modules/local/expressionatlas/getaccessions/main.nf b/modules/local/expressionatlas/getaccessions/main.nf index 804c44c9..430b3a2e 100644 --- a/modules/local/expressionatlas/getaccessions/main.nf +++ b/modules/local/expressionatlas/getaccessions/main.nf @@ -1,6 +1,6 @@ process EXPRESSIONATLAS_GETACCESSIONS { - label 'process_medium' + label 'process_high_cpus' tag "${species}" @@ -35,9 +35,13 @@ process EXPRESSIONATLAS_GETACCESSIONS { if ( platform != 'none' ) { args += " --platform $platform" } - // the folder where nltk will download data needs to be writable (necessary for singularity) """ - NLTK_DATA=\${PWD} get_eatlas_accessions.py $args + # the folder where nltk will download data needs to be writable (necessary for singularity) + export NLTK_DATA=\${PWD} + + get_eatlas_accessions.py \\ + $args \\ + --cpus ${task.cpus} """ stub: From 9a97eeedb2f031db844b9265a19945410498736c Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Tue, 25 Nov 2025 08:16:51 +0100 Subject: [PATCH 200/258] reduce and better handle base config for cpus and memory to adapt it to most PCs --- conf/base.config | 23 +++++++++++-------- conf/modules/normalisation.config | 13 ++++++++--- conf/modules/public_data.config | 8 +++---- modules/local/aggregate_results/main.nf | 2 +- modules/local/compute_base_statistics/main.nf | 2 +- .../compute_gene_transcript_lengths/main.nf | 2 +- .../local/compute_stability_scores/main.nf | 2 +- modules/local/dash_app/main.nf | 2 +- .../local/download_ensembl_annotation/main.nf | 2 +- .../local/download_ncbi_annotation/main.nf | 2 +- modules/local/geo/getaccessions/main.nf | 2 +- modules/local/get_candidate_genes/main.nf | 2 +- .../local/normalisation/compute_cpm/main.nf | 2 +- .../local/normalisation/compute_tpm/main.nf | 2 +- modules/local/normfinder/main.nf | 2 +- 15 files changed, 40 insertions(+), 28 deletions(-) diff --git a/conf/base.config b/conf/base.config index 23c50a67..884dc171 100644 --- a/conf/base.config +++ b/conf/base.config @@ -44,19 +44,24 @@ process { time = { 1.h * task.attempt } } withLabel:process_low { - cpus = { 2 } - memory = { 4.GB * task.attempt } - time = { 2.h * task.attempt } + cpus = { 2 } + memory = { 4.GB * task.attempt } + time = { 2.h * task.attempt } } withLabel:process_medium { - cpus = { 6 * task.attempt } - memory = { 10.GB * task.attempt } + cpus = { 4 } + memory = { 8.GB * task.attempt } time = { 4.h * task.attempt } } - withLabel:process_high { - cpus = { 12 * task.attempt } - memory = { 20.GB * task.attempt } - time = { 8.h * task.attempt } + withLabel:process_high_memory { + cpus = { 4 } + memory = { 12.GB * task.attempt } + time = { 8.h * task.attempt } + } + withLabel:process_high_cpus { + cpus = { 8 } + memory = { 8.GB * task.attempt } + time = { 8.h * task.attempt } } withLabel:error_ignore { errorStrategy = 'ignore' diff --git a/conf/modules/normalisation.config b/conf/modules/normalisation.config index b9e4321c..a6a496b0 100644 --- a/conf/modules/normalisation.config +++ b/conf/modules/normalisation.config @@ -1,15 +1,22 @@ process { - withName: 'NORMALISATION_DESEQ2|NORMALISATION_EDGER' { + withName: COMPUTE_CPM { publishDir = [ - path: { "${params.outdir}/normalised/${meta.dataset}/${task.process.tokenize(':')[-1].toLowerCase()}/" }, + path: { "${params.outdir}/normalised/${meta.dataset}/cpm/" }, + mode: params.publish_dir_mode + ] + } + + withName: COMPUTE_TPM { + publishDir = [ + path: { "${params.outdir}/normalised/${meta.dataset}/tpm/" }, mode: params.publish_dir_mode ] } withName: QUANTILE_NORMALISATION { publishDir = [ - path: { "${params.outdir}/quantile_normalised/${meta.dataset}/" }, + path: { "${params.outdir}/normalised/${meta.dataset}/quantile_normalised/" }, mode: params.publish_dir_mode ] } diff --git a/conf/modules/public_data.config b/conf/modules/public_data.config index b54271bc..df10451b 100644 --- a/conf/modules/public_data.config +++ b/conf/modules/public_data.config @@ -2,7 +2,7 @@ process { withName: EXPRESSIONATLAS_GETACCESSIONS { publishDir = [ - path: { "${params.outdir}/expression_atlas/accessions/" }, + path: { "${params.outdir}/public_data/expression_atlas/accessions/" }, mode: params.publish_dir_mode ] } @@ -10,7 +10,7 @@ process { withName: EXPRESSIONATLAS_GETDATA { publishDir = [ - path: { "${params.outdir}/expression_atlas/datasets/" }, + path: { "${params.outdir}/public_data/expression_atlas/datasets/" }, mode: params.publish_dir_mode ] @@ -18,14 +18,14 @@ process { withName: GEO_GETACCESSIONS { publishDir = [ - path: { "${params.outdir}/geo/accessions/" }, + path: { "${params.outdir}/public_data/geo/accessions/" }, mode: params.publish_dir_mode ] } withName: GEO_GETDATA { publishDir = [ - path: { "${params.outdir}/geo/datasets/" }, + path: { "${params.outdir}/public_data/geo/datasets/" }, mode: params.publish_dir_mode ] } diff --git a/modules/local/aggregate_results/main.nf b/modules/local/aggregate_results/main.nf index 80a21270..2df7750f 100644 --- a/modules/local/aggregate_results/main.nf +++ b/modules/local/aggregate_results/main.nf @@ -1,6 +1,6 @@ process AGGREGATE_RESULTS { - label 'process_high' + label 'process_high_memory' conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? diff --git a/modules/local/compute_base_statistics/main.nf b/modules/local/compute_base_statistics/main.nf index 5106ef62..ddf1e708 100644 --- a/modules/local/compute_base_statistics/main.nf +++ b/modules/local/compute_base_statistics/main.nf @@ -1,6 +1,6 @@ process COMPUTE_BASE_STATISTICS { - label 'process_high' + label 'process_high_memory' conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? diff --git a/modules/local/compute_gene_transcript_lengths/main.nf b/modules/local/compute_gene_transcript_lengths/main.nf index 32cf4c7e..975f55e4 100644 --- a/modules/local/compute_gene_transcript_lengths/main.nf +++ b/modules/local/compute_gene_transcript_lengths/main.nf @@ -1,6 +1,6 @@ process COMPUTE_GENE_TRANSCRIPT_LENGTHS { - label 'process_low' + label 'process_single' tag "${gff3.baseName}" diff --git a/modules/local/compute_stability_scores/main.nf b/modules/local/compute_stability_scores/main.nf index 715ddbbe..e0ca71f3 100644 --- a/modules/local/compute_stability_scores/main.nf +++ b/modules/local/compute_stability_scores/main.nf @@ -1,6 +1,6 @@ process COMPUTE_STABILITY_SCORES { - label 'process_high' + label 'process_high_memory' conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? diff --git a/modules/local/dash_app/main.nf b/modules/local/dash_app/main.nf index a00ae851..ef516828 100644 --- a/modules/local/dash_app/main.nf +++ b/modules/local/dash_app/main.nf @@ -1,6 +1,6 @@ process DASH_APP { - label 'process_high' + label 'process_high_memory' conda "${moduleDir}/app/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? diff --git a/modules/local/download_ensembl_annotation/main.nf b/modules/local/download_ensembl_annotation/main.nf index d0ed5d62..4bef236d 100644 --- a/modules/local/download_ensembl_annotation/main.nf +++ b/modules/local/download_ensembl_annotation/main.nf @@ -1,6 +1,6 @@ process DOWNLOAD_ENSEMBL_ANNOTATION { - label 'process_low' + label 'process_single' tag "${species}" diff --git a/modules/local/download_ncbi_annotation/main.nf b/modules/local/download_ncbi_annotation/main.nf index 33b9a525..ad6fdb78 100644 --- a/modules/local/download_ncbi_annotation/main.nf +++ b/modules/local/download_ncbi_annotation/main.nf @@ -1,6 +1,6 @@ process DOWNLOAD_NCBI_ANNOTATION { - label 'process_low' + label 'process_single' tag "${species}" diff --git a/modules/local/geo/getaccessions/main.nf b/modules/local/geo/getaccessions/main.nf index 86c95c37..18ad7af3 100644 --- a/modules/local/geo/getaccessions/main.nf +++ b/modules/local/geo/getaccessions/main.nf @@ -1,6 +1,6 @@ process GEO_GETACCESSIONS { - label 'process_high' + label 'process_high_cpus' tag "${species}" diff --git a/modules/local/get_candidate_genes/main.nf b/modules/local/get_candidate_genes/main.nf index e7e1622d..9f97184d 100644 --- a/modules/local/get_candidate_genes/main.nf +++ b/modules/local/get_candidate_genes/main.nf @@ -1,6 +1,6 @@ process GET_CANDIDATE_GENES { - label 'process_high' + label 'process_high_memory' conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? diff --git a/modules/local/normalisation/compute_cpm/main.nf b/modules/local/normalisation/compute_cpm/main.nf index c02c69fb..fbc9fbe2 100644 --- a/modules/local/normalisation/compute_cpm/main.nf +++ b/modules/local/normalisation/compute_cpm/main.nf @@ -1,6 +1,6 @@ process NORMALISATION_COMPUTE_CPM { - label 'process_low' + label 'process_single' tag "${meta.dataset}" diff --git a/modules/local/normalisation/compute_tpm/main.nf b/modules/local/normalisation/compute_tpm/main.nf index a11700a4..84d1e66b 100644 --- a/modules/local/normalisation/compute_tpm/main.nf +++ b/modules/local/normalisation/compute_tpm/main.nf @@ -1,6 +1,6 @@ process NORMALISATION_COMPUTE_TPM { - label 'process_low' + label 'process_single' tag "${meta.dataset}" diff --git a/modules/local/normfinder/main.nf b/modules/local/normfinder/main.nf index 48094ca4..ff160f8d 100644 --- a/modules/local/normfinder/main.nf +++ b/modules/local/normfinder/main.nf @@ -1,6 +1,6 @@ process NORMFINDER { - label 'process_high' + label 'process_high_memory' conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? From d4e9861339163278ba4a5fdfc0b82cc28a6bb985 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Tue, 25 Nov 2025 08:23:56 +0100 Subject: [PATCH 201/258] fix bug in eatlas get accessions --- bin/get_eatlas_accessions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/get_eatlas_accessions.py b/bin/get_eatlas_accessions.py index c65ed950..0ef98e3f 100755 --- a/bin/get_eatlas_accessions.py +++ b/bin/get_eatlas_accessions.py @@ -347,13 +347,13 @@ def main(): ) logger.info("Parsing experiments") - with Pool(processes=args.nb_cpu) as pool: + with Pool(processes=args.nb_cpus) as pool: results = pool.map(parse_experiment, species_experiments) if keywords: logger.info(f"Filtering experiments with keywords {keywords}") func = partial(filter_experiment_with_keywords, keywords=keywords) - with Pool(processes=args.nb_cpu) as pool: + with Pool(processes=args.nb_cpus) as pool: results = [res for res in pool.map(func, results) if res is not None] if results: From 4729cb54480d46eb6cc79664d4be0f95d9c1d1b3 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Tue, 25 Nov 2025 08:49:43 +0100 Subject: [PATCH 202/258] fix issue with input datasets not being used anymore --- workflows/stableexpression.nf | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index bb44f0e0..d55397e2 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -34,7 +34,7 @@ workflow STABLEEXPRESSION { main: ch_accessions = Channel.empty() - ch_counts = Channel.empty() + ch_downloaded_datasets = Channel.empty() ch_versions = Channel.empty() ch_multiqc_files = Channel.empty() @@ -76,10 +76,12 @@ workflow STABLEEXPRESSION { species, ch_accessions ) - ch_counts = DOWNLOAD_PUBLIC_DATASETS.out.datasets + ch_downloaded_datasets = DOWNLOAD_PUBLIC_DATASETS.out.datasets } + ch_counts = ch_input_datasets.mix( ch_downloaded_datasets ) + // store nb of genes and nb f samples at this stage in the meta maps ch_counts = storeDatasetSize( ch_counts, "nb_genes", "nb_samples" ) From 95a894e3811b94d93dd2b78f4b358f5a56d6a3e0 Mon Sep 17 00:00:00 2001 From: Olivier Date: Tue, 25 Nov 2025 14:14:23 +0100 Subject: [PATCH 203/258] add more checks for input accession format --- .../main.nf | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index fc8283f2..108e97a1 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -168,6 +168,31 @@ workflow PIPELINE_COMPLETION { // Check and validate pipeline parameters // + +def check_accession(accession) { + if ( !( accession.startsWith('E-') || accession.startsWith('GSE') ) ) { + error('Accession ' + accession + ' is not well formated. All accessions should start with "E-" or "GSE".') + } +} + + +def check_accession_string(accessions_str) { + if ( accessions_str != null && accessions_str != "" ) { + accessions_str.tokenize(',').each { accession -> + check_accession(accession) + } + } +} + +def check_accession_file(accession_file) { + if ( accession_file != null ) { + def lines = new File(accession_file).readLines() + lines.each { accession -> + check_accession(accession) + } + } +} + def validateInputParameters(params) { // checking that a species has been provided @@ -175,14 +200,12 @@ def validateInputParameters(params) { error('You must provide a species name') } - // if expression atlas accessions are provided, checking that they are well formated - if ( params.accessions ) { - params.accessions.tokenize(',').each { accession -> - if ( !accession.startsWith('E-') || !accession.startsWith('GSE') ) { - error('Accession ' + accession + ' is not well formated. All accessions should start with "E-" or "GSE".') - } - } - } + // if accessions are provided or excluded, checking that they are well formated + check_accession_string( params.accessions ) + check_accession_string( params.excluded_accessions ) + + check_accession_file( params.accessions_file ) + check_accession_file( params.excluded_accessions_file ) if ( params.keywords && ( params.skip_fetch_public_accessions || ( params.skip_fetch_eatlas_accessions && params.skip_fetch_geo_accessions ) ) ) { log.warn "Ignoring keywords as accessions will not be fetched from Expression Atlas or GEO" From b34612a65fbefcc7fc21cbf6905c6e1898cc0b57 Mon Sep 17 00:00:00 2001 From: Olivier Date: Tue, 25 Nov 2025 20:09:37 +0100 Subject: [PATCH 204/258] improve scalability --- bin/compute_base_statistics.py | 81 +++++++++---------- conf/base.config | 20 ++--- docs/output.md | 8 +- modules/local/compute_base_statistics/main.nf | 6 ++ modules/local/merge_counts/main.nf | 6 +- subworkflows/local/merge_data/main.nf | 7 +- .../main.nf | 25 +++--- 7 files changed, 71 insertions(+), 82 deletions(-) diff --git a/bin/compute_base_statistics.py b/bin/compute_base_statistics.py index cbbdba63..cf83d196 100755 --- a/bin/compute_base_statistics.py +++ b/bin/compute_base_statistics.py @@ -63,28 +63,26 @@ class GeneStatistician: # quantile intervals NB_QUANTILES: ClassVar[int] = 100 - count_lf: pl.LazyFrame + count_df: pl.DataFrame platform: str | None = field(default=None) gene_count_per_sample_df: pl.DataFrame = field(init=False) - stat_lf: pl.LazyFrame = field(init=False) + stat_df: pl.DataFrame = field(init=False) samples: list[str] = field(init=False) samples_with_low_gene_count: list[str] = field(init=False) def __post_init__(self): self.gene_count_per_sample_df = self.get_gene_counts_per_sample() - self.samples = ( - self.count_lf.select(pl.exclude(config.GENE_ID_COLNAME)) - .collect_schema() - .names() - ) + self.samples = [ + col for col in self.count_df.columns if col != config.GENE_ID_COLNAME + ] self.samples_with_low_gene_count = self.get_samples_with_low_gene_count() def get_colname(self, colname: str) -> str: return f"{self.platform}_{colname}" if self.platform else colname - def get_valid_counts(self) -> pl.LazyFrame: - return self.count_lf.select(pl.exclude(config.GENE_ID_COLNAME)) + def get_valid_counts(self) -> pl.DataFrame: + return self.count_df.select(pl.exclude(config.GENE_ID_COLNAME)) def get_gene_counts_per_sample(self) -> pl.DataFrame: """ @@ -95,9 +93,8 @@ def get_gene_counts_per_sample(self) -> pl.DataFrame: - nb_not_nulls: number of non-null values """ return ( - self.count_lf.select(pl.exclude(config.GENE_ID_COLNAME)) + self.count_df.select(pl.exclude(config.GENE_ID_COLNAME)) .count() - .collect() .transpose( include_header=True, header_name="sample", column_names=["count"] ) @@ -117,20 +114,20 @@ def get_samples_with_low_gene_count(self) -> list[str]: .to_list() ) - def get_main_statistics(self) -> pl.LazyFrame: + def get_main_statistics(self) -> pl.DataFrame: """ Compute count descriptive statistics for each gene in the count dataframe. """ logger.info("Getting descriptive statistics") # computing main stats - augmented_count_lf = self.count_lf.with_columns( + augmented_count_df = self.count_df.with_columns( mean=pl.concat_list(self.samples).row.mean(), std=pl.concat_list(self.samples).row.std(), median=pl.concat_list(self.samples).row.median(), mad=pl.concat_list(self.samples).row.mad(), ) - return augmented_count_lf.select( + return augmented_count_df.select( pl.col(config.GENE_ID_COLNAME), pl.col("mean").alias(self.get_colname(config.MEAN_COLNAME)), pl.col("std").alias(self.get_colname(config.STANDARD_DEVIATION_COLNAME)), @@ -152,18 +149,14 @@ def compute_ratios_null_values(self): if sample not in self.samples_with_low_gene_count ] - nb_nulls = ( - self.count_lf.select(pl.exclude(config.GENE_ID_COLNAME).is_null()) - .collect() - .sum_horizontal() - ) - nb_nulls_valid_samples = ( - self.count_lf.select(pl.col(valid_samples).is_null()) - .collect() - .sum_horizontal() - ) + nb_nulls = self.count_df.select( + pl.exclude(config.GENE_ID_COLNAME).is_null() + ).sum_horizontal() + nb_nulls_valid_samples = self.count_df.select( + pl.col(valid_samples).is_null() + ).sum_horizontal() - self.stat_lf = self.stat_lf.with_columns( + self.stat_df = self.stat_df.with_columns( (nb_nulls / len(self.samples)).alias( self.get_colname(config.RATIO_NULLS_COLNAME) ), @@ -173,13 +166,11 @@ def compute_ratios_null_values(self): ) def compute_ratio_zeros(self): - nb_zeros = ( - self.count_lf.select(pl.exclude(config.GENE_ID_COLNAME) == 0) - .collect() - .sum_horizontal() - ) + nb_zeros = self.count_df.select( + pl.exclude(config.GENE_ID_COLNAME) == 0 + ).sum_horizontal() - self.stat_lf = self.stat_lf.with_columns( + self.stat_df = self.stat_df.with_columns( (nb_zeros / len(self.samples)).alias( self.get_colname(config.RATIO_ZEROS_COLNAME) ), @@ -193,7 +184,7 @@ def get_quantile_intervals(self): """ logger.info("Getting cpm quantiles") mean_colname = self.get_colname(config.MEAN_COLNAME) - self.stat_lf = self.stat_lf.with_columns( + self.stat_df = self.stat_df.with_columns( ( pl.col(mean_colname).rank() / pl.col(mean_colname).count() @@ -207,17 +198,17 @@ def get_quantile_intervals(self): .alias(self.get_colname(config.EXPRESSION_LEVEL_QUANTILE_INTERVAL_COLNAME)) ) - def compute_statistics(self) -> pl.LazyFrame: + def compute_statistics(self) -> pl.DataFrame: logger.info("Computing statistics and stability score") # getting expression statistics - self.stat_lf = self.get_main_statistics() + self.stat_df = self.get_main_statistics() # adding column for nb of null values for each gene self.compute_ratios_null_values() # adding a column for the frequency of zero values self.compute_ratio_zeros() # getting quantile intervals self.get_quantile_intervals() - return self.stat_lf + return self.stat_df ##################################################### @@ -238,12 +229,12 @@ def parse_args(): return parser.parse_args() -def get_counts(file: Path) -> pl.LazyFrame: +def get_counts(file: Path) -> pl.DataFrame: # sorting dataframe (necessary to get consistent output) - return pl.scan_parquet(file).sort(config.GENE_ID_COLNAME, descending=False) + return pl.read_parquet(file).sort(config.GENE_ID_COLNAME, descending=False) -def export_data(stat_lf: pl.LazyFrame, platform: str | None): +def export_data(stat_df: pl.DataFrame, platform: str | None): """Export gene expression data to CSV files.""" outfile = ( f"{platform}.{ALL_GENES_RESULT_OUTFILE_SUFFIX}" @@ -251,7 +242,7 @@ def export_data(stat_lf: pl.LazyFrame, platform: str | None): else ALL_GENES_RESULT_OUTFILE_SUFFIX ) logger.info(f"Exporting statistics for all genes to: {outfile}") - stat_lf.collect().write_csv(outfile, float_precision=config.CSV_FLOAT_PRECISION) + stat_df.write_csv(outfile, float_precision=config.CSV_FLOAT_PRECISION) logger.info("Done") @@ -266,14 +257,18 @@ def main(): args = parse_args() # putting all counts into a single dataframe - count_lf = get_counts(args.count_file) + logger.info("Loading count data...") + count_df = get_counts(args.count_file) + logger.info( + f"Loaded count data with {count_df.shape[0]} rows and {count_df.shape[1]} columns" + ) # computing statistics (mean, standard deviation, coefficient of variation, quantiles) - gene_stat = GeneStatistician(count_lf, args.platform) - stat_lf = gene_stat.compute_statistics() + gene_stat = GeneStatistician(count_df, args.platform) + stat_df = gene_stat.compute_statistics() # exporting computed data - export_data(stat_lf, args.platform) + export_data(stat_df, args.platform) if __name__ == "__main__": diff --git a/conf/base.config b/conf/base.config index 884dc171..23879b16 100644 --- a/conf/base.config +++ b/conf/base.config @@ -45,33 +45,23 @@ process { } withLabel:process_low { cpus = { 2 } - memory = { 4.GB * task.attempt } + memory = { 4.GB + 2.GB * task.attempt } time = { 2.h * task.attempt } } withLabel:process_medium { cpus = { 4 } - memory = { 8.GB * task.attempt } + memory = { 8.GB + 4.GB * task.attempt } time = { 4.h * task.attempt } } withLabel:process_high_memory { cpus = { 4 } - memory = { 12.GB * task.attempt } + memory = { 12.GB + 4.GB * task.attempt } time = { 8.h * task.attempt } } withLabel:process_high_cpus { cpus = { 8 } - memory = { 8.GB * task.attempt } + memory = { 8.GB + 4.GB * task.attempt } time = { 8.h * task.attempt } } - withLabel:error_ignore { - errorStrategy = 'ignore' - } - withLabel:error_retry { - errorStrategy = 'retry' - maxRetries = 2 - } - withLabel: process_gpu { - ext.use_gpu = { workflow.profile.contains('gpu') } - accelerator = { workflow.profile.contains('gpu') ? 1 : null } - } + } diff --git a/docs/output.md b/docs/output.md index 47ca0cf1..aff083f2 100644 --- a/docs/output.md +++ b/docs/output.md @@ -88,8 +88,8 @@ and open your browser at `http://localhost:8080`
    Output files -- `expression_atlas/accessions/`: accessions found when querying Expression Atlas -- `expression_atlas/datasets/`: count datasets (normalized: `*.normalised.csv` / raw: `*.raw.csv`) and experimental designs (`*.design.csv`) downloaded from Expression Atlas. +- `public_data/expression_atlas/accessions/`: accessions found when querying Expression Atlas +- `public_data/expression_atlas/datasets/`: count datasets (normalized: `*.normalised.csv` / raw: `*.raw.csv`) and experimental designs (`*.design.csv`) downloaded from Expression Atlas.
    @@ -98,8 +98,8 @@ and open your browser at `http://localhost:8080`
    Output files -- `geo/accessions/`: accessions found when querying GEO -- `geo/datasets/`: count datasets (normalized: `*.normalised.csv` / raw: `*.raw.csv`) and experimental designs (`*.design.csv`) downloaded from GEO. +- `public_data/geo/accessions/`: accessions found when querying GEO +- `public_data/geo/datasets/`: count datasets (normalized: `*.normalised.csv` / raw: `*.raw.csv`) and experimental designs (`*.design.csv`) downloaded from GEO.
    diff --git a/modules/local/compute_base_statistics/main.nf b/modules/local/compute_base_statistics/main.nf index ddf1e708..b088d824 100644 --- a/modules/local/compute_base_statistics/main.nf +++ b/modules/local/compute_base_statistics/main.nf @@ -2,6 +2,12 @@ process COMPUTE_BASE_STATISTICS { label 'process_high_memory' + memory { def calc = (dataset_size / 50000).toInteger() + def result = Math.max(1, calc) // Ensure at least 1 MB + def multiplicator = 1 + 0.2 * task.attempt // increase memory usage with each attempt by 20% + return 1.MB * result * multiplicator + } + conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': diff --git a/modules/local/merge_counts/main.nf b/modules/local/merge_counts/main.nf index 204eda7f..29d5286d 100644 --- a/modules/local/merge_counts/main.nf +++ b/modules/local/merge_counts/main.nf @@ -1,10 +1,8 @@ process MERGE_COUNTS { - label "process_high" + label "process_high_memory" - maxRetries 5 - - memory { def calc = (dataset_size / 10000).toInteger() + memory { def calc = (dataset_size / 50000).toInteger() def result = Math.max(1, calc) // Ensure at least 1 MB def multiplicator = 1 + 0.2 * task.attempt // increase memory usage with each attempt by 20% return 1.MB * result * multiplicator diff --git a/subworkflows/local/merge_data/main.nf b/subworkflows/local/merge_data/main.nf index 486ee5c5..84bb9d34 100644 --- a/subworkflows/local/merge_data/main.nf +++ b/subworkflows/local/merge_data/main.nf @@ -4,7 +4,6 @@ include { MERGE_COUNTS as MERGE_MICROARRAY_COUNTS } from '../../../modules include { getWholeDatasetSize } from '../../../subworkflows/local/utils_nfcore_stableexpression_pipeline' - /* ======================================================================================== SUBWORKFLOW TO DOWNLOAD EXPRESSIONATLAS ACCESSIONS AND DATASETS @@ -59,9 +58,9 @@ workflow MERGE_DATA { .set { ch_platform_counts } ch_whole_rnaseq_size - .mix(ch_whole_microarray_size) - .reduce { rnaseq_size, microarray_size -> rnaseq_size + microarray_size } - .set { ch_whole_size } + .mix(ch_whole_microarray_size) + .reduce { rnaseq_size, microarray_size -> rnaseq_size + microarray_size } + .set { ch_whole_size } MERGE_ALL_COUNTS( ch_platform_counts.collect(), diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index 108e97a1..482e9a71 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -421,18 +421,6 @@ def storeDatasetSize( ch_counts, nb_genes_key, nb_samples_key ) { } } -def getWholeDatasetSize( ch_counts ) { - return ch_counts - .filter { meta, file -> - meta.nb_genes_after_idmapping > 0 && meta.nb_samples_after_idmapping > 0 - } - .map { meta, file -> - meta.nb_genes_after_idmapping * meta.nb_samples_after_idmapping - } - .reduce { size_1, size_2 -> size_1 + size_2 } - .flatten() -} - /* ======================================================================================== @@ -455,3 +443,16 @@ def checkCounts(ch_counts) { } } } + + +def getWholeDatasetSize( ch_counts ) { + return ch_counts + .filter { meta, file -> + meta.nb_genes_after_idmapping > 0 && meta.nb_samples_after_idmapping > 0 + } + .map { meta, file -> + meta.nb_genes_after_idmapping * meta.nb_samples_after_idmapping + } + .reduce { size_1, size_2 -> size_1 + size_2 } + .flatten() +} From 7db7908d97016423b84d7d57ef873f2a119d86b9 Mon Sep 17 00:00:00 2001 From: Olivier Date: Wed, 26 Nov 2025 07:00:07 +0100 Subject: [PATCH 205/258] add process to clean gene IDs before collecting them --- bin/clean_gene_ids.py | 124 +++++++++++++++++++++ modules/local/clean_gene_ids/main.nf | 33 ++++++ modules/local/clean_gene_ids/spec-file.txt | 48 ++++++++ subworkflows/local/idmapping/main.nf | 5 +- 4 files changed, 209 insertions(+), 1 deletion(-) create mode 100755 bin/clean_gene_ids.py create mode 100644 modules/local/clean_gene_ids/main.nf create mode 100644 modules/local/clean_gene_ids/spec-file.txt diff --git a/bin/clean_gene_ids.py b/bin/clean_gene_ids.py new file mode 100755 index 00000000..d3655a16 --- /dev/null +++ b/bin/clean_gene_ids.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 + +# Written by Olivier Coen. Released under the MIT license. + +import argparse +import logging +import sys +from pathlib import Path + +import config +import pandas as pd +import polars as pl + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +################################################################## +# CONSTANTS +################################################################## + +CLEANED_FILE_SUFFIX = ".cleaned.csv" + +FAILURE_REASON_FILE = "failure_reason.txt" + +################################################################## +# FUNCTIONS +################################################################## + + +def parse_args(): + parser = argparse.ArgumentParser("Rename gene IDs using mapped IDs") + parser.add_argument( + "--count-file", type=Path, required=True, help="Input file containing counts" + ) + return parser.parse_args() + + +def parse_table(file: Path, **kwargs): + if file.suffix == ".csv": + return pd.read_csv(file, header=0, **kwargs) + else: # .tsv + return pd.read_csv(file, header=0, sep="\t", **kwargs) + + +def parse_count_table(file: Path): + # transitting to pandas dataframe helps to avoid parsing errors + df = parse_table(file, index_col=0) + # whatever the name of the first col, rename it to "gene_id" + df.index.rename(config.GENE_ID_COLNAME, inplace=True) + df.index = df.index.astype(str) + return pl.from_pandas(df.reset_index()) + + +def clean_ensembl_gene_id_versioning(df: pl.DataFrame): + """ + Clean Ensembl gene IDs by removing version numbers. + Remove the dot and the numbers after it in IDs like ENSG00000000003.17 + """ + return df.with_columns( + pl.when(pl.col(config.GENE_ID_COLNAME).str.starts_with("ENSG")) + .then(pl.col(config.GENE_ID_COLNAME).str.extract(r"^(ENSG\d+)", 1)) + .otherwise(pl.col(config.GENE_ID_COLNAME)) + .alias(config.GENE_ID_COLNAME) + ) + + +def clean_mirna_ids(df: pl.DataFrame): + """ + Clean miRNA IDs by removing the 5p / 3p identifier. + """ + return df.with_columns( + pl.when(pl.col(config.GENE_ID_COLNAME).str.contains(r"-[53]p$")) + .then(pl.col(config.GENE_ID_COLNAME).str.extract(r"^(.*?)-[53]p$")) + .otherwise(pl.col(config.GENE_ID_COLNAME)) + .alias(config.GENE_ID_COLNAME) + ) + + +################################################################## +# MAIN +################################################################## + + +def main(): + args = parse_args() + + logger.info(f"Converting IDs for count file {args.count_file.name}...") + + ############################################################# + # PARSING FILES + ############################################################# + + df = parse_count_table(args.count_file) + + if df.is_empty(): + msg = "COUNT FILE IS EMPTY" + logger.warning(msg) + with open(FAILURE_REASON_FILE, "w") as f: + f.write(msg) + sys.exit(0) + + try: + df = clean_ensembl_gene_id_versioning(df) + df = clean_mirna_ids(df) + except Exception as e: + msg = f"ERROR CLEANING IDS in count file {args.count_file.name}: {e}" + logger.error(msg) + with open(FAILURE_REASON_FILE, "w") as f: + f.write(msg) + sys.exit(0) + + ############################################################# + # WRITING OUTFILE + ############################################################# + # writing to output file + + logger.info("Writing output file") + outfile = args.count_file.with_name(args.count_file.stem + CLEANED_FILE_SUFFIX) + df.write_csv(outfile) + + +if __name__ == "__main__": + main() diff --git a/modules/local/clean_gene_ids/main.nf b/modules/local/clean_gene_ids/main.nf new file mode 100644 index 00000000..45acf711 --- /dev/null +++ b/modules/local/clean_gene_ids/main.nf @@ -0,0 +1,33 @@ +process CLEAN_GENE_IDS { + + label 'process_low' + + tag "${meta.dataset}" + + conda "${moduleDir}/spec-file.txt" + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/c9/c9b43e446f2c3b794644fd4c1c86ab09ba0afafc0c02e3fcdf45509ffc89fc4d/data': + 'community.wave.seqera.io/library/pandas_polars:29ea1468b5490a67' }" + + input: + tuple val(meta), path(count_file) + + output: + tuple val(meta), path('*.cleaned.csv'), optional: true, emit: counts + tuple val(meta.dataset), path("failure_reason.txt"), optional: true, topic: id_cleaning_failure_reason + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions + tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions + + script: + """ + clean_gene_ids.py \\ + --count-file "$count_file" + """ + + + stub: + """ + touch fake.cleaned.csv + """ + +} diff --git a/modules/local/clean_gene_ids/spec-file.txt b/modules/local/clean_gene_ids/spec-file.txt new file mode 100644 index 00000000..d850c144 --- /dev/null +++ b/modules/local/clean_gene_ids/spec-file.txt @@ -0,0 +1,48 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda#f7b4d76975aac7e5d9e6ad13845f92fe +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda#c0374badb3a5d4b1372db28d19462c53 +https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda#5b767048b1b3ee9a954b06f4084f93dc +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 +https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda#6432cb5d4ac0046c3ac0a8a0f95842f9 +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_104.conda#a6abd2796fc332536735f68ba23f7901 +https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda#8b09ae86839581147ef2e5c5e229d164 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda#35f29eec58405aaf55e01cb470d8c26a +https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc +https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda#280ea6eee9e2ddefde25ff799c4f0363 +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda#f627678cf829bd70bccf141a19c3ad3e +https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.0-hee844dc_0.conda#729a572a3ebb8c43933b30edcc628ceb +https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda#80c07c68d2f6870250959dcc95b209d1 +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda#f0991f0f84902f6b6009b4d2350a83aa +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda#9ee58d5c534af06558933af3c845a780 +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 +https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 +https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda#86bc20552bf46075e3d92b67f089172d +https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a +https://conda.anaconda.org/conda-forge/linux-64/python-3.13.9-hc97d973_101_cp313.conda#4780fe896e961722d0623fa91d0d3378 +https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.9-py313hd8ed1ab_101.conda#367133808e89325690562099851529c8 +https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.9-h4df99d1_101.conda#f41e3c1125e292e6bfcea8392a3de3d8 +https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda#aaa2a381ccc56eac91d63b6c1240312f +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-hcd61629_7.conda#f116940d825ffc9104400f0d7f1a4551 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_7.conda#8621a450add4e231f676646880703f49 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda#be43915efc66345cccb3c310b6ed0374 +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-2_h4a7cf45_openblas.conda#6146bf1b7f58113d54614c6ec683c14a +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-2_h0358290_openblas.conda#a84b2b7ed34206d14739fb8d29cd2799 +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-2_h47877c9_openblas.conda#9fb20e74a7436dc94dd39d9a9decddc3 +https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.5-py313hf6604e3_0.conda#15f43bcd12c90186e78801fafc53d89b +https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda#3339e3b65d58accf4ca4fb8748ab16b3 +https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 +https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 +https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 +https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.3-py313h08cd8bf_1.conda#9e87d4bda0c2711161d765332fa38781 +https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda#bf47878473e5ab9fdb4115735230e191 +https://conda.anaconda.org/conda-forge/linux-64/polars-runtime-32-1.35.2-py310hffdcd12_0.conda#2b90c3aaf73a5b6028b068cf3c76e0b7 +https://conda.anaconda.org/conda-forge/noarch/polars-1.35.2-pyh6a1acc5_0.conda#24e8f78d79881b3c035f89f4b83c565c diff --git a/subworkflows/local/idmapping/main.nf b/subworkflows/local/idmapping/main.nf index db0c8bd1..8a99ecdc 100644 --- a/subworkflows/local/idmapping/main.nf +++ b/subworkflows/local/idmapping/main.nf @@ -1,3 +1,4 @@ +include { CLEAN_GENE_IDS } from '../../../modules/local/clean_gene_ids' include { COLLECT_GENE_IDS } from '../../../modules/local/collect_gene_ids' include { GPROFILER_IDMAPPING } from '../../../modules/local/gprofiler/idmapping' include { RENAME_GENE_IDS } from '../../../modules/local/rename_gene_ids' @@ -19,7 +20,6 @@ workflow ID_MAPPING { custom_gene_metadata outdir - main: ch_gene_id_mapping = Channel.empty() @@ -34,6 +34,9 @@ workflow ID_MAPPING { // here we cannot use directly COLLECT_GENE_IDS for runs comprising a huge number of files (eg. human) // so that we proceed by chunks, and perform a final merging step using the Java VM + CLEAN_GENE_IDS ( ch_counts ) + ch_counts = CLEAN_GENE_IDS.out.counts + // TRICK: // the buffer operator creates non-deterministic chunks // which prevents resuming the pipeline From 4fdde27a681de979b5f7f6c59a32c157b24c780d Mon Sep 17 00:00:00 2001 From: Olivier Date: Wed, 26 Nov 2025 07:00:57 +0100 Subject: [PATCH 206/258] improve gene ID logging and QC --- assets/multiqc_config.yml | 78 ++++++++++++++------------- bin/rename_gene_ids.py | 25 ++++++++- modules/local/rename_gene_ids/main.nf | 4 ++ subworkflows/local/multiqc/main.nf | 33 ++++++++++-- 4 files changed, 96 insertions(+), 44 deletions(-) diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index 48af23d9..66980574 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -440,42 +440,6 @@ custom_data: Number of genes included in the dataset after normalisation color: "#bf812d" - skewness: - section_name: "Skewness" - file_format: "csv" - description: | - Distribution of expression skewness across samples. - plot_type: "linegraph" - pconfig: - categories: true - headers: - skewness: - title: "Skewness" - description: | - Skewness of the distribution of the normalised expression - color: "#bf812d" - - uniform_distribution_probabilities: - section_name: "Probabilities of normal / uniform count distribution" - file_format: "csv" - description: | - Pvalue of Kolmogorov-Smirnov test to normal / uniform distribution. - The higher the pvalue, the more likely the distribution is normal / uniform. - If the pvalue < 0.05, the null hypothesis is rejected and the distribution is not normal / uniform. - Samples showing a pvalue lower than the threshold (set in parameters) when not considered for stability scoring. - plot_type: "linegraph" - pconfig: - categories: true - logswitch: true - logswitch_active: true - logswitch_label: "Log10" - headers: - kolmogorov_smirnov_pvalue: - title: "KS test to normal / uniform distribution - pvalue" - description: | - Pvalue of Kolmogorov-Smirnov test to normal / uniform distribution. - color: "#bf812d" - eatlas_selected_experiments_metadata: section_name: "Selected" parent_id: eatlas @@ -575,8 +539,42 @@ custom_data: Warnings during download of GEO datasets plot_type: "table" + id_cleaning_failure_reasons: + section_name: "Gene ID cleaning failure reasons" + parent_id: idmapping + parent_name: "ID mapping" + parent_description: "Information about the ID mapping" + file_format: "tsv" + no_violin: true + description: | + Reasons of failure during gene ID cleaning + plot_type: "table" + + id_mapping_stats: + section_name: "Gene ID mapping statistics" + parent_id: idmapping + parent_name: "ID mapping" + parent_description: "Information about the ID mapping" + file_format: "csv" + pconfig: + sort_samples: true + tt_decimals: 0 + cpswitch: true # show the 'Counts / Percentages' switch + cpswitch_c_active: false # show percentages + stacking: "relative" + description: | + Statistics of gene ID mapping, dataset per dataset + categories: + mapped: + name: "Mapped gene IDs" + color: "#10C995" + unmapped: + name: "Unmapped gene IDs" + color: "#E3224A" + plot_type: "barplot" + renaming_warning_reasons: - section_name: "Warning reasons" + section_name: "Gene renaming warning reasons" parent_id: idmapping parent_name: "ID mapping" parent_description: "Information about the ID mapping" @@ -587,7 +585,7 @@ custom_data: plot_type: "table" renaming_failure_reasons: - section_name: "Failure reasons" + section_name: "Gene renaming failure reasons" parent_id: idmapping parent_name: "ID mapping" parent_description: "Information about the ID mapping" @@ -657,8 +655,12 @@ sp: fn: "*geo_failure_reasons.csv" geo_warning_reasons: fn: "*geo_warning_reasons.csv" + id_cleaning_failure_reasons: + fn: "*id_cleaning_failure_reasons.tsv" renaming_warning_reasons: fn: "*renaming_warning_reasons.tsv" + id_mapping_stats: + fn: "*id_mapping_stats.csv" renaming_failure_reasons: fn: "*renaming_failure_reasons.tsv" normalisation_failure_reasons: diff --git a/bin/rename_gene_ids.py b/bin/rename_gene_ids.py index 28e1fd66..ddb41fc2 100755 --- a/bin/rename_gene_ids.py +++ b/bin/rename_gene_ids.py @@ -26,6 +26,9 @@ WARNING_REASON_FILE = "warning_reason.txt" FAILURE_REASON_FILE = "failure_reason.txt" +MAPPED_FILE_SUFFIX = "mapped.txt" +UNMAPPED_FILE_SUFFIX = "unmapped.txt" + ################################################################## # FUNCTIONS ################################################################## @@ -101,9 +104,18 @@ def main(): # IMPORTANT: KEEPING ONLY GENES THAT HAVE BEEN CONVERTED # filtering the DataFrame to keep only the rows where the index can be mapped original_nb_genes = len(df) + rejected_df = df.filter(~pl.col(config.GENE_ID_COLNAME).is_in(mapping_dict.keys())) + nb_unmapped_genes = len(rejected_df) # df = df.loc[df.index.isin(mapping_dict)] df = df.filter(pl.col(config.GENE_ID_COLNAME).is_in(mapping_dict.keys())) + nb_mapped_genes = len(df) + + with open(MAPPED_FILE_SUFFIX, "w") as f: + f.write(str(nb_mapped_genes)) + + with open(UNMAPPED_FILE_SUFFIX, "w") as f: + f.write(str(nb_unmapped_genes)) if df.is_empty(): msg = "NO GENES WERE MAPPED" @@ -113,12 +125,21 @@ def main(): sys.exit(0) if len(df) < original_nb_genes: - msg = f"Only {len(df) / original_nb_genes:.2%} of genes were mapped ({len(df)} out of {original_nb_genes})" + sample_size = min(5, nb_unmapped_genes) + example_rejected_genes = ( + rejected_df[config.GENE_ID_COLNAME].head(sample_size).to_list() + ) + msg = ( + f"{nb_mapped_genes / original_nb_genes:.2%} of genes were mapped ({nb_mapped_genes} out of {original_nb_genes}). " + + f"Example of unmapped genes: {example_rejected_genes}" + ) logger.warning(msg) with open(WARNING_REASON_FILE, "a") as f: f.write(msg) else: - logger.info(f"All genes were mapped ({len(df)} out of {original_nb_genes})") + logger.info( + f"All genes were mapped ({nb_mapped_genes} out of {original_nb_genes})" + ) logger.info("Renaming gene names") # renaming gene names to mapped ids using mapping dict diff --git a/modules/local/rename_gene_ids/main.nf b/modules/local/rename_gene_ids/main.nf index d6ecd06c..5886029b 100644 --- a/modules/local/rename_gene_ids/main.nf +++ b/modules/local/rename_gene_ids/main.nf @@ -17,6 +17,7 @@ process RENAME_GENE_IDS { tuple val(meta), path('*.renamed.csv'), optional: true, emit: counts tuple val(meta.dataset), path("failure_reason.txt"), optional: true, topic: renaming_failure_reason tuple val(meta.dataset), path("warning_reason.txt"), optional: true, topic: renaming_warning_reason + tuple val(meta.dataset), env("NB_MAPPED"), env("NB_UNMAPPED"), topic: id_mapping_stats tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions @@ -26,6 +27,9 @@ process RENAME_GENE_IDS { rename_gene_ids.py \\ --count-file "$count_file" \\ $mapping_arg + + NB_MAPPED=\$(cat mapped.txt) + NB_UNMAPPED=\$(cat unmapped.txt) """ diff --git a/subworkflows/local/multiqc/main.nf b/subworkflows/local/multiqc/main.nf index 41fbec8b..3fc19568 100644 --- a/subworkflows/local/multiqc/main.nf +++ b/subworkflows/local/multiqc/main.nf @@ -72,8 +72,31 @@ workflow MULTIQC_WORKFLOW { } .set { ch_geo_warning_reasons } + Channel.topic('id_cleaning_failure_reason') + .map { dataset, file -> [ dataset, file.readLines()[0] ] } + .collectFile( + name: 'id_cleaning_failure_reasons.tsv', + seed: "Dataset\tReason", + newLine: true, + storeDir: "${params.outdir}/errors/" + ) { + item -> "${item[0]}\t${item[1]}" + } + .set { ch_id_cleaning_failure_reasons } + + Channel.topic('id_mapping_stats') + .collectFile( + name: 'id_mapping_stats.csv', + seed: "Dataset,Nb mapped,Nb unmapped", + newLine: true, + storeDir: "${params.outdir}/statistics/" + ) { + item -> "${item[0]},${item[1]},${item[2]}" + } + .set { ch_id_mapping_stats } + Channel.topic('renaming_warning_reason') - .map { accession, file -> [ accession, file.readLines()[0] ] } + .map { dataset, file -> [ dataset, file.readLines()[0] ] } .collectFile( name: 'renaming_warning_reasons.tsv', seed: "Dataset\tReason", @@ -85,7 +108,7 @@ workflow MULTIQC_WORKFLOW { .set { ch_id_mapping_warning_reasons } Channel.topic('renaming_failure_reason') - .map { accession, file -> [ accession, file.readLines()[0] ] } + .map { dataset, file -> [ dataset, file.readLines()[0] ] } .collectFile( name: 'renaming_failure_reasons.tsv', seed: "Dataset\tReason", @@ -97,7 +120,7 @@ workflow MULTIQC_WORKFLOW { .set { ch_id_mapping_failure_reasons } Channel.topic('normalisation_warning_reason') - .map { accession, file -> [ accession, file.readLines()[0] ] } + .map { dataset, file -> [ dataset, file.readLines()[0] ] } .collectFile( name: 'normalisation_warning_reasons.tsv', seed: "Dataset\tReason", @@ -109,7 +132,7 @@ workflow MULTIQC_WORKFLOW { .set { ch_normalisation_warning_reasons } Channel.topic('normalisation_failure_reason') - .map { accession, file -> [ accession, file.readLines()[0] ] } + .map { dataset, file -> [ dataset, file.readLines()[0] ] } .collectFile( name: 'normalisation_failure_reasons.tsv', seed: "Dataset\tReason", @@ -135,6 +158,8 @@ workflow MULTIQC_WORKFLOW { .mix( Channel.topic('geo_rejected_datasets').collect() ) .mix( ch_geo_failure_reasons ) .mix( ch_geo_warning_reasons ) + .mix( ch_id_cleaning_failure_reasons ) + .mix( ch_id_mapping_stats ) .mix( ch_id_mapping_warning_reasons ) .mix( ch_id_mapping_failure_reasons ) .mix( ch_normalisation_failure_reasons ) From c5982fc23532c20c0e85295902a032ab6637f836 Mon Sep 17 00:00:00 2001 From: Olivier Date: Wed, 26 Nov 2025 10:14:30 +0100 Subject: [PATCH 207/258] add QC statistics (ratio zeros and skewness) in multiqc --- assets/multiqc_config.yml | 102 ++++++++++-------- bin/collect_gene_ids.py | 2 +- bin/collect_statistics.py | 40 +++++++ bin/get_dataset_statistics.py | 77 ++++--------- modules/local/collect_statistics/main.nf | 24 +++++ .../local/collect_statistics/spec-file.txt | 45 ++++++++ .../main.nf | 18 +--- .../spec-file.txt | 0 subworkflows/local/multiqc/main.nf | 55 +++++++--- workflows/stableexpression.nf | 7 ++ 10 files changed, 243 insertions(+), 127 deletions(-) create mode 100755 bin/collect_statistics.py create mode 100644 modules/local/collect_statistics/main.nf create mode 100644 modules/local/collect_statistics/spec-file.txt rename modules/local/{old/dataset_statistics => compute_dataset_statistics}/main.nf (58%) rename modules/local/{old/dataset_statistics => compute_dataset_statistics}/spec-file.txt (100%) diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index 66980574..2e34b9ff 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -317,6 +317,9 @@ custom_data: gene_statistics: section_name: "Descriptive statistics - All genes" + parent_id: stats + parent_name: "Statistics" + parent_description: "Various statistics at the gene or sample level" file_format: "csv" description: | Distribution of descriptive statistics for all genes. @@ -425,20 +428,60 @@ custom_data: title: "Ratio zero values [Microarray only]" color: "rgb(106, 78, 193)" - gene_counts: - section_name: "Gene counts" + skewness: + section_name: "Count skewness" + parent_id: stats + parent_name: "Statistics" + parent_description: "Various statistics at the gene or sample level" file_format: "csv" + pconfig: + sort_samples: false + #xmin: 0 + #xmax: 1 + xlab: Skewness + ylab: Dataset description: | - Distribution of gene counts across samples. - plot_type: "linegraph" + Distribution of count skewness across samples, displayed dataset per dataset. + plot_type: "boxplot" + + ratio_zeros: + section_name: "Proportion of zeros" + parent_id: stats + parent_name: "Statistics" + parent_description: "Various statistics at the gene or sample level" + file_format: "csv" pconfig: - categories: true - headers: - count: - title: "Gene count" - description: | - Number of genes included in the dataset after normalisation - color: "#bf812d" + sort_samples: false + #xmin: 0 + #xmax: 1 + xlab: Proportion of zeros + ylab: Dataset + description: | + Distribution of zeros across samples, displayed dataset per dataset. + plot_type: "boxplot" + + id_mapping_stats: + section_name: "Gene ID mapping statistics" + parent_id: idmapping + parent_name: "ID mapping" + parent_description: "Information about the ID mapping" + file_format: "csv" + pconfig: + sort_samples: true + tt_decimals: 0 + cpswitch: true # show the 'Counts / Percentages' switch + cpswitch_c_active: false # show percentages + stacking: "relative" + description: | + Statistics of gene ID mapping, dataset per dataset + categories: + mapped: + name: "Mapped gene IDs" + color: "#10C995" + unmapped: + name: "Unmapped gene IDs" + color: "#E3224A" + plot_type: "barplot" eatlas_selected_experiments_metadata: section_name: "Selected" @@ -550,29 +593,6 @@ custom_data: Reasons of failure during gene ID cleaning plot_type: "table" - id_mapping_stats: - section_name: "Gene ID mapping statistics" - parent_id: idmapping - parent_name: "ID mapping" - parent_description: "Information about the ID mapping" - file_format: "csv" - pconfig: - sort_samples: true - tt_decimals: 0 - cpswitch: true # show the 'Counts / Percentages' switch - cpswitch_c_active: false # show percentages - stacking: "relative" - description: | - Statistics of gene ID mapping, dataset per dataset - categories: - mapped: - name: "Mapped gene IDs" - color: "#10C995" - unmapped: - name: "Unmapped gene IDs" - color: "#E3224A" - plot_type: "barplot" - renaming_warning_reasons: section_name: "Gene renaming warning reasons" parent_id: idmapping @@ -581,7 +601,7 @@ custom_data: file_format: "tsv" no_violin: true description: | - Reasons of warning during gene ID renaming + Reasons of warning during gene ID renaming. You can further investigate ID mapping issues on the g:Profiler website at https://biit.cs.ut.ee/gprofiler/convert plot_type: "table" renaming_failure_reasons: @@ -631,12 +651,12 @@ sp: gene_statistics: fn: "*all_genes_summary.csv" max_filesize: 50000000 # 50MB - gene_counts: - fn: "*gene_count_statistics.csv" + id_mapping_stats: + fn: "*id_mapping_stats.csv" skewness: - fn: "*skewness_statistics.csv" - uniform_distribution_probabilities: - fn: "*ks_test_statistics.csv" + fn: "*skewness.transposed.csv" + ratio_zeros: + fn: "*ratio_zeros.transposed.csv" eatlas_selected_experiments_metadata: fn: "*selected_experiments.metadata.tsv" eatlas_all_experiments_metadata: @@ -659,8 +679,6 @@ sp: fn: "*id_cleaning_failure_reasons.tsv" renaming_warning_reasons: fn: "*renaming_warning_reasons.tsv" - id_mapping_stats: - fn: "*id_mapping_stats.csv" renaming_failure_reasons: fn: "*renaming_failure_reasons.tsv" normalisation_failure_reasons: diff --git a/bin/collect_gene_ids.py b/bin/collect_gene_ids.py index 28aa7d66..e6ebaf3a 100755 --- a/bin/collect_gene_ids.py +++ b/bin/collect_gene_ids.py @@ -23,7 +23,7 @@ def parse_args(): - parser = argparse.ArgumentParser(description="Merge count datasets") + parser = argparse.ArgumentParser(description="Collect gene IDs from count files") parser.add_argument( "--counts", type=str, dest="count_files", required=True, help="Count files" ) diff --git a/bin/collect_statistics.py b/bin/collect_statistics.py new file mode 100755 index 00000000..8a185864 --- /dev/null +++ b/bin/collect_statistics.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +# Written by Olivier Coen. Released under the MIT license. + +import logging +import sys +from pathlib import Path + +import pandas as pd + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def main(): + file = Path(sys.argv[1]) + + logger.info("Collecting statistics...") + # parsing file manually because it's not a standard CSV format + with open(file, "r") as f: + lines = f.readlines() + data = [line.strip().split(",") for line in lines] + + # getting max number of columns + max_nb_cols = max(len(row) for row in data) + # fill missing values with None + for row in data: + row += [None] * (max_nb_cols - len(row)) + + df = pd.DataFrame(data) + # the first item is the dataset name + df.set_index(df.columns[0], inplace=True) + + outfile = file.name.replace(".csv", ".transposed.csv") + logger.info(f"Saving statistics to {outfile}") + df.T.to_csv(outfile, index=False, header=True) + + +if __name__ == "__main__": + main() diff --git a/bin/get_dataset_statistics.py b/bin/get_dataset_statistics.py index 07901379..b32fa311 100755 --- a/bin/get_dataset_statistics.py +++ b/bin/get_dataset_statistics.py @@ -6,18 +6,17 @@ import logging from pathlib import Path -import config import pandas as pd -from scipy import stats + +# from scipy import stats logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -QUANT_NORM_SUFFIX = ".quant_norm.parquet" -DATASET_STATISTICS_SUFFIX = ".dataset_stats.csv" +COL_TO_OUTFILE = {"skewness": "skewness.txt", "ratio_zeros": "ratio_zeros.txt"} -ALLOWED_TARGET_DISTRIBUTIONS = ["normal", "uniform"] +# ALLOWED_TARGET_DISTRIBUTIONS = ["normal", "uniform"] ##################################################### @@ -34,58 +33,25 @@ def parse_args(): parser.add_argument( "--counts", type=Path, dest="count_file", required=True, help="Count file" ) - parser.add_argument( - "--output", type=str, dest="outfile_name", required=True, help="Output file" - ) - parser.add_argument( - "--target-distrib", - type=str, - dest="target_distribution", - required=True, - choices=ALLOWED_TARGET_DISTRIBUTIONS, - help="Target distribution to map counts to", - ) return parser.parse_args() -def compute_kolmogorov_smirnov_test_to_target_distribution( - count_df: pd.DataFrame, target_distribution: str -) -> pd.Series: - """Compute Kolmogorov-Smirnov test to target distribution.""" - - if target_distribution == "normal": - cum_distrib_function = stats.norm.cdf - elif target_distribution == "uniform": - cum_distrib_function = stats.uniform.cdf - else: - raise ValueError(f"Unknown target distribution: {target_distribution}") - - ks_tests = pd.Series(index=count_df.columns) - for col in count_df.columns: - ks = stats.ks_1samp(count_df[col], cum_distrib_function, nan_policy="omit") - ks_tests[col] = ks.pvalue - - return ks_tests - - -def compute_dataset_statistics( - count_df: pd.DataFrame, target_distribution: str -) -> pd.DataFrame: - dataset_stats_df = count_df.describe() - dataset_stats_df.loc["skewness"] = count_df.skew() - # for each sample, test distance to target distribution - ks_tests = compute_kolmogorov_smirnov_test_to_target_distribution( - count_df, target_distribution - ) - dataset_stats_df.loc[config.KS_TEST_COLNAME] = ks_tests - return dataset_stats_df.T +def compute_dataset_statistics(count_df: pd.DataFrame) -> pd.DataFrame: + skewness = count_df.skew() + ratio_zeros = (count_df == 0).sum() / len(count_df) + return pd.DataFrame({"skewness": skewness, "ratio_zeros": ratio_zeros}).T -def export_count_data(dataset_stats_df: pd.DataFrame, outfile_name: str): - """Export dataset statistics to CSV files.""" - logger.info(f"Exporting dataset statistics counts to: {outfile_name}") - dataset_stats_df.index.name = config.SAMPLE_COLNAME - dataset_stats_df.to_csv(outfile_name, index=True, header=True) +def export_count_data(dataset_stats_df: pd.DataFrame): + """ + Export dataset statistics to CSV files. + Write each statistic to a separate file, on a single row + """ + for col, outfile_name in COL_TO_OUTFILE.items(): + logger.info(f"Exporting dataset statistics {col} to: {outfile_name}") + pd.DataFrame(dataset_stats_df.loc[col]).T.to_csv( + outfile_name, index=False, header=False, float_format="%.4f" + ) ##################################################### @@ -100,12 +66,11 @@ def main(): count_file = args.count_file logger.info(f"Computing dataset statistics for {count_file.name}") - count_df = pd.read_parquet(count_file) - count_df.set_index(config.GENE_ID_COLNAME, inplace=True) + count_df = pd.read_csv(count_file, index_col=0, header=0) - dataset_stats_df = compute_dataset_statistics(count_df, args.target_distribution) + dataset_stats_df = compute_dataset_statistics(count_df) - export_count_data(dataset_stats_df, args.outfile_name) + export_count_data(dataset_stats_df) if __name__ == "__main__": diff --git a/modules/local/collect_statistics/main.nf b/modules/local/collect_statistics/main.nf new file mode 100644 index 00000000..7de1e85b --- /dev/null +++ b/modules/local/collect_statistics/main.nf @@ -0,0 +1,24 @@ +process COLLECT_STATISTICS { + + tag "${file.baseName}" + label "process_high_memory" + + conda "${moduleDir}/spec-file.txt" + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/60/604657081a64b39e17bb6ad307e545aa6aebf4133b64d6766515c9789bb2d304/data': + 'community.wave.seqera.io/library/pandas_tqdm:2ca37c1047243549' }" + + input: + path file + + output: + path '*.transposed.csv', emit: csv + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions + tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions + + script: + """ + collect_statistics.py $file + """ + +} diff --git a/modules/local/collect_statistics/spec-file.txt b/modules/local/collect_statistics/spec-file.txt new file mode 100644 index 00000000..3635dc57 --- /dev/null +++ b/modules/local/collect_statistics/spec-file.txt @@ -0,0 +1,45 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda#f7b4d76975aac7e5d9e6ad13845f92fe +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda#c0374badb3a5d4b1372db28d19462c53 +https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda#f0991f0f84902f6b6009b4d2350a83aa +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda#5b767048b1b3ee9a954b06f4084f93dc +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 +https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda#6432cb5d4ac0046c3ac0a8a0f95842f9 +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-h1aa0949_0.conda#1450224b3e7d17dfeb985364b77a4d47 +https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda#8b09ae86839581147ef2e5c5e229d164 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda#35f29eec58405aaf55e01cb470d8c26a +https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc +https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda#280ea6eee9e2ddefde25ff799c4f0363 +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda#f627678cf829bd70bccf141a19c3ad3e +https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.0-hee844dc_0.conda#729a572a3ebb8c43933b30edcc628ceb +https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda#80c07c68d2f6870250959dcc95b209d1 +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda#9ee58d5c534af06558933af3c845a780 +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 +https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 +https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda#86bc20552bf46075e3d92b67f089172d +https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a +https://conda.anaconda.org/conda-forge/linux-64/python-3.13.9-hc97d973_101_cp313.conda#4780fe896e961722d0623fa91d0d3378 +https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda#962b9857ee8e7018c22f2776ffa0b2d7 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-hcd61629_7.conda#f116940d825ffc9104400f0d7f1a4551 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_7.conda#8621a450add4e231f676646880703f49 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda#be43915efc66345cccb3c310b6ed0374 +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-1_h4a7cf45_openblas.conda#8b39e1ae950f1b54a3959c58ca2c32b8 +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-1_h0358290_openblas.conda#a670bff9eb7963ea41b4e09a4e4ab608 +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-1_h47877c9_openblas.conda#dee12a83aa4aca5077ea23c0605de044 +https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.5-py313hf6604e3_0.conda#15f43bcd12c90186e78801fafc53d89b +https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda#3339e3b65d58accf4ca4fb8748ab16b3 +https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 +https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 +https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 +https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.3-py313h08cd8bf_1.conda#9e87d4bda0c2711161d765332fa38781 +https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda#bf47878473e5ab9fdb4115735230e191 +https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda#9efbfdc37242619130ea42b1cc4ed861 diff --git a/modules/local/old/dataset_statistics/main.nf b/modules/local/compute_dataset_statistics/main.nf similarity index 58% rename from modules/local/old/dataset_statistics/main.nf rename to modules/local/compute_dataset_statistics/main.nf index e9747155..094d23d4 100644 --- a/modules/local/old/dataset_statistics/main.nf +++ b/modules/local/compute_dataset_statistics/main.nf @@ -1,4 +1,4 @@ -process DATASET_STATISTICS { +process COMPUTE_DATASET_STATISTICS { label 'process_single' @@ -11,28 +11,18 @@ process DATASET_STATISTICS { input: tuple val(meta), path(count_file) - val target_distribution output: - tuple val(meta), path('*.dataset_stats.csv'), emit: stats + tuple val(meta.dataset), path("skewness.txt"), topic: skewness + tuple val(meta.dataset), path("ratio_zeros.txt"), topic: ratio_zeros tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions - tuple val("${task.process}"), val('scipy'), eval('python3 -c "import scipy; print(scipy.__version__)"'), topic: versions - tuple val("${task.process}"), val('pyarrow'), eval('python3 -c "import pyarrow; print(pyarrow.__version__)"'), topic: versions script: def prefix = task.ext.prefix ?: "${meta.dataset}" """ get_dataset_statistics.py \ - --counts $count_file \ - --target-distrib $target_distribution \ - --output ${prefix}.dataset_stats.csv - """ - - - stub: - """ - touch count.cpm.dataset_stats.csv + --counts $count_file """ } diff --git a/modules/local/old/dataset_statistics/spec-file.txt b/modules/local/compute_dataset_statistics/spec-file.txt similarity index 100% rename from modules/local/old/dataset_statistics/spec-file.txt rename to modules/local/compute_dataset_statistics/spec-file.txt diff --git a/subworkflows/local/multiqc/main.nf b/subworkflows/local/multiqc/main.nf index 3fc19568..ebe5ae36 100644 --- a/subworkflows/local/multiqc/main.nf +++ b/subworkflows/local/multiqc/main.nf @@ -1,4 +1,5 @@ include { MULTIQC } from '../../../modules/nf-core/multiqc' +include { COLLECT_STATISTICS } from '../../../modules/local/collect_statistics' include { formatVersionsToYAML } from '../utils_nfcore_stableexpression_pipeline' include { methodsDescriptionText } from '../utils_nfcore_stableexpression_pipeline' @@ -20,6 +21,42 @@ workflow MULTIQC_WORKFLOW { main: + // ------------------------------------------------------------------------------------ + // STATS + // ------------------------------------------------------------------------------------ + + Channel.topic('id_mapping_stats') + .collectFile( + name: 'id_mapping_stats.csv', + seed: "Dataset,Nb mapped,Nb unmapped", + newLine: true, + storeDir: "${params.outdir}/statistics/" + ) { + item -> "${item[0]},${item[1]},${item[2]}" + } + .set { ch_id_mapping_stats } + + Channel.topic('skewness') + .map { dataset, file -> "${dataset},${file.readLines()[0]}" } // concatenate dataset name with skewness values + .collectFile( + name: 'skewness.csv', + newLine: true, + storeDir: "${params.outdir}/statistics/" + ) + .set { ch_skewness } + + Channel.topic('ratio_zeros') + .map { dataset, file -> "${dataset},${file.readLines()[0]}" } // concatenate dataset name with skewness values + .collectFile( + name: 'ratio_zeros.csv', + newLine: true, + storeDir: "${params.outdir}/statistics/" + ) + .set { ch_ratio_zeros } + + ch_to_collect = ch_skewness.mix( ch_ratio_zeros ) + COLLECT_STATISTICS( ch_to_collect ) + // ------------------------------------------------------------------------------------ // FAILURE / WARNING REPORTS // ------------------------------------------------------------------------------------ @@ -84,17 +121,6 @@ workflow MULTIQC_WORKFLOW { } .set { ch_id_cleaning_failure_reasons } - Channel.topic('id_mapping_stats') - .collectFile( - name: 'id_mapping_stats.csv', - seed: "Dataset,Nb mapped,Nb unmapped", - newLine: true, - storeDir: "${params.outdir}/statistics/" - ) { - item -> "${item[0]},${item[1]},${item[2]}" - } - .set { ch_id_mapping_stats } - Channel.topic('renaming_warning_reason') .map { dataset, file -> [ dataset, file.readLines()[0] ] } .collectFile( @@ -151,15 +177,16 @@ workflow MULTIQC_WORKFLOW { ch_multiqc_files .mix( Channel.topic('eatlas_all_datasets').collect() ) .mix( Channel.topic('eatlas_selected_datasets').collect() ) - .mix( ch_eatlas_failure_reasons ) - .mix( ch_eatlas_warning_reasons ) .mix( Channel.topic('geo_all_datasets').collect() ) .mix( Channel.topic('geo_selected_datasets').collect() ) .mix( Channel.topic('geo_rejected_datasets').collect() ) + .mix( COLLECT_STATISTICS.out.csv ) + .mix( ch_id_mapping_stats ) + .mix( ch_eatlas_failure_reasons ) + .mix( ch_eatlas_warning_reasons ) .mix( ch_geo_failure_reasons ) .mix( ch_geo_warning_reasons ) .mix( ch_id_cleaning_failure_reasons ) - .mix( ch_id_mapping_stats ) .mix( ch_id_mapping_warning_reasons ) .mix( ch_id_mapping_failure_reasons ) .mix( ch_normalisation_failure_reasons ) diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index d55397e2..75bf4667 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -13,6 +13,7 @@ include { BASE_STATISTICS } from '../subworkflows/local/b include { STABILITY_SCORING } from '../subworkflows/local/stability_scoring' include { MULTIQC_WORKFLOW } from '../subworkflows/local/multiqc' +include { COMPUTE_DATASET_STATISTICS } from '../modules/local/compute_dataset_statistics' include { AGGREGATE_RESULTS } from '../modules/local/aggregate_results' include { DASH_APP } from '../modules/local/dash_app' @@ -121,6 +122,12 @@ workflow STABLEEXPRESSION { params.quantile_norm_target_distrib ) + // ----------------------------------------------------------------- + // COMPUTE VARIOUS STATISTICS AT THE SAMPLE LEVEL + // ----------------------------------------------------------------- + + COMPUTE_DATASET_STATISTICS ( ch_counts ) + // ----------------------------------------------------------------- // MERGE DATA // ----------------------------------------------------------------- From d0ab387566e9a36181e7579d5cbca2a1ce4ffc78 Mon Sep 17 00:00:00 2001 From: Olivier Date: Wed, 26 Nov 2025 12:22:13 +0100 Subject: [PATCH 208/258] add error handling when no gene ID mapping is possible --- bin/gprofiler_map_ids.py | 6 +++++- modules/local/gprofiler/idmapping/main.nf | 20 ++++++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/bin/gprofiler_map_ids.py b/bin/gprofiler_map_ids.py index c8a78455..e24d63af 100755 --- a/bin/gprofiler_map_ids.py +++ b/bin/gprofiler_map_ids.py @@ -78,10 +78,14 @@ def main(): ) if not mapping_dict: - raise ValueError( + msg = ( f"No mapping found for gene IDs such as {' '.join(gene_ids[:5])} on species {args.species} " + f"and g:Profiler target database {args.gprofiler_target_db}" ) + logger.error(msg) + with open(FAILURE_REASON_FILE, "w") as fout: + fout.write(msg) + sys.exit(100) ############################################################# # WRITING MAPPING diff --git a/modules/local/gprofiler/idmapping/main.nf b/modules/local/gprofiler/idmapping/main.nf index 73f2d523..ee5a8029 100644 --- a/modules/local/gprofiler/idmapping/main.nf +++ b/modules/local/gprofiler/idmapping/main.nf @@ -4,6 +4,21 @@ process GPROFILER_IDMAPPING { tag "${species} IDs to ${gprofiler_target_db}" + errorStrategy = { + if (task.exitStatus == 100 ) { + log.error("Could not map gene IDs to ${gprofiler_target_db} database.") + 'finish' + } else if (task.exitStatus in ((130..145) + 104 + 175) && task.attempt <= 10) { // OOM & related errors; should be retried as long as memory does not fit + sleep(Math.pow(2, task.attempt) * 200 as long) + 'retry' + } else if (task.attempt <= 3) { // all other errors should be retried with exponential backoff with max retry = 3 + sleep(Math.pow(2, task.attempt) * 200 as long) + 'retry' + } else { // after 3 retries, ignore the error + 'finish' + } + } + conda "${moduleDir}/spec-file.txt" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/5c/5c28c8e613c062828aaee4b950029bc90a1a1aa94d5f61016a588c8ec7be8b65/data': @@ -15,14 +30,15 @@ process GPROFILER_IDMAPPING { val gprofiler_target_db output: - path('mapped_gene_ids.csv'), optional: true, emit: mapping - path('gene_metadata.csv'), optional: true, emit: metadata + path('mapped_gene_ids.csv'), emit: mapping + path('gene_metadata.csv'), emit: metadata tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions tuple val("${task.process}"), val('requests'), eval('python3 -c "import requests; print(requests.__version__)"'), topic: versions script: """ + # intercepting exit code 100 gprofiler_map_ids.py \\ --gene-ids $gene_id_file \\ --species "$species" \\ From c9d996cc89652ad31a6798928dc004997610416f Mon Sep 17 00:00:00 2001 From: Olivier Date: Wed, 26 Nov 2025 13:53:48 +0100 Subject: [PATCH 209/258] fix bug in download_geo_data.R when multiple suppl columns are found --- bin/download_geo_data.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/download_geo_data.R b/bin/download_geo_data.R index 53143c7e..9bec3732 100755 --- a/bin/download_geo_data.R +++ b/bin/download_geo_data.R @@ -482,7 +482,7 @@ get_all_rnaseq_counts <- function(platform) { # checking if all files were skipped if (length(count_df_list) == 0) { message("No valid files found") - return(data.frame()) + next } # full outer join From 5b3b35056606cccd518ddc32753b2638cc6a975c Mon Sep 17 00:00:00 2001 From: Olivier Date: Thu, 27 Nov 2025 09:20:08 +0100 Subject: [PATCH 210/258] handle case where there is no library_strategy column in geo metadata in download_geo_data.R --- bin/download_geo_data.R | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bin/download_geo_data.R b/bin/download_geo_data.R index 9bec3732..498e885a 100755 --- a/bin/download_geo_data.R +++ b/bin/download_geo_data.R @@ -198,10 +198,15 @@ get_platform_id <- function(metadata) { ##################################################### get_rnaseq_samples <- function(geo_data, design_df) { + rnaseq_sample_df_list <- list() for (i in 1:length(geo_data)) { data <- geo_data[[ i ]] metadata <- pData(data) + if (!("library_strategy" %in% colnames(metadata))) { + message("library_strategy column not found in metadata") + next + } rnaseq_sample_df_list[[i]] <- metadata %>% filter(library_strategy == "RNA-Seq" & geo_accession %in% design_df$sample) %>% select(geo_accession) @@ -423,7 +428,7 @@ get_raw_counts_from_url <- function(data_url) { tryCatch({ counts <- read.table(filename, header = has_header, sep = separator, row.names = 1) }, error = function(e) { - write_warning(paste("ERROR WHILE PARSING:", filename)) + write_warning(paste("ERROR WHILE PARSING", filename, ":", e)) return(NULL) }) From 7f010f012c045af70696f5b52856f0003f98d43b Mon Sep 17 00:00:00 2001 From: Olivier Date: Thu, 27 Nov 2025 11:00:29 +0100 Subject: [PATCH 211/258] major refactoring of conda/micromamba environments; update some module containers to suit script needs --- ...stics.py => compute_dataset_statistics.py} | 2 - ....py => compute_gene_transcript_lengths.py} | 0 bin/compute_stability_scores.py | 13 -- bin/get_geo_dataset_accessions.py | 14 +- bin/make_cross_join.py | 5 +- conf/base.config | 2 +- .../local/aggregate_results/environment.yml | 7 + modules/local/aggregate_results/main.nf | 2 +- modules/local/aggregate_results/spec-file.txt | 32 --- modules/local/clean_gene_ids/environment.yml | 8 + modules/local/clean_gene_ids/main.nf | 3 +- modules/local/clean_gene_ids/spec-file.txt | 48 ----- .../local/collect_gene_ids/environment.yml | 8 + modules/local/collect_gene_ids/main.nf | 2 +- modules/local/collect_gene_ids/spec-file.txt | 45 ---- .../local/collect_statistics/environment.yml | 7 + modules/local/collect_statistics/main.nf | 6 +- .../local/collect_statistics/spec-file.txt | 45 ---- .../compute_base_statistics/environment.yml | 7 + modules/local/compute_base_statistics/main.nf | 2 +- .../compute_base_statistics/spec-file.txt | 40 ---- .../environment.yml | 7 + .../local/compute_dataset_statistics/main.nf | 8 +- .../compute_dataset_statistics/spec-file.txt | 107 ---------- .../environment.yml | 7 + .../compute_gene_transcript_lengths/main.nf | 4 +- .../spec-file.txt | 42 ---- .../compute_stability_scores/environment.yml | 7 + .../local/compute_stability_scores/main.nf | 6 +- .../compute_stability_scores/spec-file.txt | 44 ---- modules/local/dash_app/app/environment.yml | 14 ++ modules/local/dash_app/app/spec-file.txt | 156 -------------- modules/local/dash_app/main.nf | 2 +- .../environment.yml | 11 + .../local/download_ensembl_annotation/main.nf | 2 +- .../download_ensembl_annotation/spec-file.txt | 64 ------ .../download_ncbi_annotation/environment.yml | 8 + .../local/download_ncbi_annotation/main.nf | 2 +- .../download_ncbi_annotation/spec-file.txt | 57 ----- .../getaccessions/environment.yml | 11 + .../expressionatlas/getaccessions/main.nf | 2 +- .../getaccessions/spec-file.txt | 65 ------ .../expressionatlas/getdata/environment.yml | 9 + modules/local/expressionatlas/getdata/main.nf | 3 +- .../expressionatlas/getdata/spec-file.txt | 174 --------------- .../genorm/compute_m_measure/environment.yml | 7 + .../local/genorm/compute_m_measure/main.nf | 2 +- .../genorm/compute_m_measure/spec-file.txt | 32 --- .../local/genorm/cross_join/environment.yml | 7 + modules/local/genorm/cross_join/main.nf | 2 +- modules/local/genorm/cross_join/spec-file.txt | 32 --- .../genorm/expression_ratio/environment.yml | 7 + modules/local/genorm/expression_ratio/main.nf | 2 +- .../genorm/expression_ratio/spec-file.txt | 32 --- .../local/genorm/make_chunks/environment.yml | 7 + modules/local/genorm/make_chunks/main.nf | 2 +- .../local/genorm/make_chunks/spec-file.txt | 32 --- .../ratio_standard_variation/environment.yml | 7 + .../genorm/ratio_standard_variation/main.nf | 2 +- .../ratio_standard_variation/spec-file.txt | 32 --- .../local/geo/getaccessions/environment.yml | 13 ++ modules/local/geo/getaccessions/main.nf | 5 +- modules/local/geo/getaccessions/spec-file.txt | 74 ------- modules/local/geo/getdata/environment.yml | 10 + modules/local/geo/getdata/main.nf | 2 +- modules/local/geo/getdata/spec-file.txt | 201 ------------------ .../local/get_candidate_genes/environment.yml | 7 + modules/local/get_candidate_genes/main.nf | 2 +- .../local/get_candidate_genes/spec-file.txt | 32 --- .../local/gprofiler/idmapping/environment.yml | 9 + modules/local/gprofiler/idmapping/main.nf | 2 +- .../local/gprofiler/idmapping/spec-file.txt | 57 ----- modules/local/merge_counts/environment.yml | 8 + modules/local/merge_counts/main.nf | 2 +- modules/local/merge_counts/spec-file.txt | 45 ---- .../normalisation/compute_cpm/environment.yml | 7 + .../local/normalisation/compute_cpm/main.nf | 2 +- .../normalisation/compute_cpm/spec-file.txt | 42 ---- .../normalisation/compute_tpm/environment.yml | 7 + .../local/normalisation/compute_tpm/main.nf | 2 +- .../normalisation/compute_tpm/spec-file.txt | 42 ---- modules/local/normfinder/environment.yml | 10 + modules/local/normfinder/main.nf | 2 +- modules/local/normfinder/spec-file.txt | 43 ---- modules/local/old/clean_count_data/main.nf | 2 +- modules/local/old/deseq2/main.nf | 2 +- .../old/download_genome_annotation/main.nf | 2 +- modules/local/old/edger/main.nf | 2 +- .../old/get_annotation_accession/main.nf | 2 +- .../quantile_normalisation/environment.yml | 9 + modules/local/quantile_normalisation/main.nf | 2 +- .../quantile_normalisation/spec-file.txt | 110 ---------- modules/local/rename_gene_ids/environment.yml | 8 + modules/local/rename_gene_ids/main.nf | 3 +- modules/local/rename_gene_ids/spec-file.txt | 48 ----- nextflow.config | 1 + 96 files changed, 297 insertions(+), 1835 deletions(-) rename bin/{get_dataset_statistics.py => compute_dataset_statistics.py} (98%) rename bin/{get_gene_transcript_lengths.py => compute_gene_transcript_lengths.py} (100%) create mode 100644 modules/local/aggregate_results/environment.yml delete mode 100644 modules/local/aggregate_results/spec-file.txt create mode 100644 modules/local/clean_gene_ids/environment.yml delete mode 100644 modules/local/clean_gene_ids/spec-file.txt create mode 100644 modules/local/collect_gene_ids/environment.yml delete mode 100644 modules/local/collect_gene_ids/spec-file.txt create mode 100644 modules/local/collect_statistics/environment.yml delete mode 100644 modules/local/collect_statistics/spec-file.txt create mode 100644 modules/local/compute_base_statistics/environment.yml delete mode 100644 modules/local/compute_base_statistics/spec-file.txt create mode 100644 modules/local/compute_dataset_statistics/environment.yml delete mode 100644 modules/local/compute_dataset_statistics/spec-file.txt create mode 100644 modules/local/compute_gene_transcript_lengths/environment.yml delete mode 100644 modules/local/compute_gene_transcript_lengths/spec-file.txt create mode 100644 modules/local/compute_stability_scores/environment.yml delete mode 100644 modules/local/compute_stability_scores/spec-file.txt create mode 100644 modules/local/dash_app/app/environment.yml delete mode 100644 modules/local/dash_app/app/spec-file.txt create mode 100644 modules/local/download_ensembl_annotation/environment.yml delete mode 100644 modules/local/download_ensembl_annotation/spec-file.txt create mode 100644 modules/local/download_ncbi_annotation/environment.yml delete mode 100644 modules/local/download_ncbi_annotation/spec-file.txt create mode 100644 modules/local/expressionatlas/getaccessions/environment.yml delete mode 100644 modules/local/expressionatlas/getaccessions/spec-file.txt create mode 100644 modules/local/expressionatlas/getdata/environment.yml delete mode 100644 modules/local/expressionatlas/getdata/spec-file.txt create mode 100644 modules/local/genorm/compute_m_measure/environment.yml delete mode 100644 modules/local/genorm/compute_m_measure/spec-file.txt create mode 100644 modules/local/genorm/cross_join/environment.yml delete mode 100644 modules/local/genorm/cross_join/spec-file.txt create mode 100644 modules/local/genorm/expression_ratio/environment.yml delete mode 100644 modules/local/genorm/expression_ratio/spec-file.txt create mode 100644 modules/local/genorm/make_chunks/environment.yml delete mode 100644 modules/local/genorm/make_chunks/spec-file.txt create mode 100644 modules/local/genorm/ratio_standard_variation/environment.yml delete mode 100644 modules/local/genorm/ratio_standard_variation/spec-file.txt create mode 100644 modules/local/geo/getaccessions/environment.yml delete mode 100644 modules/local/geo/getaccessions/spec-file.txt create mode 100644 modules/local/geo/getdata/environment.yml delete mode 100644 modules/local/geo/getdata/spec-file.txt create mode 100644 modules/local/get_candidate_genes/environment.yml delete mode 100644 modules/local/get_candidate_genes/spec-file.txt create mode 100644 modules/local/gprofiler/idmapping/environment.yml delete mode 100644 modules/local/gprofiler/idmapping/spec-file.txt create mode 100644 modules/local/merge_counts/environment.yml delete mode 100644 modules/local/merge_counts/spec-file.txt create mode 100644 modules/local/normalisation/compute_cpm/environment.yml delete mode 100644 modules/local/normalisation/compute_cpm/spec-file.txt create mode 100644 modules/local/normalisation/compute_tpm/environment.yml delete mode 100644 modules/local/normalisation/compute_tpm/spec-file.txt create mode 100644 modules/local/normfinder/environment.yml delete mode 100644 modules/local/normfinder/spec-file.txt create mode 100644 modules/local/quantile_normalisation/environment.yml delete mode 100644 modules/local/quantile_normalisation/spec-file.txt create mode 100644 modules/local/rename_gene_ids/environment.yml delete mode 100644 modules/local/rename_gene_ids/spec-file.txt diff --git a/bin/get_dataset_statistics.py b/bin/compute_dataset_statistics.py similarity index 98% rename from bin/get_dataset_statistics.py rename to bin/compute_dataset_statistics.py index b32fa311..be8217dc 100755 --- a/bin/get_dataset_statistics.py +++ b/bin/compute_dataset_statistics.py @@ -8,8 +8,6 @@ import pandas as pd -# from scipy import stats - logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) diff --git a/bin/get_gene_transcript_lengths.py b/bin/compute_gene_transcript_lengths.py similarity index 100% rename from bin/get_gene_transcript_lengths.py rename to bin/compute_gene_transcript_lengths.py diff --git a/bin/compute_stability_scores.py b/bin/compute_stability_scores.py index 6558967d..0dd75cde 100755 --- a/bin/compute_stability_scores.py +++ b/bin/compute_stability_scores.py @@ -10,7 +10,6 @@ import config import polars as pl -from sklearn.preprocessing import QuantileTransformer logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -46,18 +45,6 @@ def parse_stability_score_weights(self): ): self.weights[weight_field] = float(weight) - """ - @staticmethod - def quantile_normalise(data: pl.Series, new_name: str) -> pl.Series: - ''' - Quantile normalize a series - ''' - array = data.to_numpy().reshape(-1, 1) - transformer = QuantileTransformer(output_distribution="uniform", subsample=None) - normalised_array = transformer.fit_transform(array) - return pl.Series(new_name, normalised_array.ravel()) - """ - def linear_normalise(self, data: pl.Series, new_name: str) -> pl.Series: """ Linearly normalise a series diff --git a/bin/get_geo_dataset_accessions.py b/bin/get_geo_dataset_accessions.py index ee04e57b..31933c49 100755 --- a/bin/get_geo_dataset_accessions.py +++ b/bin/get_geo_dataset_accessions.py @@ -14,7 +14,6 @@ import requests import xmltodict from Bio import Entrez -from gprofiler_utils import chunk_list from natural_language_utils import keywords_in_fields from requests.exceptions import ConnectionError, HTTPError from tenacity import ( @@ -214,6 +213,19 @@ def download_file_at_url(url: str, output_file: Path): # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +def chunk_list(lst: list, chunksize: int) -> list: + """Splits a list into chunks of a given size. + + Args: + lst (list): The list to split. + chunksize (int): The size of each chunk. + + Returns: + list: A list of chunks, where each chunk is a list of len(chunksize). + """ + return [lst[i : i + chunksize] for i in range(0, len(lst), chunksize)] + + def fetch_geo_datasets_for_species(species: str) -> list[dict]: """ Fetch GEO datasets (GSE series) for a given species diff --git a/bin/make_cross_join.py b/bin/make_cross_join.py index 6070a75f..f28a597c 100755 --- a/bin/make_cross_join.py +++ b/bin/make_cross_join.py @@ -2,10 +2,11 @@ # Written by Olivier Coen. Released under the MIT license. -import polars as pl -from pathlib import Path import argparse import logging +from pathlib import Path + +import polars as pl logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) diff --git a/conf/base.config b/conf/base.config index 23879b16..4de37115 100644 --- a/conf/base.config +++ b/conf/base.config @@ -25,7 +25,7 @@ process { sleep(Math.pow(2, task.attempt) * 200 as long) 'retry' } else { // after 3 retries, ignore the error - 'ignore' + 'terminate' } } maxRetries = 10 diff --git a/modules/local/aggregate_results/environment.yml b/modules/local/aggregate_results/environment.yml new file mode 100644 index 00000000..4fa78808 --- /dev/null +++ b/modules/local/aggregate_results/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::polars==1.35.2 diff --git a/modules/local/aggregate_results/main.nf b/modules/local/aggregate_results/main.nf index 2df7750f..e6f493b4 100644 --- a/modules/local/aggregate_results/main.nf +++ b/modules/local/aggregate_results/main.nf @@ -2,7 +2,7 @@ process AGGREGATE_RESULTS { label 'process_high_memory' - conda "${moduleDir}/spec-file.txt" + 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/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" diff --git a/modules/local/aggregate_results/spec-file.txt b/modules/local/aggregate_results/spec-file.txt deleted file mode 100644 index 0ffb80b1..00000000 --- a/modules/local/aggregate_results/spec-file.txt +++ /dev/null @@ -1,32 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_5.conda#dcd5ff1940cd38f6df777cac86819d60 -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_5.conda#264fbfba7fb20acf3b29cde153e345ce -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda#0be7c6e070c19105f966d3758448d018 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda#4211416ecba1866fab0c6470986c22d6 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda#0b367fad34931cb79e0d6b7e5c06bb1c -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.1-he9a06e4_0.conda#af930c65e9a79a3423d6d36e265cef65 -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda#74784ee3d225fc3dca89edb635b4e5cc -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda#ffffb341206dd0dab0c36053c048d621 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.13.7-h2b335a9_100_cp313.conda#724dcf9960e933838247971da07fe5cf -https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.7-py313hd8ed1ab_100.conda#c5623ddbd37c5dafa7754a83f97de01e -https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.7-h4df99d1_100.conda#47a123ca8e727d886a2c6d0c71658f8c -https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda#aaa2a381ccc56eac91d63b6c1240312f -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_5.conda#4e02a49aaa9d5190cb630fa43528fbe6 -https://conda.anaconda.org/conda-forge/noarch/pip-25.2-pyh145f28c_0.conda#e7ab34d5a93e0819b62563c78635d937 -https://conda.anaconda.org/conda-forge/linux-64/polars-default-1.33.1-py39hf521cc8_0.conda#900f486d119d5c83d14c812068a3ecad -https://conda.anaconda.org/conda-forge/linux-64/polars-1.33.1-default_h755bcc6_0.conda#1884a1a6acc457c8e4b59b0f6450e140 diff --git a/modules/local/clean_gene_ids/environment.yml b/modules/local/clean_gene_ids/environment.yml new file mode 100644 index 00000000..d44b366f --- /dev/null +++ b/modules/local/clean_gene_ids/environment.yml @@ -0,0 +1,8 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::pandas==2.3.3 + - conda-forge::polars==1.35.2 diff --git a/modules/local/clean_gene_ids/main.nf b/modules/local/clean_gene_ids/main.nf index 45acf711..b3fc38ad 100644 --- a/modules/local/clean_gene_ids/main.nf +++ b/modules/local/clean_gene_ids/main.nf @@ -4,7 +4,7 @@ process CLEAN_GENE_IDS { tag "${meta.dataset}" - conda "${moduleDir}/spec-file.txt" + 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/c9/c9b43e446f2c3b794644fd4c1c86ab09ba0afafc0c02e3fcdf45509ffc89fc4d/data': 'community.wave.seqera.io/library/pandas_polars:29ea1468b5490a67' }" @@ -17,6 +17,7 @@ process CLEAN_GENE_IDS { tuple val(meta.dataset), path("failure_reason.txt"), optional: true, topic: id_cleaning_failure_reason tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions + tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions script: """ diff --git a/modules/local/clean_gene_ids/spec-file.txt b/modules/local/clean_gene_ids/spec-file.txt deleted file mode 100644 index d850c144..00000000 --- a/modules/local/clean_gene_ids/spec-file.txt +++ /dev/null @@ -1,48 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda#f7b4d76975aac7e5d9e6ad13845f92fe -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda#c0374badb3a5d4b1372db28d19462c53 -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda#5b767048b1b3ee9a954b06f4084f93dc -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda#6432cb5d4ac0046c3ac0a8a0f95842f9 -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_104.conda#a6abd2796fc332536735f68ba23f7901 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda#8b09ae86839581147ef2e5c5e229d164 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda#35f29eec58405aaf55e01cb470d8c26a -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda#280ea6eee9e2ddefde25ff799c4f0363 -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda#f627678cf829bd70bccf141a19c3ad3e -https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.0-hee844dc_0.conda#729a572a3ebb8c43933b30edcc628ceb -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda#80c07c68d2f6870250959dcc95b209d1 -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda#f0991f0f84902f6b6009b4d2350a83aa -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda#9ee58d5c534af06558933af3c845a780 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda#86bc20552bf46075e3d92b67f089172d -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.13.9-hc97d973_101_cp313.conda#4780fe896e961722d0623fa91d0d3378 -https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.9-py313hd8ed1ab_101.conda#367133808e89325690562099851529c8 -https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.9-h4df99d1_101.conda#f41e3c1125e292e6bfcea8392a3de3d8 -https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda#aaa2a381ccc56eac91d63b6c1240312f -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-hcd61629_7.conda#f116940d825ffc9104400f0d7f1a4551 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_7.conda#8621a450add4e231f676646880703f49 -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda#be43915efc66345cccb3c310b6ed0374 -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-2_h4a7cf45_openblas.conda#6146bf1b7f58113d54614c6ec683c14a -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-2_h0358290_openblas.conda#a84b2b7ed34206d14739fb8d29cd2799 -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-2_h47877c9_openblas.conda#9fb20e74a7436dc94dd39d9a9decddc3 -https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.5-py313hf6604e3_0.conda#15f43bcd12c90186e78801fafc53d89b -https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda#3339e3b65d58accf4ca4fb8748ab16b3 -https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 -https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 -https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 -https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.3-py313h08cd8bf_1.conda#9e87d4bda0c2711161d765332fa38781 -https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda#bf47878473e5ab9fdb4115735230e191 -https://conda.anaconda.org/conda-forge/linux-64/polars-runtime-32-1.35.2-py310hffdcd12_0.conda#2b90c3aaf73a5b6028b068cf3c76e0b7 -https://conda.anaconda.org/conda-forge/noarch/polars-1.35.2-pyh6a1acc5_0.conda#24e8f78d79881b3c035f89f4b83c565c diff --git a/modules/local/collect_gene_ids/environment.yml b/modules/local/collect_gene_ids/environment.yml new file mode 100644 index 00000000..fa92ab03 --- /dev/null +++ b/modules/local/collect_gene_ids/environment.yml @@ -0,0 +1,8 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::pandas==2.3.3 + - conda-forge::tqdm==4.67.1 diff --git a/modules/local/collect_gene_ids/main.nf b/modules/local/collect_gene_ids/main.nf index a7cfe5ca..1ce3d1a0 100644 --- a/modules/local/collect_gene_ids/main.nf +++ b/modules/local/collect_gene_ids/main.nf @@ -3,7 +3,7 @@ process COLLECT_GENE_IDS { tag "chunk ${task.index}" label "process_high" - conda "${moduleDir}/spec-file.txt" + 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/60/604657081a64b39e17bb6ad307e545aa6aebf4133b64d6766515c9789bb2d304/data': 'community.wave.seqera.io/library/pandas_tqdm:2ca37c1047243549' }" diff --git a/modules/local/collect_gene_ids/spec-file.txt b/modules/local/collect_gene_ids/spec-file.txt deleted file mode 100644 index 3635dc57..00000000 --- a/modules/local/collect_gene_ids/spec-file.txt +++ /dev/null @@ -1,45 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda#f7b4d76975aac7e5d9e6ad13845f92fe -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda#c0374badb3a5d4b1372db28d19462c53 -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda#f0991f0f84902f6b6009b4d2350a83aa -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda#5b767048b1b3ee9a954b06f4084f93dc -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda#6432cb5d4ac0046c3ac0a8a0f95842f9 -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-h1aa0949_0.conda#1450224b3e7d17dfeb985364b77a4d47 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda#8b09ae86839581147ef2e5c5e229d164 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda#35f29eec58405aaf55e01cb470d8c26a -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda#280ea6eee9e2ddefde25ff799c4f0363 -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda#f627678cf829bd70bccf141a19c3ad3e -https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.0-hee844dc_0.conda#729a572a3ebb8c43933b30edcc628ceb -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda#80c07c68d2f6870250959dcc95b209d1 -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda#9ee58d5c534af06558933af3c845a780 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda#86bc20552bf46075e3d92b67f089172d -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.13.9-hc97d973_101_cp313.conda#4780fe896e961722d0623fa91d0d3378 -https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda#962b9857ee8e7018c22f2776ffa0b2d7 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-hcd61629_7.conda#f116940d825ffc9104400f0d7f1a4551 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_7.conda#8621a450add4e231f676646880703f49 -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda#be43915efc66345cccb3c310b6ed0374 -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-1_h4a7cf45_openblas.conda#8b39e1ae950f1b54a3959c58ca2c32b8 -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-1_h0358290_openblas.conda#a670bff9eb7963ea41b4e09a4e4ab608 -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-1_h47877c9_openblas.conda#dee12a83aa4aca5077ea23c0605de044 -https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.5-py313hf6604e3_0.conda#15f43bcd12c90186e78801fafc53d89b -https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda#3339e3b65d58accf4ca4fb8748ab16b3 -https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 -https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 -https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 -https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.3-py313h08cd8bf_1.conda#9e87d4bda0c2711161d765332fa38781 -https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda#bf47878473e5ab9fdb4115735230e191 -https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda#9efbfdc37242619130ea42b1cc4ed861 diff --git a/modules/local/collect_statistics/environment.yml b/modules/local/collect_statistics/environment.yml new file mode 100644 index 00000000..104de0e2 --- /dev/null +++ b/modules/local/collect_statistics/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::pandas==2.3.3 diff --git a/modules/local/collect_statistics/main.nf b/modules/local/collect_statistics/main.nf index 7de1e85b..923e6973 100644 --- a/modules/local/collect_statistics/main.nf +++ b/modules/local/collect_statistics/main.nf @@ -3,10 +3,10 @@ process COLLECT_STATISTICS { tag "${file.baseName}" label "process_high_memory" - conda "${moduleDir}/spec-file.txt" + 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/60/604657081a64b39e17bb6ad307e545aa6aebf4133b64d6766515c9789bb2d304/data': - 'community.wave.seqera.io/library/pandas_tqdm:2ca37c1047243549' }" + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/3d/3d7126100b0eb7cb53dfb50291707ea8dda3b9738b76551ab73605d0acbe114b/data': + 'community.wave.seqera.io/library/pandas:2.3.3--5a902bf824a79745' }" input: path file diff --git a/modules/local/collect_statistics/spec-file.txt b/modules/local/collect_statistics/spec-file.txt deleted file mode 100644 index 3635dc57..00000000 --- a/modules/local/collect_statistics/spec-file.txt +++ /dev/null @@ -1,45 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda#f7b4d76975aac7e5d9e6ad13845f92fe -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda#c0374badb3a5d4b1372db28d19462c53 -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda#f0991f0f84902f6b6009b4d2350a83aa -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda#5b767048b1b3ee9a954b06f4084f93dc -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda#6432cb5d4ac0046c3ac0a8a0f95842f9 -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-h1aa0949_0.conda#1450224b3e7d17dfeb985364b77a4d47 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda#8b09ae86839581147ef2e5c5e229d164 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda#35f29eec58405aaf55e01cb470d8c26a -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda#280ea6eee9e2ddefde25ff799c4f0363 -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda#f627678cf829bd70bccf141a19c3ad3e -https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.0-hee844dc_0.conda#729a572a3ebb8c43933b30edcc628ceb -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda#80c07c68d2f6870250959dcc95b209d1 -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda#9ee58d5c534af06558933af3c845a780 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda#86bc20552bf46075e3d92b67f089172d -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.13.9-hc97d973_101_cp313.conda#4780fe896e961722d0623fa91d0d3378 -https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda#962b9857ee8e7018c22f2776ffa0b2d7 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-hcd61629_7.conda#f116940d825ffc9104400f0d7f1a4551 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_7.conda#8621a450add4e231f676646880703f49 -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda#be43915efc66345cccb3c310b6ed0374 -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-1_h4a7cf45_openblas.conda#8b39e1ae950f1b54a3959c58ca2c32b8 -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-1_h0358290_openblas.conda#a670bff9eb7963ea41b4e09a4e4ab608 -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-1_h47877c9_openblas.conda#dee12a83aa4aca5077ea23c0605de044 -https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.5-py313hf6604e3_0.conda#15f43bcd12c90186e78801fafc53d89b -https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda#3339e3b65d58accf4ca4fb8748ab16b3 -https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 -https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 -https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 -https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.3-py313h08cd8bf_1.conda#9e87d4bda0c2711161d765332fa38781 -https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda#bf47878473e5ab9fdb4115735230e191 -https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda#9efbfdc37242619130ea42b1cc4ed861 diff --git a/modules/local/compute_base_statistics/environment.yml b/modules/local/compute_base_statistics/environment.yml new file mode 100644 index 00000000..4fa78808 --- /dev/null +++ b/modules/local/compute_base_statistics/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::polars==1.35.2 diff --git a/modules/local/compute_base_statistics/main.nf b/modules/local/compute_base_statistics/main.nf index b088d824..11bf56c3 100644 --- a/modules/local/compute_base_statistics/main.nf +++ b/modules/local/compute_base_statistics/main.nf @@ -8,7 +8,7 @@ process COMPUTE_BASE_STATISTICS { return 1.MB * result * multiplicator } - conda "${moduleDir}/spec-file.txt" + 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/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" diff --git a/modules/local/compute_base_statistics/spec-file.txt b/modules/local/compute_base_statistics/spec-file.txt deleted file mode 100644 index 1bf5d691..00000000 --- a/modules/local/compute_base_statistics/spec-file.txt +++ /dev/null @@ -1,40 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda#3cd1a7238a0dd3d0860fdefc496cc854 -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda#9e60c55e725c20d23125a5f0dd69af5d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda#e66f2b8ad787e7beb0f846e4bd7e8493 -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62ee74e96c5ebb0af99386de58cf9553 -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.6.15-hbd8a1cb_0.conda#72525f07d72806e3b639ad4504c30ce5 -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_0.conda#e31316a586cac398b1fcdb10ace786b9 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda#530566b68c3b8ce7eec4cd047eae19fe -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda#bfbca721fd33188ef923dfe9ba172f29 -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda#323dc8f259224d13078aaf7ce96c3efe -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-32_h59b9bed_openblas.conda#2af9f3d5c2e39f417ce040f5a35c40c6 -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-32_he106b2a_openblas.conda#3d3f9355e52f269cd8bc2c440d8a5263 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda#db0bfbe7dd197b68ad5f30333bae6ce0 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-32_h7ac8fdf_openblas.conda#6c3f04ccb6c578138e9f9899da0bd714 -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda#d864d34357c3b65a4b731f78c0801dc4 -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.2-h6cd9bfd_0.conda#b04c7eda6d7dab1e6503135e7fad4d25 -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda#6d11a5edae89fe413c0569f16d308f5a -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b -https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda#c87df2ab1448ba69169652ab9547082d -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.12.8-h9e4cc4f_1_cpython.conda#7fd2fd79436d9b473812f14e86746844 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda#0dfcdc155cf23812a0c9deada86fb723 -https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.1-py312h6cf2f7f_0.conda#7e086a30150af2536a1059885368dcf0 -https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda#58335b26c38bf4a20f399384c33cbcf9 -https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e -https://conda.anaconda.org/conda-forge/noarch/wheel-0.45.1-pyhd8ed1ab_1.conda#75cb7132eb58d97896e173ef12ac9986 -https://conda.anaconda.org/conda-forge/noarch/pip-25.1.1-pyh8b19718_0.conda#32d0781ace05105cc99af55d36cbec7c -https://conda.anaconda.org/conda-forge/linux-64/polars-1.17.1-py312hda0fa55_1.conda#d9d77bfc286b6044dc045d1696c6acdc diff --git a/modules/local/compute_dataset_statistics/environment.yml b/modules/local/compute_dataset_statistics/environment.yml new file mode 100644 index 00000000..104de0e2 --- /dev/null +++ b/modules/local/compute_dataset_statistics/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::pandas==2.3.3 diff --git a/modules/local/compute_dataset_statistics/main.nf b/modules/local/compute_dataset_statistics/main.nf index 094d23d4..3547515e 100644 --- a/modules/local/compute_dataset_statistics/main.nf +++ b/modules/local/compute_dataset_statistics/main.nf @@ -4,10 +4,10 @@ process COMPUTE_DATASET_STATISTICS { tag "${meta.dataset}" - conda "${moduleDir}/spec-file.txt" + 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/5f/5fe497e7a739fa611fedd6f72ab9a3cf925873a5ded3188161fc85fd376b2c1c/data': - 'community.wave.seqera.io/library/pandas_pyarrow_python_scipy:7cad0d297a717147' }" + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/3d/3d7126100b0eb7cb53dfb50291707ea8dda3b9738b76551ab73605d0acbe114b/data': + 'community.wave.seqera.io/library/pandas:2.3.3--5a902bf824a79745' }" input: tuple val(meta), path(count_file) @@ -21,7 +21,7 @@ process COMPUTE_DATASET_STATISTICS { script: def prefix = task.ext.prefix ?: "${meta.dataset}" """ - get_dataset_statistics.py \ + compute_dataset_statistics.py \ --counts $count_file """ diff --git a/modules/local/compute_dataset_statistics/spec-file.txt b/modules/local/compute_dataset_statistics/spec-file.txt deleted file mode 100644 index ebfe01c3..00000000 --- a/modules/local/compute_dataset_statistics/spec-file.txt +++ /dev/null @@ -1,107 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda#3cd1a7238a0dd3d0860fdefc496cc854 -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda#9e60c55e725c20d23125a5f0dd69af5d -https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.10.6-hb9d3cd8_0.conda#d7d4680337a14001b0e043e96529409b -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.6.15-hbd8a1cb_0.conda#72525f07d72806e3b639ad4504c30ce5 -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda#c87df2ab1448ba69169652ab9547082d -https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.8.1-h1a47875_3.conda#55a8561fdbbbd34f50f57d9be12ed084 -https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.3.0-h4e1184b_5.conda#3f4c1197462a6df2be6dc8241828fe93 -https://conda.anaconda.org/conda-forge/linux-64/s2n-1.5.11-h072c03f_0.conda#5e8060d52f676a40edef0006a75c718f -https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.15.3-h173a860_6.conda#9a063178f1af0a898526cc24ba7be486 -https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.9.2-hefd7a92_4.conda#5ce4df662d32d3123ea8da15571b6f51 -https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.2.2-h4e1184b_0.conda#dcd498d493818b776a77fbc242fbf8e4 -https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.8.1-h205f482_0.conda#9c500858e88df50af3cc883d194de78a -https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.2.2-h4e1184b_4.conda#74e8c3e4df4ceae34aa2959df4b28101 -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda#6d11a5edae89fe413c0569f16d308f5a -https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.5.0-h7959bf6_11.conda#9b3fb60fe57925a92f399bc3fc42eccf -https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.11.0-h11f4f37_12.conda#96c3e0221fa2da97619ee82faa341a73 -https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.7.9-he1b24dc_1.conda#caafc32928a5f7f3f7ef67d287689144 -https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.29.9-he0e7f3f_2.conda#8a4e6fc8a3b285536202b5456a74a940 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda#e66f2b8ad787e7beb0f846e4bd7e8493 -https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.1-h166bdaf_0.tar.bz2#30186d27e2c9fa62b45fb1476b7200e3 -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda#c277e0a4d549b03ac1e9d6cbbe3d017b -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_3.conda#57541755b5a51691955012b8e197c06c -https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda#3f43953b7d3fb3aaa1d0d0723d91e368 -https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.5-hb9d3cd8_0.conda#f7f0d6cc2dc986d42ac2689ec88192be -https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda#172bf1cd1ff8629f2b1179945ed45055 -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.64.0-h161d5f1_0.conda#19e57602824042dfd0446292ef90488b -https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda#eecce068c7e4eddeb169591baac20ac4 -https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda#6432cb5d4ac0046c3ac0a8a0f95842f9 -https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.14.1-h332b0f4_0.conda#45f6713cb00f124af300342512219182 -https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.489-h4d475cb_0.conda#b775e9f46dfa94b228a81d8e8c6d8b1d -https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.14.0-h5cfcd09_0.conda#0a8838771cc2e985cd295e01ae83baf1 -https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.10.0-h113e628_0.conda#73f73f60854f325a55f1d31459f2ab73 -https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 -https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h4ce23a2_1.conda#e796ff8ddc598affdf7c173d6145f087 -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.8-h4bc477f_0.conda#14dbe05b929e329dbaa6f2d0aa19466d -https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.8.0-h736e048_1.conda#13de36be8de3ae3f05ba127631599213 -https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.13.0-h3cf044e_1.conda#7eb66060455c7a47d9dcdbfa9f46579b -https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.12.0-ha633028_1.conda#7c1980f89dd41b097549782121a73490 -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62ee74e96c5ebb0af99386de58cf9553 -https://conda.anaconda.org/conda-forge/linux-64/gflags-2.2.2-h5888daf_1005.conda#d411fc29e338efb48c5fd4576d71d881 -https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hbabe93e_0.conda#ff862eebdfeb2fd048ae9dc92510baca -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_0.conda#e31316a586cac398b1fcdb10ace786b9 -https://conda.anaconda.org/conda-forge/linux-64/libabseil-20240722.0-cxx17_hbbce691_4.conda#488f260ccda0afaf08acb286db439c2f -https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.1.0-hb9d3cd8_3.conda#cb98af5db26e3f482bebb80ce9d947d3 -https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.1.0-hb9d3cd8_3.conda#1c6eecffad553bde44c5238770cfb7da -https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.1.0-hb9d3cd8_3.conda#3facafe58f3858eb95527c7d3a3fc578 -https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-5.28.3-h6128344_1.conda#d8703f1ffe5a06356f06467f1d0b9464 -https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2024.07.02-hbbce691_2.conda#b2fede24428726dd867611664fb372e8 -https://conda.anaconda.org/conda-forge/linux-64/re2-2024.07.02-h9925aae_2.conda#e84ddf12bde691e8ec894b00ea829ddf -https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.67.1-h25350d4_2.conda#bfcedaf5f9b003029cc6abe9431f66bf -https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.35.0-h2b5623c_0.conda#1040ab07d7af9f23cf2466ffe4e58db1 -https://conda.anaconda.org/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2#c965a5aa0d5c1c37ffc62dff36e28400 -https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-2.35.0-h0121fbd_0.conda#34e2243e0428aac6b3e903ef99b6d57d -https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-headers-1.18.0-ha770c72_1.conda#4fb055f57404920a43b147031471e03b -https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h3f2d84a_0.conda#d76872d096d063e226482c99337209dc -https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.1-hb9d3cd8_2.conda#c9f075ab2f33b3bbee9e62d4ad0a6cd8 -https://conda.anaconda.org/conda-forge/linux-64/prometheus-cpp-1.3.0-ha5d0236_0.conda#a83f6a2fdc079e643237887a37460668 -https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-1.18.0-hfcad708_1.conda#1f5a5d66e77a39dc5bd639ec953705cf -https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.10.0-h202a827_0.conda#0f98f3e95272d118f7931b6bef69bfe5 -https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda#9de5350a85c4a20c685259b889aa6393 -https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.1-h8bd8927_1.conda#3b3e64af585eadfb52bb90b553db5edf -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/orc-2.0.3-h12ee42a_2.conda#4f6f9f3f80354ad185e276c120eac3f0 -https://conda.anaconda.org/conda-forge/linux-64/libarrow-19.0.0-hfa2a6e7_9_cpu.conda#9e09f9cd5c0eb584be78ebea4e0db151 -https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-19.0.0-hcb10f89_9_cpu.conda#cd6e5cd25096e02ddc591f0bc7d0354b -https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda#a1cfcc585f0c42bf8d5546bb1dfb668d -https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.21.0-h0e7cc3e_0.conda#dcb95c0a98ba9ff737f7ae482aef7833 -https://conda.anaconda.org/conda-forge/linux-64/libparquet-19.0.0-h081d1f1_9_cpu.conda#de0b82dc1e9f6f9fb66306f0a15e16fa -https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-19.0.0-hcb10f89_9_cpu.conda#da890e33d20acb4713e73ed841d5088b -https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-19.0.0-h08228c5_9_cpu.conda#9d6c1688d87aeb9fc4513bde60d2f2f3 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda#530566b68c3b8ce7eec4cd047eae19fe -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda#bfbca721fd33188ef923dfe9ba172f29 -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda#323dc8f259224d13078aaf7ce96c3efe -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-32_h59b9bed_openblas.conda#2af9f3d5c2e39f417ce040f5a35c40c6 -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-32_he106b2a_openblas.conda#3d3f9355e52f269cd8bc2c440d8a5263 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda#db0bfbe7dd197b68ad5f30333bae6ce0 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-32_h7ac8fdf_openblas.conda#6c3f04ccb6c578138e9f9899da0bd714 -https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda#d864d34357c3b65a4b731f78c0801dc4 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.2-h6cd9bfd_0.conda#b04c7eda6d7dab1e6503135e7fad4d25 -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b -https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 -https://conda.anaconda.org/conda-forge/linux-64/python-3.12.8-h9e4cc4f_1_cpython.conda#7fd2fd79436d9b473812f14e86746844 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda#0dfcdc155cf23812a0c9deada86fb723 -https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.1-py312h6cf2f7f_0.conda#7e086a30150af2536a1059885368dcf0 -https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda#a451d576819089b0d672f18768be0f65 -https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 -https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 -https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 -https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.3-py312hf9745cd_3.conda#2979458c23c7755683a0598fb33e7666 -https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e -https://conda.anaconda.org/conda-forge/noarch/wheel-0.45.1-pyhd8ed1ab_1.conda#75cb7132eb58d97896e173ef12ac9986 -https://conda.anaconda.org/conda-forge/noarch/pip-25.1.1-pyh8b19718_0.conda#32d0781ace05105cc99af55d36cbec7c -https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-19.0.0-py312h01725c0_0_cpu.conda#7ab1143b9ac1af5cc4a630706f643627 -https://conda.anaconda.org/conda-forge/linux-64/pyarrow-19.0.0-py312h7900ff3_0.conda#14f86e63b5c214dd9fb34e5472d4bafc -https://conda.anaconda.org/conda-forge/linux-64/scipy-1.15.0-py312h180e4f1_1.conda#401e9d25f6ed7d9d9a06da0dca473c3e diff --git a/modules/local/compute_gene_transcript_lengths/environment.yml b/modules/local/compute_gene_transcript_lengths/environment.yml new file mode 100644 index 00000000..104de0e2 --- /dev/null +++ b/modules/local/compute_gene_transcript_lengths/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::pandas==2.3.3 diff --git a/modules/local/compute_gene_transcript_lengths/main.nf b/modules/local/compute_gene_transcript_lengths/main.nf index 975f55e4..ada61de4 100644 --- a/modules/local/compute_gene_transcript_lengths/main.nf +++ b/modules/local/compute_gene_transcript_lengths/main.nf @@ -4,7 +4,7 @@ process COMPUTE_GENE_TRANSCRIPT_LENGTHS { tag "${gff3.baseName}" - conda "${moduleDir}/spec-file.txt" + 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/3d/3d7126100b0eb7cb53dfb50291707ea8dda3b9738b76551ab73605d0acbe114b/data': 'community.wave.seqera.io/library/pandas:2.3.3--5a902bf824a79745' }" @@ -25,7 +25,7 @@ process COMPUTE_GENE_TRANSCRIPT_LENGTHS { gzip -c -d ${gff3} > ${gff3_name} fi - get_gene_transcript_lengths.py \\ + compute_gene_transcript_lengths.py \\ --annotation ${gff3_name} """ diff --git a/modules/local/compute_gene_transcript_lengths/spec-file.txt b/modules/local/compute_gene_transcript_lengths/spec-file.txt deleted file mode 100644 index f79332cf..00000000 --- a/modules/local/compute_gene_transcript_lengths/spec-file.txt +++ /dev/null @@ -1,42 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda#f7b4d76975aac7e5d9e6ad13845f92fe -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda#c0374badb3a5d4b1372db28d19462c53 -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda#f0991f0f84902f6b6009b4d2350a83aa -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda#280ea6eee9e2ddefde25ff799c4f0363 -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda#5b767048b1b3ee9a954b06f4084f93dc -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda#f627678cf829bd70bccf141a19c3ad3e -https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-bootstrap_ha15bf96_3.conda#3036ca5b895b7f5146c5a25486234a68 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-hcd61629_7.conda#f116940d825ffc9104400f0d7f1a4551 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_7.conda#8621a450add4e231f676646880703f49 -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda#be43915efc66345cccb3c310b6ed0374 -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-1_h4a7cf45_openblas.conda#8b39e1ae950f1b54a3959c58ca2c32b8 -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-1_h0358290_openblas.conda#a670bff9eb7963ea41b4e09a4e4ab608 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda#8b09ae86839581147ef2e5c5e229d164 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda#35f29eec58405aaf55e01cb470d8c26a -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-1_h47877c9_openblas.conda#dee12a83aa4aca5077ea23c0605de044 -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.0-hee844dc_0.conda#729a572a3ebb8c43933b30edcc628ceb -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda#80c07c68d2f6870250959dcc95b209d1 -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda#9ee58d5c534af06558933af3c845a780 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda#86bc20552bf46075e3d92b67f089172d -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.13.9-hc97d973_101_cp313.conda#4780fe896e961722d0623fa91d0d3378 -https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.5-py313hf6604e3_0.conda#15f43bcd12c90186e78801fafc53d89b -https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda#3339e3b65d58accf4ca4fb8748ab16b3 -https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 -https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 -https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 -https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.3-py313h08cd8bf_1.conda#9e87d4bda0c2711161d765332fa38781 -https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda#bf47878473e5ab9fdb4115735230e191 diff --git a/modules/local/compute_stability_scores/environment.yml b/modules/local/compute_stability_scores/environment.yml new file mode 100644 index 00000000..4fa78808 --- /dev/null +++ b/modules/local/compute_stability_scores/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::polars==1.35.2 diff --git a/modules/local/compute_stability_scores/main.nf b/modules/local/compute_stability_scores/main.nf index e0ca71f3..d5ae8996 100644 --- a/modules/local/compute_stability_scores/main.nf +++ b/modules/local/compute_stability_scores/main.nf @@ -2,10 +2,10 @@ process COMPUTE_STABILITY_SCORES { label 'process_high_memory' - conda "${moduleDir}/spec-file.txt" + 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/01/0118e0577564644b18f94fa6525fe3a2aec845721081b55d82a18e803a50ab17/data': - 'community.wave.seqera.io/library/polars_scikit-learn:036e189d7c1f9704' }" + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': + 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" input: path stat_file diff --git a/modules/local/compute_stability_scores/spec-file.txt b/modules/local/compute_stability_scores/spec-file.txt deleted file mode 100644 index ae52e9e3..00000000 --- a/modules/local/compute_stability_scores/spec-file.txt +++ /dev/null @@ -1,44 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_5.conda#dcd5ff1940cd38f6df777cac86819d60 -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_5.conda#264fbfba7fb20acf3b29cde153e345ce -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda#0be7c6e070c19105f966d3758448d018 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda#4211416ecba1866fab0c6470986c22d6 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda#0b367fad34931cb79e0d6b7e5c06bb1c -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.1-he9a06e4_0.conda#af930c65e9a79a3423d6d36e265cef65 -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda#74784ee3d225fc3dca89edb635b4e5cc -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda#ffffb341206dd0dab0c36053c048d621 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.13.7-h2b335a9_100_cp313.conda#724dcf9960e933838247971da07fe5cf -https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.7-py313hd8ed1ab_100.conda#c5623ddbd37c5dafa7754a83f97de01e -https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.7-h4df99d1_100.conda#47a123ca8e727d886a2c6d0c71658f8c -https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda#aaa2a381ccc56eac91d63b6c1240312f -https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e -https://conda.anaconda.org/conda-forge/noarch/joblib-1.5.2-pyhd8ed1ab_0.conda#4e717929cfa0d49cef92d911e31d0e90 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_5.conda#fbd4008644add05032b6764807ee2cba -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_5.conda#0c91408b3dec0b97e8a3c694845bd63b -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_2.conda#dfc5aae7b043d9f56ba99514d5e60625 -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-35_h4a7cf45_openblas.conda#6da7e852c812a84096b68158574398d0 -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-35_h0358290_openblas.conda#8aa3389d36791ecd31602a247b1f3641 -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-35_h47877c9_openblas.conda#aa0b36b71d44f74686f13b9bfabec891 -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_5.conda#4e02a49aaa9d5190cb630fa43528fbe6 -https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.3-py313hf6604e3_0.conda#3122d20dc438287e125fb5acff1df170 -https://conda.anaconda.org/conda-forge/noarch/pip-25.2-pyh145f28c_0.conda#e7ab34d5a93e0819b62563c78635d937 -https://conda.anaconda.org/conda-forge/linux-64/polars-default-1.33.1-py39hf521cc8_0.conda#900f486d119d5c83d14c812068a3ecad -https://conda.anaconda.org/conda-forge/linux-64/polars-1.33.1-default_h755bcc6_0.conda#1884a1a6acc457c8e4b59b0f6450e140 -https://conda.anaconda.org/conda-forge/linux-64/scipy-1.16.2-py313h11c21cd_0.conda#85a80978a04be9c290b8fe6d9bccff1c -https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda#9d64911b31d57ca443e9f1e36b04385f -https://conda.anaconda.org/conda-forge/linux-64/scikit-learn-1.7.2-py313h06d4379_0.conda#f9b838aa75bd584fb85f46686f4f1453 diff --git a/modules/local/dash_app/app/environment.yml b/modules/local/dash_app/app/environment.yml new file mode 100644 index 00000000..98518853 --- /dev/null +++ b/modules/local/dash_app/app/environment.yml @@ -0,0 +1,14 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::pandas==2.3.3 + - conda-forge::polars==1.35.2 + - conda-forge::scipy==1.16.3 + - conda-forge::dash==3.3.0 + - conda-forge::dash-mantine-components==2.4.0 + - conda-forge::dash-extensions==2.0.4 + - conda-forge::dash-iconify==0.1.2 + - conda-forge::dash-ag-grid==32.3.2 diff --git a/modules/local/dash_app/app/spec-file.txt b/modules/local/dash_app/app/spec-file.txt deleted file mode 100644 index 15f7268c..00000000 --- a/modules/local/dash_app/app/spec-file.txt +++ /dev/null @@ -1,156 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda#f7b4d76975aac7e5d9e6ad13845f92fe -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda#c0374badb3a5d4b1372db28d19462c53 -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-ha97dd6f_2.conda#14bae321b8127b63cba276bd53fac237 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda#4211416ecba1866fab0c6470986c22d6 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda#0b367fad34931cb79e0d6b7e5c06bb1c -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda#80c07c68d2f6870250959dcc95b209d1 -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-hbd8a1cb_0.conda#f9e5fbc24009179e8b0409624691758a -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.4-h26f9b46_0.conda#14edad12b59ccbfa3910d42c72adc2a0 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.13.8-h2b335a9_101_cp313.conda#ae8cf86b9140c7b6a6593a582a8eab8a -https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.8-py313hd8ed1ab_101.conda#d60198f8c2b5cf84e195791f540935ba -https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.8-h4df99d1_101.conda#2af36fadd7f3804bc6d414655c282fa5 -https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda#aaa2a381ccc56eac91d63b6c1240312f -https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda#0caa1af407ecff61170c9437a808404d -https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda#edd329d7d3a4ab45dcf905899a7a6115 -https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda#2934f256a8acfe48f6ebb4fce6cde29c -https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.12.4-hb03c661_0.conda#ae5621814cb99642c9308977fe90ed0d -https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.9.2-he7b75e1_1.conda#c04d1312e7feec369308d656c18e7f3e -https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.3.1-h92c474e_6.conda#3490e744cb8b9d5a3b9785839d618a17 -https://conda.anaconda.org/conda-forge/linux-64/s2n-1.5.26-h5ac9029_0.conda#0cfd80e699ae130623c0f42c6c6cf798 -https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.22.0-h57f3b0d_1.conda#2de3494a513d360155b7f4da7b017840 -https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.10.4-h94feff3_3.conda#8dd69714ac24879be0865676eb333f6b -https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.2.4-h92c474e_1.conda#4ab554b102065910f098f88b40163835 -https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.9.1-h48c9088_3.conda#afdbdbe7f786f47a36a51fdc2fe91210 -https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.2.7-h92c474e_2.conda#248831703050fe9a5b2680a7589fdba9 -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda#5b767048b1b3ee9a954b06f4084f93dc -https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.5.6-h82d11aa_3.conda#a6374ed86387e0b1967adc8d8988db86 -https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.13.3-h2b1cf8c_6.conda#7bb5e26afec09a59283ec1783798d74a -https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.8.6-h4e5ac4b_5.conda#1557911474d926a8bd7b32a5f02bba35 -https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.34.4-h60c762c_0.conda#d41cf259f1b3e2a2347b11b98f64623d -https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda#b38117a3c920364aff79f870c984b4a3 -https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda#c277e0a4d549b03ac1e9d6cbbe3d017b -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda#280ea6eee9e2ddefde25ff799c4f0363 -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda#f627678cf829bd70bccf141a19c3ad3e -https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda#3f43953b7d3fb3aaa1d0d0723d91e368 -https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.5-hb9d3cd8_0.conda#f7f0d6cc2dc986d42ac2689ec88192be -https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda#172bf1cd1ff8629f2b1179945ed45055 -https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda#b499ce4b026493a13774bcf0f4c33849 -https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda#eecce068c7e4eddeb169591baac20ac4 -https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda#6432cb5d4ac0046c3ac0a8a0f95842f9 -https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.14.1-h332b0f4_0.conda#45f6713cb00f124af300342512219182 -https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.606-h32384e2_4.conda#31067fbcb4ddfd76bc855532cc228568 -https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.16.0-h3a458e0_1.conda#682cb082bbd998528c51f1e77d9ce415 -https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.12.0-ha729027_0.conda#3dab8d6fa3d10fe4104f1fbe59c10176 -https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 -https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda#915f5995e94f60e9a4826e0b0920ee88 -https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.0-ha9997c6_1.conda#b24dd2bd61cd8e4f8a13ee2a945a723c -https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.0-h26afc86_1.conda#8337b675e0cad517fbcb3daf7588087a -https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.10.0-h4bb41a7_3.conda#1efaf34774bfb92ecf2fa8fa985b2752 -https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.14.0-hb1c9500_1.conda#30da390c211967189c58f83ab58a6f0c -https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.12.0-h8b27e44_3.conda#7b738aea4f1b8ae2d1118156ad3ae993 -https://conda.anaconda.org/conda-forge/noarch/blinker-1.9.0-pyhff2d567_0.conda#42834439227a4551b939beeeb8a4b085 -https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py313h7033f15_4.conda#bc8624c405856b1d047dd0a81829b08c -https://conda.anaconda.org/conda-forge/noarch/cachelib-0.13.0-pyhd8ed1ab_1.conda#aa353e8215130ccadcc81cf5a45f9579 -https://conda.anaconda.org/conda-forge/noarch/certifi-2025.10.5-pyhd8ed1ab_0.conda#257ae203f1d204107ba389607d375ded -https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda#12c566707c80111f9799308d9e265aef -https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py313hf01b4d8_0.conda#062317cc1cd26fbf6454e86ddd3622c4 -https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.4-pyhd8ed1ab_0.conda#a22d1fd9bf98827e280a02875d9a007a -https://conda.anaconda.org/conda-forge/noarch/click-8.3.0-pyh707e725_0.conda#e76c4ba9e1837847679421b8d549b784 -https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda#df5e78d904988eb55042c0c97446079f -https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda#63ccfdc3a3ce25b027b8767eb722fca8 -https://conda.anaconda.org/conda-forge/noarch/itsdangerous-2.2.0-pyhd8ed1ab_1.conda#7ac5f795c15f288984e32add616cdc59 -https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py313h3dea7bd_0.conda#c14389156310b8ed3520d84f854be1ee -https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda#446bd6c8cb26050d528881df495ce646 -https://conda.anaconda.org/conda-forge/noarch/werkzeug-3.1.3-pyhd8ed1ab_1.conda#0a9b57c159d56b508613cc39022c1b9e -https://conda.anaconda.org/conda-forge/noarch/flask-3.1.2-pyhd8ed1ab_0.conda#ba67a9febeda36948fee26a3dec3d914 -https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda#598fd7d4d0de2455fb74f56063969a97 -https://conda.anaconda.org/conda-forge/noarch/narwhals-2.8.0-pyhcf101f3_0.conda#727dc504e3e5efbb7d48353335056ed2 -https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda#58335b26c38bf4a20f399384c33cbcf9 -https://conda.anaconda.org/conda-forge/noarch/plotly-6.3.1-pyhd8ed1ab_0.conda#673da098d6dc0d6d75780a3d3c46034a -https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda#53abe63df7e10a6ba605dc5f9f961d36 -https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda#0a802cb9888dd14eeefc611f05c40b6e -https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda#8e6923fc12f1fe8f8c4e5c9f343256ac -https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda#164fc43f0b53b6e3a7bc7dce5e4f1dc9 -https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda#461219d1a5bd61342293efa2c0c90eac -https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.25.0-py313h54dd161_0.conda#1fe43bd1fc86e22ad3eb0edec637f8a2 -https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda#436c165519e140cb08d246a4472a9d6a -https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda#db0c6b99149880c8ba515cf4abe93ee4 -https://conda.anaconda.org/conda-forge/noarch/retrying-1.4.2-pyhe01879c_0.conda#128b46a47ea164f9a8659cb6da2f3555 -https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e -https://conda.anaconda.org/conda-forge/noarch/dash-3.2.0-pyhd8ed1ab_0.conda#da955d1c354e25b15b4e09f837baf01d -https://conda.anaconda.org/conda-forge/noarch/dash-ag-grid-32.3.2-pyhd8ed1ab_0.conda#1a75235389991e3f429eef06b7117de9 -https://conda.anaconda.org/conda-forge/noarch/dataclass-wizard-0.35.1-pyhd8ed1ab_0.conda#74ed89aa1b231012173cf76d7ede8d75 -https://conda.anaconda.org/conda-forge/noarch/flask-caching-2.3.1-pyhd8ed1ab_0.conda#fb85fa05db13110f34c773f0862f6ca2 -https://conda.anaconda.org/conda-forge/noarch/editorconfig-0.17.1-pyhe01879c_1.conda#946b7c3d528a3d231874322efdc25ed6 -https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda#3339e3b65d58accf4ca4fb8748ab16b3 -https://conda.anaconda.org/conda-forge/noarch/jsbeautifier-1.15.4-pyhd8ed1ab_0.conda#60c0e0783e98da236b4e0f55242f10c7 -https://conda.anaconda.org/conda-forge/noarch/more-itertools-10.8.0-pyhd8ed1ab_0.conda#d7620a15dc400b448e1c88a981b23ddd -https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.41.4-py313h843e2db_0.conda#d42ccdecefbe670d2a50ee3ce784166b -https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhd8ed1ab_0.conda#399701494e731ce73fdd86c185a3d1b4 -https://conda.anaconda.org/conda-forge/noarch/pydantic-2.12.2-pyh3cfb1c2_0.conda#fc3a3515b4e71b22b635c4ae34e2f3ea -https://conda.anaconda.org/conda-forge/noarch/dash-extensions-2.0.4-pyhd8ed1ab_0.conda#d0b5c1d8824f86f43cd88034b5724e26 -https://conda.anaconda.org/conda-forge/noarch/dash-iconify-0.1.2-pyhd8ed1ab_0.conda#462fd3653b5ca7fd07072c632d0361d6 -https://conda.anaconda.org/conda-forge/noarch/dash-mantine-components-2.3.0-pyhd8ed1ab_0.conda#873924129b715871a58922ab8cc15ebd -https://conda.anaconda.org/conda-forge/linux-64/gflags-2.2.2-h5888daf_1005.conda#d411fc29e338efb48c5fd4576d71d881 -https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hbabe93e_0.conda#ff862eebdfeb2fd048ae9dc92510baca -https://conda.anaconda.org/conda-forge/linux-64/libabseil-20250512.1-cxx17_hba17884_0.conda#83b160d4da3e1e847bf044997621ed63 -https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.1.0-hb03c661_4.conda#1d29d2e33fe59954af82ef54a8af3fe1 -https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.1.0-hb03c661_4.conda#5cb5a1c9a94a78f5b23684bcb845338d -https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.1.0-hb03c661_4.conda#2e55011fa483edb8bfe3fd92e860cd79 -https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-6.31.1-h49aed37_2.conda#94cb88daa0892171457d9fdc69f43eca -https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2025.08.12-h7b12aa8_1.conda#0a801dabf8776bb86b12091d2f99377e -https://conda.anaconda.org/conda-forge/linux-64/re2-2025.08.12-h5301d42_1.conda#4637c13ff87424af0f6a981ab6f5ffa5 -https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.73.1-h1e535eb_0.conda#8075d8550f773a17288c7ec2cf2f2d56 -https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.39.0-hdb79228_0.conda#a2e30ccd49f753fd30de0d30b1569789 -https://conda.anaconda.org/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2#c965a5aa0d5c1c37ffc62dff36e28400 -https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-2.39.0-hdbdcf42_0.conda#bd21962ff8a9d1ce4720d42a35a4af40 -https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-headers-1.21.0-ha770c72_1.conda#9e298d76f543deb06eb0f3413675e13a -https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h54a6638_1.conda#16c2a0e9c4a166e53632cfca4f68d020 -https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.1-hb9d3cd8_2.conda#c9f075ab2f33b3bbee9e62d4ad0a6cd8 -https://conda.anaconda.org/conda-forge/linux-64/prometheus-cpp-1.3.0-ha5d0236_0.conda#a83f6a2fdc079e643237887a37460668 -https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-1.21.0-hb9b0907_1.conda#1c0320794855f457dea27d35c4c71e23 -https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda#9de5350a85c4a20c685259b889aa6393 -https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_0.conda#3d8da0248bdae970b4ade636a104b7f5 -https://conda.anaconda.org/conda-forge/linux-64/orc-2.2.1-hd747db4_0.conda#ddab8b2af55b88d63469c040377bd37e -https://conda.anaconda.org/conda-forge/linux-64/libarrow-21.0.0-h56a6dad_8_cpu.conda#3dc4bd7a6243159d2a3291e259222ddc -https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.11.0-hb04c3b8_0.conda#34fb73fd2d5a613d8f17ce2eaa15a8a5 -https://conda.anaconda.org/conda-forge/linux-64/libarrow-compute-21.0.0-h8c2c5c3_8_cpu.conda#64342bd7f29894d3f16ef7b71f8f2328 -https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-21.0.0-h635bf11_8_cpu.conda#1b8f002c3ea2f207a8306d94370f526b -https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda#a1cfcc585f0c42bf8d5546bb1dfb668d -https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.22.0-h454ac66_1.conda#8ed82d90e6b1686f5e98f8b7825a15ef -https://conda.anaconda.org/conda-forge/linux-64/libparquet-21.0.0-h790f06f_8_cpu.conda#80344ce1bdd57e68bd70e742430a408c -https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-21.0.0-h635bf11_8_cpu.conda#e0aef220789dd2234cbfb8baf759d405 -https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-21.0.0-h3f74fd7_8_cpu.conda#86f6d887749f5f7f30d91ef6a5e01515 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-hcd61629_7.conda#f116940d825ffc9104400f0d7f1a4551 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_7.conda#8621a450add4e231f676646880703f49 -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_2.conda#dfc5aae7b043d9f56ba99514d5e60625 -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-37_h4a7cf45_openblas.conda#8bc098f29d8a7e3517bac5b25aab39b1 -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-37_h0358290_openblas.conda#3794858d4d6910a7fc3c181519e0b77a -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-37_h47877c9_openblas.conda#8305e6a5ed432ad3e5a609e8024dbc17 -https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.3-py313hf6604e3_0.conda#3122d20dc438287e125fb5acff1df170 -https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 -https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 -https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 -https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.3-py313h08cd8bf_1.conda#9e87d4bda0c2711161d765332fa38781 -https://conda.anaconda.org/conda-forge/noarch/pip-25.2-pyh145f28c_0.conda#e7ab34d5a93e0819b62563c78635d937 -https://conda.anaconda.org/conda-forge/linux-64/polars-runtime-32-1.34.0-py310hffdcd12_0.conda#496b18392ef5af544d22d18d91a2a371 -https://conda.anaconda.org/conda-forge/noarch/polars-1.34.0-pyh6a1acc5_0.conda#d398dbcb3312bbebc2b2f3dbb98b4262 -https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-21.0.0-py313he109ebe_1_cpu.conda#91bebcdab448722d7b919ffe4f9504e2 -https://conda.anaconda.org/conda-forge/linux-64/pyarrow-21.0.0-py313h78bf25f_1.conda#58ab79f6cc05e9daeb74560d80256270 -https://conda.anaconda.org/conda-forge/linux-64/scipy-1.16.2-py313h11c21cd_0.conda#85a80978a04be9c290b8fe6d9bccff1c diff --git a/modules/local/dash_app/main.nf b/modules/local/dash_app/main.nf index ef516828..4b966434 100644 --- a/modules/local/dash_app/main.nf +++ b/modules/local/dash_app/main.nf @@ -2,7 +2,7 @@ process DASH_APP { label 'process_high_memory' - conda "${moduleDir}/app/spec-file.txt" + conda "${moduleDir}/app/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/4e/4eec747f2063edcc2d1b64e3b84a6b154fde1b9cd9d698446321b4a535432272/data': 'community.wave.seqera.io/library/dash-ag-grid_dash-extensions_dash-iconify_dash-mantine-components_pruned:7cf6396dd8cd850e' }" diff --git a/modules/local/download_ensembl_annotation/environment.yml b/modules/local/download_ensembl_annotation/environment.yml new file mode 100644 index 00000000..7aa02d40 --- /dev/null +++ b/modules/local/download_ensembl_annotation/environment.yml @@ -0,0 +1,11 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::pandas==2.3.3 + - conda-forge::requests==2.32.5 + - conda-forge::tqdm==4.67.1 + - conda-forge::bs4==4.14.2 + - conda-forge::tenacity==9.1.2 diff --git a/modules/local/download_ensembl_annotation/main.nf b/modules/local/download_ensembl_annotation/main.nf index 4bef236d..59cbf722 100644 --- a/modules/local/download_ensembl_annotation/main.nf +++ b/modules/local/download_ensembl_annotation/main.nf @@ -4,7 +4,7 @@ process DOWNLOAD_ENSEMBL_ANNOTATION { tag "${species}" - conda "${moduleDir}/spec-file.txt" + 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/5f/5fa11d593e2f2d68c60acc6a00c812793112bff4691754c992fff6b038458604/data': 'community.wave.seqera.io/library/bs4_pandas_requests_tenacity_tqdm:32f7387852168716' }" diff --git a/modules/local/download_ensembl_annotation/spec-file.txt b/modules/local/download_ensembl_annotation/spec-file.txt deleted file mode 100644 index 8255eae2..00000000 --- a/modules/local/download_ensembl_annotation/spec-file.txt +++ /dev/null @@ -1,64 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda#f7b4d76975aac7e5d9e6ad13845f92fe -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda#c0374badb3a5d4b1372db28d19462c53 -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-bootstrap_ha15bf96_3.conda#3036ca5b895b7f5146c5a25486234a68 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda#8b09ae86839581147ef2e5c5e229d164 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda#35f29eec58405aaf55e01cb470d8c26a -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda#280ea6eee9e2ddefde25ff799c4f0363 -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda#5b767048b1b3ee9a954b06f4084f93dc -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda#f627678cf829bd70bccf141a19c3ad3e -https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.0-hee844dc_0.conda#729a572a3ebb8c43933b30edcc628ceb -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda#80c07c68d2f6870250959dcc95b209d1 -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda#f0991f0f84902f6b6009b4d2350a83aa -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda#9ee58d5c534af06558933af3c845a780 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda#86bc20552bf46075e3d92b67f089172d -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.13.9-hc97d973_101_cp313.conda#4780fe896e961722d0623fa91d0d3378 -https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8-pyhd8ed1ab_0.conda#18c019ccf43769d211f2cf78e9ad46c2 -https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda#0caa1af407ecff61170c9437a808404d -https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda#edd329d7d3a4ab45dcf905899a7a6115 -https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.2-pyha770c72_0.conda#749ebebabc2cae99b2e5b3edd04c6ca2 -https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py313h09d1b84_0.conda#dfd94363b679c74937b3926731ee861a -https://conda.anaconda.org/conda-forge/noarch/bs4-4.14.2-hd8ed1ab_0.conda#19dbd742f9c26cfe9b89a05461aa68c3 -https://conda.anaconda.org/conda-forge/noarch/certifi-2025.11.12-pyhd8ed1ab_0.conda#96a02a5c1a65470a7e4eedb644c872fd -https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda#12c566707c80111f9799308d9e265aef -https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py313hf46b229_1.conda#d0616e7935acab407d1543b28c446f6f -https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.4-pyhd8ed1ab_0.conda#a22d1fd9bf98827e280a02875d9a007a -https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda#962b9857ee8e7018c22f2776ffa0b2d7 -https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda#0a802cb9888dd14eeefc611f05c40b6e -https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda#8e6923fc12f1fe8f8c4e5c9f343256ac -https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda#164fc43f0b53b6e3a7bc7dce5e4f1dc9 -https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda#53abe63df7e10a6ba605dc5f9f961d36 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-hcd61629_7.conda#f116940d825ffc9104400f0d7f1a4551 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_7.conda#8621a450add4e231f676646880703f49 -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda#be43915efc66345cccb3c310b6ed0374 -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-2_h4a7cf45_openblas.conda#6146bf1b7f58113d54614c6ec683c14a -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-2_h0358290_openblas.conda#a84b2b7ed34206d14739fb8d29cd2799 -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-2_h47877c9_openblas.conda#9fb20e74a7436dc94dd39d9a9decddc3 -https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.5-py313hf6604e3_0.conda#15f43bcd12c90186e78801fafc53d89b -https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda#3339e3b65d58accf4ca4fb8748ab16b3 -https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 -https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 -https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 -https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.3-py313h08cd8bf_1.conda#9e87d4bda0c2711161d765332fa38781 -https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda#bf47878473e5ab9fdb4115735230e191 -https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda#461219d1a5bd61342293efa2c0c90eac -https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda#6432cb5d4ac0046c3ac0a8a0f95842f9 -https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.25.0-py313h54dd161_1.conda#710d4663806d0f72b2fb414e936223b5 -https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda#436c165519e140cb08d246a4472a9d6a -https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda#db0c6b99149880c8ba515cf4abe93ee4 -https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.2-pyhd8ed1ab_0.conda#5d99943f2ae3cc69e1ada12ce9d4d701 -https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda#9efbfdc37242619130ea42b1cc4ed861 diff --git a/modules/local/download_ncbi_annotation/environment.yml b/modules/local/download_ncbi_annotation/environment.yml new file mode 100644 index 00000000..087abd4b --- /dev/null +++ b/modules/local/download_ncbi_annotation/environment.yml @@ -0,0 +1,8 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::requests==2.32.5 + - conda-forge::tenacity==9.1.2 diff --git a/modules/local/download_ncbi_annotation/main.nf b/modules/local/download_ncbi_annotation/main.nf index ad6fdb78..c59fce96 100644 --- a/modules/local/download_ncbi_annotation/main.nf +++ b/modules/local/download_ncbi_annotation/main.nf @@ -4,7 +4,7 @@ process DOWNLOAD_NCBI_ANNOTATION { tag "${species}" - conda "${moduleDir}/spec-file.txt" + 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/5c/5c28c8e613c062828aaee4b950029bc90a1a1aa94d5f61016a588c8ec7be8b65/data': 'community.wave.seqera.io/library/pandas_requests_tenacity:5ba56df089a9d718' }" diff --git a/modules/local/download_ncbi_annotation/spec-file.txt b/modules/local/download_ncbi_annotation/spec-file.txt deleted file mode 100644 index 3233c10b..00000000 --- a/modules/local/download_ncbi_annotation/spec-file.txt +++ /dev/null @@ -1,57 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda#3cd1a7238a0dd3d0860fdefc496cc854 -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda#9e60c55e725c20d23125a5f0dd69af5d -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda#6d11a5edae89fe413c0569f16d308f5a -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda#e66f2b8ad787e7beb0f846e4bd7e8493 -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62ee74e96c5ebb0af99386de58cf9553 -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_0.conda#e31316a586cac398b1fcdb10ace786b9 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda#db0bfbe7dd197b68ad5f30333bae6ce0 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda#d864d34357c3b65a4b731f78c0801dc4 -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.2-h6cd9bfd_0.conda#b04c7eda6d7dab1e6503135e7fad4d25 -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b -https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.6.15-hbd8a1cb_0.conda#72525f07d72806e3b639ad4504c30ce5 -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda#c87df2ab1448ba69169652ab9547082d -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.12.8-h9e4cc4f_1_cpython.conda#7fd2fd79436d9b473812f14e86746844 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda#0dfcdc155cf23812a0c9deada86fb723 -https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h2ec8cdc_3.conda#a32e0c069f6c3dcac635f7b0b0dac67e -https://conda.anaconda.org/conda-forge/noarch/certifi-2025.6.15-pyhd8ed1ab_0.conda#781d068df0cc2407d4db0ecfbb29225b -https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda#12c566707c80111f9799308d9e265aef -https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py312h06ac9bb_0.conda#a861504bbea4161a9170b85d4d2be840 -https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.2-pyhd8ed1ab_0.conda#40fe4284b8b5835a9073a645139f35af -https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda#0a802cb9888dd14eeefc611f05c40b6e -https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda#8e6923fc12f1fe8f8c4e5c9f343256ac -https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda#b4754fb1bdcb70c8fd54f918301582c6 -https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda#39a4f67be3286c86d696df570b1201b7 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda#530566b68c3b8ce7eec4cd047eae19fe -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda#bfbca721fd33188ef923dfe9ba172f29 -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda#323dc8f259224d13078aaf7ce96c3efe -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-32_h59b9bed_openblas.conda#2af9f3d5c2e39f417ce040f5a35c40c6 -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-32_he106b2a_openblas.conda#3d3f9355e52f269cd8bc2c440d8a5263 -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-32_h7ac8fdf_openblas.conda#6c3f04ccb6c578138e9f9899da0bd714 -https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.1-py312h6cf2f7f_0.conda#7e086a30150af2536a1059885368dcf0 -https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda#a451d576819089b0d672f18768be0f65 -https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 -https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 -https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 -https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.3-py312hf9745cd_3.conda#2979458c23c7755683a0598fb33e7666 -https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e -https://conda.anaconda.org/conda-forge/noarch/wheel-0.45.1-pyhd8ed1ab_1.conda#75cb7132eb58d97896e173ef12ac9986 -https://conda.anaconda.org/conda-forge/noarch/pip-25.1.1-pyh8b19718_0.conda#32d0781ace05105cc99af55d36cbec7c -https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda#461219d1a5bd61342293efa2c0c90eac -https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py312h66e93f0_2.conda#630db208bc7bbb96725ce9832c7423bb -https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda#436c165519e140cb08d246a4472a9d6a -https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda#a9b9368f3701a417eac9edbcae7cb737 -https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.2-pyhd8ed1ab_0.conda#5d99943f2ae3cc69e1ada12ce9d4d701 diff --git a/modules/local/expressionatlas/getaccessions/environment.yml b/modules/local/expressionatlas/getaccessions/environment.yml new file mode 100644 index 00000000..6abe20f0 --- /dev/null +++ b/modules/local/expressionatlas/getaccessions/environment.yml @@ -0,0 +1,11 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::pandas==2.3.3 + - conda-forge::requests==2.32.5 + - conda-forge::tenacity==9.1.2 + - conda-forge::pyyaml==6.0.3 + - conda-forge::nltk==3.9.2 diff --git a/modules/local/expressionatlas/getaccessions/main.nf b/modules/local/expressionatlas/getaccessions/main.nf index 430b3a2e..0aded0be 100644 --- a/modules/local/expressionatlas/getaccessions/main.nf +++ b/modules/local/expressionatlas/getaccessions/main.nf @@ -4,7 +4,7 @@ process EXPRESSIONATLAS_GETACCESSIONS { tag "${species}" - conda "${moduleDir}/spec-file.txt" + 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/f2/f2219a174683388670dc0817da45717014aca444323027480f84aaaf12bfb460/data': 'community.wave.seqera.io/library/nltk_data_pandas_pyyaml_requests_tenacity:5f5f82f858433879' }" diff --git a/modules/local/expressionatlas/getaccessions/spec-file.txt b/modules/local/expressionatlas/getaccessions/spec-file.txt deleted file mode 100644 index 11788e79..00000000 --- a/modules/local/expressionatlas/getaccessions/spec-file.txt +++ /dev/null @@ -1,65 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda#3cd1a7238a0dd3d0860fdefc496cc854 -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda#9e60c55e725c20d23125a5f0dd69af5d -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda#6d11a5edae89fe413c0569f16d308f5a -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda#e66f2b8ad787e7beb0f846e4bd7e8493 -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62ee74e96c5ebb0af99386de58cf9553 -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda#0be7c6e070c19105f966d3758448d018 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda#db0bfbe7dd197b68ad5f30333bae6ce0 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda#d864d34357c3b65a4b731f78c0801dc4 -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.2-h6cd9bfd_0.conda#b04c7eda6d7dab1e6503135e7fad4d25 -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b -https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.7.9-hbd8a1cb_0.conda#54521bf3b59c86e2f55b7294b40a04dc -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda#c87df2ab1448ba69169652ab9547082d -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.12.11-h9e4cc4f_0_cpython.conda#94206474a5608243a10c92cefbe0908f -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda#0dfcdc155cf23812a0c9deada86fb723 -https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h2ec8cdc_3.conda#a32e0c069f6c3dcac635f7b0b0dac67e -https://conda.anaconda.org/conda-forge/noarch/certifi-2025.6.15-pyhd8ed1ab_0.conda#781d068df0cc2407d4db0ecfbb29225b -https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda#12c566707c80111f9799308d9e265aef -https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py312h06ac9bb_0.conda#a861504bbea4161a9170b85d4d2be840 -https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.2-pyhd8ed1ab_0.conda#40fe4284b8b5835a9073a645139f35af -https://conda.anaconda.org/conda-forge/noarch/click-8.2.1-pyh707e725_0.conda#94b550b8d3a614dbd326af798c7dfb40 -https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda#962b9857ee8e7018c22f2776ffa0b2d7 -https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda#0a802cb9888dd14eeefc611f05c40b6e -https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda#8e6923fc12f1fe8f8c4e5c9f343256ac -https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda#b4754fb1bdcb70c8fd54f918301582c6 -https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda#39a4f67be3286c86d696df570b1201b7 -https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e -https://conda.anaconda.org/conda-forge/noarch/joblib-1.5.1-pyhd8ed1ab_0.conda#fb1c14694de51a476ce8636d92b6f42c -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda#530566b68c3b8ce7eec4cd047eae19fe -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda#bfbca721fd33188ef923dfe9ba172f29 -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda#323dc8f259224d13078aaf7ce96c3efe -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-32_h59b9bed_openblas.conda#2af9f3d5c2e39f417ce040f5a35c40c6 -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-32_he106b2a_openblas.conda#3d3f9355e52f269cd8bc2c440d8a5263 -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-32_h7ac8fdf_openblas.conda#6c3f04ccb6c578138e9f9899da0bd714 -https://conda.anaconda.org/conda-forge/linux-64/regex-2024.11.6-py312h66e93f0_0.conda#647770db979b43f9c9ca25dcfa7dc4e4 -https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda#9efbfdc37242619130ea42b1cc4ed861 -https://conda.anaconda.org/conda-forge/noarch/nltk-3.9.1-pyhd8ed1ab_1.conda#85fd21c82d46f871d3820c17270e575d -https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.1-py312h6cf2f7f_0.conda#7e086a30150af2536a1059885368dcf0 -https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda#a451d576819089b0d672f18768be0f65 -https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 -https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 -https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 -https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.0-py312hf9745cd_0.conda#ac82ac336dbe61106e21fb2e11704459 -https://conda.anaconda.org/conda-forge/noarch/wheel-0.45.1-pyhd8ed1ab_1.conda#75cb7132eb58d97896e173ef12ac9986 -https://conda.anaconda.org/conda-forge/noarch/pip-25.1.1-pyh8b19718_0.conda#32d0781ace05105cc99af55d36cbec7c -https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda#461219d1a5bd61342293efa2c0c90eac -https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2#4cb3ad778ec2d5a7acbdf254eb1c42ae -https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py312h178313f_2.conda#cf2485f39740de96e2a7f2bb18ed2fee -https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py312h66e93f0_2.conda#630db208bc7bbb96725ce9832c7423bb -https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda#436c165519e140cb08d246a4472a9d6a -https://conda.anaconda.org/conda-forge/noarch/requests-2.32.4-pyhd8ed1ab_0.conda#f6082eae112814f1447b56a5e1f6ed05 -https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.2-pyhd8ed1ab_0.conda#5d99943f2ae3cc69e1ada12ce9d4d701 diff --git a/modules/local/expressionatlas/getdata/environment.yml b/modules/local/expressionatlas/getdata/environment.yml new file mode 100644 index 00000000..cdb6c8ed --- /dev/null +++ b/modules/local/expressionatlas/getdata/environment.yml @@ -0,0 +1,9 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::r-base==4.4.3 + - conda-forge::r-optparse==1.7.5 + - bioconda::bioconductor-expressionatlas==1.34.0 diff --git a/modules/local/expressionatlas/getdata/main.nf b/modules/local/expressionatlas/getdata/main.nf index 6a5a2ece..267b3233 100644 --- a/modules/local/expressionatlas/getdata/main.nf +++ b/modules/local/expressionatlas/getdata/main.nf @@ -6,7 +6,7 @@ process EXPRESSIONATLAS_GETDATA { maxForks 8 // limiting to 8 threads at a time to avoid 429 errors with the Expression Atlas API server - conda "${moduleDir}/spec-file.txt" + 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/7f/7fd21450c3a3f7df37fa0480170780019e9686be319da1c9e10712f7f17cca26/data': 'community.wave.seqera.io/library/bioconductor-expressionatlas_r-base_r-optparse:ca0f8cd9d3f44af9' }" @@ -24,6 +24,7 @@ process EXPRESSIONATLAS_GETDATA { script: """ + which python download_eatlas_data.R --accession $accession """ diff --git a/modules/local/expressionatlas/getdata/spec-file.txt b/modules/local/expressionatlas/getdata/spec-file.txt deleted file mode 100644 index 7bbe4329..00000000 --- a/modules/local/expressionatlas/getdata/spec-file.txt +++ /dev/null @@ -1,174 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda#3cd1a7238a0dd3d0860fdefc496cc854 -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/noarch/_r-mutex-1.0.1-anacondar_1.tar.bz2#19f9db5f4f1b7f5ef5f6d67207f25f38 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda#9e60c55e725c20d23125a5f0dd69af5d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda#e66f2b8ad787e7beb0f846e4bd7e8493 -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62ee74e96c5ebb0af99386de58cf9553 -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_0.conda#e31316a586cac398b1fcdb10ace786b9 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda#db0bfbe7dd197b68ad5f30333bae6ce0 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.2-h6cd9bfd_0.conda#b04c7eda6d7dab1e6503135e7fad4d25 -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.6.15-hbd8a1cb_0.conda#72525f07d72806e3b639ad4504c30ce5 -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda#c87df2ab1448ba69169652ab9547082d -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-7_cp313.conda#e84b44e6300f1703cb25d29120c5b1d8 -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.13.5-hec9711d_102_cp313.conda#89e07d92cf50743886f41638d58c4328 -https://conda.anaconda.org/conda-forge/noarch/argcomplete-3.6.2-pyhd8ed1ab_0.conda#eb9d4263271ca287d2e0cf5a86da2d3a -https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-3.10.0-he073ed8_18.conda#ad8527bf134a90e1c9ed35fa0b64318c -https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.17-h0157908_18.conda#460eba7851277ec1fd80a1a24080787a -https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.44-h4bf12b8_0.conda#7a1b5c3fbc0419961eaed361eedc90d4 -https://conda.anaconda.org/conda-forge/linux-64/bwidget-1.10.1-ha770c72_1.conda#983b92277d78c0d0ec498e460caa0e6d -https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.50-h943b412_0.conda#51de14db340a848869e69c632b43cca7 -https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.13.3-h48d6fc4_1.conda#3c255be50a506c50765a93a6644f32fe -https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.13.3-ha770c72_1.conda#51f5be229d83ecd401fb369ab96ae669 -https://conda.anaconda.org/conda-forge/linux-64/freetype-2.13.3-ha770c72_1.conda#9ccd736d31e0c6e41f54e704e5312811 -https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.15.0-h7e30c49_1.conda#8f5b0b297b59e1ac160ad4beec99dbee -https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2#0c96522c6bdaed4b1566d11387caaf45 -https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2#34893075a5c9e55cdafac56607368fc6 -https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2#4d59c254e01d9cde7957100457e2d5fb -https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda#49023d73832ef61042f6a237cb2687e7 -https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29 -https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda#6d11a5edae89fe413c0569f16d308f5a -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_3.conda#57541755b5a51691955012b8e197c06c -https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 -https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h4ce23a2_1.conda#e796ff8ddc598affdf7c173d6145f087 -https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.45-hc749103_0.conda#b90bece58b4c2bf25969b70f3be42d25 -https://conda.anaconda.org/conda-forge/linux-64/libglib-2.84.2-h3618099_0.conda#072ab14a02164b7c0c089055368ff776 -https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda#b3c17d95b5a10c6e64a21fa17573e70e -https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb9d3cd8_0.conda#f6ebe2cb3f82ba6c057dde5d9debe4f7 -https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb9d3cd8_0.conda#8035c64cb77ed555e3f150b7b3972480 -https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda#92ed62436b625154323d40d5f2f11dd7 -https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.2-h29eaf8c_0.conda#39b4228a867772d610c02e06f939a5b8 -https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda#fb901ff28063514abb6046c9ec2c4a45 -https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda#1c74ff8c35dcadf952a16f752ca5aa49 -https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.12-h4f16b4b_0.conda#db038ce880f100acc74dba10302b5630 -https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.6-hb9d3cd8_0.conda#febbab7d15033c913d53c7a2c102309d -https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda#96d57aba173e878a2089d5638016dc5e -https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda#09262e66b19567aff4f592fb53b28760 -https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.1-h166bdaf_0.tar.bz2#30186d27e2c9fa62b45fb1476b7200e3 -https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda#c277e0a4d549b03ac1e9d6cbbe3d017b -https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda#3f43953b7d3fb3aaa1d0d0723d91e368 -https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.5-hb9d3cd8_0.conda#f7f0d6cc2dc986d42ac2689ec88192be -https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda#172bf1cd1ff8629f2b1179945ed45055 -https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.64.0-h161d5f1_0.conda#19e57602824042dfd0446292ef90488b -https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda#eecce068c7e4eddeb169591baac20ac4 -https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda#6432cb5d4ac0046c3ac0a8a0f95842f9 -https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.14.1-h332b0f4_0.conda#45f6713cb00f124af300342512219182 -https://conda.anaconda.org/conda-forge/linux-64/curl-8.14.1-h332b0f4_0.conda#60279087a10b4ab59a70daa838894e4b -https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-15.1.0-h4c094af_103.conda#ea67e87d658d31dc33818f9574563269 -https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-15.1.0-h97b714f_3.conda#bbcff9bf972a0437bea8e431e4b327bb -https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-15.1.0-h4393ad2_3.conda#f39f96280dd8b1ec8cbd395a3d3fdd1e -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda#530566b68c3b8ce7eec4cd047eae19fe -https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-15.1.0-h3b9cdf2_3.conda#649c5fe0593a880702e434bc375f3e8a -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda#bfbca721fd33188ef923dfe9ba172f29 -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda#323dc8f259224d13078aaf7ce96c3efe -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-32_h59b9bed_openblas.conda#2af9f3d5c2e39f417ce040f5a35c40c6 -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-32_he106b2a_openblas.conda#3d3f9355e52f269cd8bc2c440d8a5263 -https://conda.anaconda.org/conda-forge/linux-64/gsl-2.7-he838d99_0.tar.bz2#fec079ba39c9cca093bf4c00001825de -https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-15.1.0-h4c094af_103.conda#83bbc814f0aeccccb5ea10267bea0d2e -https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-15.1.0-h6a1bac1_3.conda#d71cc504fcfdbee8dd7925ebb9c2bf85 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-15.1.0-h69a702a_3.conda#6e5d0574e57a38c36e674e9a18eee2b4 -https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.0-hb9d3cd8_0.conda#9fa334557db9f63da6c9285fd2a48638 -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-32_h7ac8fdf_openblas.conda#6c3f04ccb6c578138e9f9899da0bd714 -https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda#9344155d33912347b37f0ae6c410a835 -https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.24-h86f0d12_0.conda#64f0c503da58ec25ebd359e4d990afa8 -https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.5.0-h851e524_0.conda#63f790534398730f59e1b899c3644d4a -https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.0-hf01ce69_5.conda#e79a094918988bb1807462cd42c83962 -https://conda.anaconda.org/conda-forge/linux-64/make-4.4.1-hb9d3cd8_2.conda#33405d2a66b1411db9f7242c8b97c9e7 -https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.10-h36c2ea0_0.tar.bz2#ac7bc6a654f8f41b352b38f4051135f8 -https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-h5888daf_0.conda#951ff8d9e5536896408e89d63230b8d5 -https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-11.2.1-h3beb420_0.conda#0e6e192d4b3d95708ad192d957cf3163 -https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.4-hadf4263_0.conda#79f71230c069a287efe3a8614069ddf1 -https://conda.anaconda.org/conda-forge/linux-64/sed-4.9-h6688a6e_0.conda#171afc5f7ca0408bbccbcb69ade85f92 -https://conda.anaconda.org/conda-forge/linux-64/tktable-2.10-h8d826fa_7.conda#3ac51142c19ba95ae0fadefa333c9afb -https://conda.anaconda.org/conda-forge/linux-64/xorg-libxt-1.3.1-hb9d3cd8_0.conda#279b0de5f6ba95457190a1c459a64e31 -https://conda.anaconda.org/conda-forge/linux-64/r-base-4.3.3-h65010dc_18.conda#721ea26859f44b206b0146eae8444657 -https://conda.anaconda.org/bioconda/noarch/bioconductor-biocgenerics-0.48.1-r43hdfd78af_2.tar.bz2#a313dd8a932cfd178fad2f3e7e6a6184 -https://conda.anaconda.org/bioconda/linux-64/bioconductor-biobase-2.62.0-r43ha9d7317_3.tar.bz2#536352cf94bc990f2d723564fe0d6ff9 -https://conda.anaconda.org/conda-forge/noarch/r-biocmanager-1.30.26-r43hc72bb7e_0.conda#f761a528299b72c890846fc1d1ddf099 -https://conda.anaconda.org/conda-forge/linux-64/r-base64enc-0.1_3-r43hb1dbf0f_1007.conda#3509080778587bf9d42eae11e0246633 -https://conda.anaconda.org/conda-forge/linux-64/r-digest-0.6.37-r43h0d4f4ea_0.conda#edcd201672d47f522666954cc7b25e0d -https://conda.anaconda.org/conda-forge/linux-64/r-rlang-1.1.6-r43h93ab643_0.conda#057b78b5adfffc99092504a1da563abe -https://conda.anaconda.org/conda-forge/linux-64/r-ellipsis-0.3.2-r43hb1dbf0f_3.conda#b8349582a31b17184a7674f4c847a5ad -https://conda.anaconda.org/conda-forge/linux-64/r-fastmap-1.2.0-r43ha18555a_1.conda#ac100509c0d93c8dc19e53fb299a48b5 -https://conda.anaconda.org/conda-forge/linux-64/r-htmltools-0.5.8.1-r43ha18555a_1.conda#7b26688542e1b7a39fc62affeef9d32e -https://conda.anaconda.org/conda-forge/noarch/r-jquerylib-0.1.4-r43hc72bb7e_3.conda#39eb4928bdd8752b548f7cbe8fa7cabd -https://conda.anaconda.org/conda-forge/noarch/r-evaluate-1.0.4-r43hc72bb7e_0.conda#483ee1d772c6a47e28e2bf7b3e161ebe -https://conda.anaconda.org/conda-forge/linux-64/r-xfun-0.52-r43h93ab643_0.conda#5f6b312d62bad138ba4d54869a3b1c81 -https://conda.anaconda.org/conda-forge/noarch/r-highr-0.11-r43hc72bb7e_1.conda#23e4d2048f51cbe7c0fb8b9230edc701 -https://conda.anaconda.org/conda-forge/linux-64/r-yaml-2.3.10-r43hdb488b9_0.conda#ad2267cd6e74c87f2720494ea13f0fa4 -https://conda.anaconda.org/conda-forge/noarch/r-knitr-1.50-r43hc72bb7e_0.conda#8223b7a4d358fa5b5f8403c33885be13 -https://conda.anaconda.org/conda-forge/linux-64/pandoc-3.7.0.2-ha770c72_0.conda#db0c1632047d38997559ce2c4741dd91 -https://conda.anaconda.org/conda-forge/linux-64/r-cachem-1.1.0-r43hb1dbf0f_1.conda#02b195910b59c2cfd1fb7159edbb047a -https://conda.anaconda.org/conda-forge/linux-64/r-jsonlite-2.0.0-r43h2b5f3a1_0.conda#576ceaa3103ec089c0314e717d74e4de -https://conda.anaconda.org/conda-forge/linux-64/r-cli-3.6.5-r43h93ab643_0.conda#5d49a07fdd4ca869c4a79082692c4d2a -https://conda.anaconda.org/conda-forge/linux-64/r-glue-1.8.0-r43h2b5f3a1_0.conda#381d612db7519f2a54f1b187e738ac7b -https://conda.anaconda.org/conda-forge/noarch/r-lifecycle-1.0.4-r43hc72bb7e_1.conda#7a0a8ba1fe2cf12b39062d8291e2fca8 -https://conda.anaconda.org/conda-forge/noarch/r-memoise-2.0.1-r43hc72bb7e_3.conda#98e3d2eb6635a5f7b8af487f47184a98 -https://conda.anaconda.org/conda-forge/linux-64/r-mime-0.13-r43h2b5f3a1_0.conda#908a1d0ff246fcf4040de60d49b08049 -https://conda.anaconda.org/conda-forge/linux-64/r-fs-1.6.6-r43h93ab643_0.conda#6a5d79631aeaeef651fbfd7cbf5a954d -https://conda.anaconda.org/conda-forge/noarch/r-r6-2.6.1-r43hc72bb7e_0.conda#be02712c703445dc5cabbe0f22d0d063 -https://conda.anaconda.org/conda-forge/linux-64/r-rappdirs-0.3.3-r43hb1dbf0f_3.conda#9fb3dd1c37f2b0d351850edf594fb1a9 -https://conda.anaconda.org/conda-forge/linux-64/r-sass-0.4.10-r43h93ab643_0.conda#7e74b09775521a8ac1464c413e3c2646 -https://conda.anaconda.org/conda-forge/noarch/r-bslib-0.9.0-r43hc72bb7e_0.conda#7d028d04efd751bc558a6aaab0940f15 -https://conda.anaconda.org/conda-forge/noarch/r-fontawesome-0.5.3-r43hc72bb7e_0.conda#e2061ac4dd2fcbcd59611167f03eec19 -https://conda.anaconda.org/conda-forge/noarch/r-tinytex-0.57-r43hc72bb7e_0.conda#1986e92b398b17e8c88353dc2e444939 -https://conda.anaconda.org/conda-forge/noarch/r-rmarkdown-2.29-r43hc72bb7e_0.conda#5ecdb9c42acf2f0730c9793dfac09b8d -https://conda.anaconda.org/conda-forge/noarch/r-bookdown-0.43-r43hc72bb7e_0.conda#4afcb86c5eeabd3be77a46f6703e3971 -https://conda.anaconda.org/bioconda/noarch/bioconductor-biocstyle-2.30.0-r43hdfd78af_0.tar.bz2#2fdfd5cd16c84e0d71d7ffb3be4e54d3 -https://conda.anaconda.org/conda-forge/linux-64/oniguruma-6.9.10-hb9d3cd8_0.conda#6ce853cb231f18576d2db5c2d4cb473e -https://conda.anaconda.org/conda-forge/linux-64/jq-1.8.1-h73b1eb8_0.conda#2714e43bfc035f7ef26796632aa1b523 -https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2#4cb3ad778ec2d5a7acbdf254eb1c42ae -https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py313h8060acc_2.conda#50992ba61a8a1f8c2d346168ae1c86df -https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e -https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda#b0dd904de08b7db706167240bf37b164 -https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.13.3-pyha770c72_0.conda#146402bf0f11cbeb8f781fa4309a95d3 -https://conda.anaconda.org/conda-forge/noarch/xmltodict-0.14.2-pyhd8ed1ab_1.conda#96ef17b8734b174d35346da0762f0137 -https://conda.anaconda.org/conda-forge/noarch/yq-3.4.3-pyhe01879c_2.conda#18cefe7c50c1228da474ea0e95a8e646 -https://conda.anaconda.org/bioconda/noarch/bioconductor-data-packages-20250625-hdfd78af_0.tar.bz2#34d7066b99d7e6769305dcebf0a9de87 -https://conda.anaconda.org/bioconda/linux-64/bioconductor-s4vectors-0.40.2-r43ha9d7317_2.tar.bz2#6aa465e83dabb7ed5b853519d8a334e4 -https://conda.anaconda.org/bioconda/linux-64/bioconductor-iranges-2.36.0-r43ha9d7317_2.tar.bz2#cca51afd40439bea147c1adf9857bec0 -https://conda.anaconda.org/conda-forge/linux-64/r-matrixstats-1.5.0-r43h2b5f3a1_0.conda#bbf709a87ed6a14852cb0a4171539a06 -https://conda.anaconda.org/bioconda/noarch/bioconductor-matrixgenerics-1.14.0-r43hdfd78af_3.tar.bz2#c79f36cc0cd464874aefd50a700d0079 -https://conda.anaconda.org/conda-forge/noarch/r-abind-1.4_5-r43hc72bb7e_1006.conda#75d26096ffa98e1cde7b27b9530899a1 -https://conda.anaconda.org/conda-forge/noarch/r-crayon-1.5.3-r43hc72bb7e_1.conda#bafc77be1942ea00228cf18d2cb30e35 -https://conda.anaconda.org/conda-forge/linux-64/r-lattice-0.22_7-r43h2b5f3a1_0.conda#1902233545ef5232dacdd973153d77c4 -https://conda.anaconda.org/conda-forge/linux-64/r-matrix-1.6_5-r43he966344_1.conda#df8a1175a62460e02dbf340966cbfeab -https://conda.anaconda.org/bioconda/linux-64/bioconductor-s4arrays-1.2.0-r43ha9d7317_2.tar.bz2#28fd3fe7fd8d087c1cfa7805bbd16661 -https://conda.anaconda.org/bioconda/linux-64/bioconductor-zlibbioc-1.48.0-r43ha9d7317_2.tar.bz2#b460a5493c1d67ff386a0e63eb078a64 -https://conda.anaconda.org/bioconda/linux-64/bioconductor-xvector-0.42.0-r43ha9d7317_2.tar.bz2#16f45b1c97517cc3d063a442a43689a4 -https://conda.anaconda.org/bioconda/linux-64/bioconductor-sparsearray-1.2.2-r43ha9d7317_2.tar.bz2#41f1e8c1cfb7ff594e923c05e02d9ecb -https://conda.anaconda.org/bioconda/linux-64/bioconductor-delayedarray-0.28.0-r43ha9d7317_2.tar.bz2#cec6a218547ee2af2b823957a373a655 -https://conda.anaconda.org/conda-forge/linux-64/r-statmod-1.5.0-r43ha36c22a_2.conda#d1b3431cbf858fec53e7eb00f8b8cde0 -https://conda.anaconda.org/bioconda/linux-64/bioconductor-limma-3.58.1-r43ha9d7317_1.tar.bz2#c8af3f878cedd1c3c4b6a61a722cddc0 -https://conda.anaconda.org/bioconda/noarch/bioconductor-genomeinfodbdata-1.2.11-r43hdfd78af_1.tar.bz2#14721a7fde8cfe4703796dfd5a119d76 -https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.8-h4bc477f_0.conda#14dbe05b929e329dbaa6f2d0aa19466d -https://conda.anaconda.org/conda-forge/linux-64/r-bitops-1.0_9-r43h2b5f3a1_0.conda#8643d84c1d28ea73e48db9deb9a2eff3 -https://conda.anaconda.org/conda-forge/linux-64/r-rcurl-1.98_1.16-r43he8228da_1.conda#e03c3ff98b32efffb620d7dec4df34b1 -https://conda.anaconda.org/bioconda/noarch/bioconductor-genomeinfodb-1.38.1-r43hdfd78af_1.tar.bz2#03e20a01b672b693c9470dec80d83993 -https://conda.anaconda.org/bioconda/linux-64/bioconductor-genomicranges-1.54.1-r43ha9d7317_2.tar.bz2#01031256b035b2d4a15c14b690be39aa -https://conda.anaconda.org/bioconda/noarch/bioconductor-summarizedexperiment-1.32.0-r43hdfd78af_0.tar.bz2#6bed161da6d64cef9f9ebd5fbc2452e7 -https://conda.anaconda.org/conda-forge/linux-64/r-curl-6.2.2-r43h2700575_0.conda#c41790376a6567e34b7e8c04c04d9ea2 -https://conda.anaconda.org/conda-forge/linux-64/r-sys-3.4.3-r43h2b5f3a1_0.conda#b7ce9f99da446a47b97950ff9a9cbb60 -https://conda.anaconda.org/conda-forge/linux-64/r-askpass-1.2.1-r43h2b5f3a1_0.conda#8ccad521ab24a75928dd54af1e42632d -https://conda.anaconda.org/conda-forge/linux-64/r-openssl-2.3.3-r43he8289e2_0.conda#00a4cd47c633cd3c83f3e5e960b3ddf5 -https://conda.anaconda.org/conda-forge/noarch/r-httr-1.4.7-r43hc72bb7e_1.conda#746050b53c705dbe5ac9c5fbce51737a -https://conda.anaconda.org/conda-forge/linux-64/r-xml-3.99_0.17-r43h5bae778_2.conda#0b6f80438b17c41128f8e00a15aae22d -https://conda.anaconda.org/conda-forge/linux-64/r-xml2-1.3.8-r43h1bb2df6_0.conda#0dd4c0d50e2770cb8fab723a08c50f0c -https://conda.anaconda.org/bioconda/noarch/bioconductor-expressionatlas-1.30.0-r43hdfd78af_0.tar.bz2#50c241f6b09864ffe8e25ed80ffb3b64 -https://conda.anaconda.org/conda-forge/noarch/pip-25.1.1-pyh145f28c_0.conda#01384ff1639c6330a0924791413b8714 -https://conda.anaconda.org/conda-forge/noarch/r-getopt-1.20.4-r43ha770c72_1.conda#cf6793c369dbc7ef63d9c1bc9b186615 -https://conda.anaconda.org/conda-forge/noarch/r-optparse-1.7.5-r43hc72bb7e_1.conda#ae32080aac0f74e73e7cd6e774db1c73 diff --git a/modules/local/genorm/compute_m_measure/environment.yml b/modules/local/genorm/compute_m_measure/environment.yml new file mode 100644 index 00000000..4fa78808 --- /dev/null +++ b/modules/local/genorm/compute_m_measure/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::polars==1.35.2 diff --git a/modules/local/genorm/compute_m_measure/main.nf b/modules/local/genorm/compute_m_measure/main.nf index 07aba914..a75c9c3f 100644 --- a/modules/local/genorm/compute_m_measure/main.nf +++ b/modules/local/genorm/compute_m_measure/main.nf @@ -2,7 +2,7 @@ process COMPUTE_M_MEASURE { label 'process_medium' - conda "${moduleDir}/spec-file.txt" + 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/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" diff --git a/modules/local/genorm/compute_m_measure/spec-file.txt b/modules/local/genorm/compute_m_measure/spec-file.txt deleted file mode 100644 index 0ffb80b1..00000000 --- a/modules/local/genorm/compute_m_measure/spec-file.txt +++ /dev/null @@ -1,32 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_5.conda#dcd5ff1940cd38f6df777cac86819d60 -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_5.conda#264fbfba7fb20acf3b29cde153e345ce -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda#0be7c6e070c19105f966d3758448d018 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda#4211416ecba1866fab0c6470986c22d6 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda#0b367fad34931cb79e0d6b7e5c06bb1c -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.1-he9a06e4_0.conda#af930c65e9a79a3423d6d36e265cef65 -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda#74784ee3d225fc3dca89edb635b4e5cc -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda#ffffb341206dd0dab0c36053c048d621 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.13.7-h2b335a9_100_cp313.conda#724dcf9960e933838247971da07fe5cf -https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.7-py313hd8ed1ab_100.conda#c5623ddbd37c5dafa7754a83f97de01e -https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.7-h4df99d1_100.conda#47a123ca8e727d886a2c6d0c71658f8c -https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda#aaa2a381ccc56eac91d63b6c1240312f -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_5.conda#4e02a49aaa9d5190cb630fa43528fbe6 -https://conda.anaconda.org/conda-forge/noarch/pip-25.2-pyh145f28c_0.conda#e7ab34d5a93e0819b62563c78635d937 -https://conda.anaconda.org/conda-forge/linux-64/polars-default-1.33.1-py39hf521cc8_0.conda#900f486d119d5c83d14c812068a3ecad -https://conda.anaconda.org/conda-forge/linux-64/polars-1.33.1-default_h755bcc6_0.conda#1884a1a6acc457c8e4b59b0f6450e140 diff --git a/modules/local/genorm/cross_join/environment.yml b/modules/local/genorm/cross_join/environment.yml new file mode 100644 index 00000000..4fa78808 --- /dev/null +++ b/modules/local/genorm/cross_join/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::polars==1.35.2 diff --git a/modules/local/genorm/cross_join/main.nf b/modules/local/genorm/cross_join/main.nf index e62281ef..98545e6d 100644 --- a/modules/local/genorm/cross_join/main.nf +++ b/modules/local/genorm/cross_join/main.nf @@ -2,7 +2,7 @@ process CROSS_JOIN { label 'process_low' - conda "${moduleDir}/spec-file.txt" + 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/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" diff --git a/modules/local/genorm/cross_join/spec-file.txt b/modules/local/genorm/cross_join/spec-file.txt deleted file mode 100644 index 0ffb80b1..00000000 --- a/modules/local/genorm/cross_join/spec-file.txt +++ /dev/null @@ -1,32 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_5.conda#dcd5ff1940cd38f6df777cac86819d60 -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_5.conda#264fbfba7fb20acf3b29cde153e345ce -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda#0be7c6e070c19105f966d3758448d018 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda#4211416ecba1866fab0c6470986c22d6 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda#0b367fad34931cb79e0d6b7e5c06bb1c -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.1-he9a06e4_0.conda#af930c65e9a79a3423d6d36e265cef65 -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda#74784ee3d225fc3dca89edb635b4e5cc -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda#ffffb341206dd0dab0c36053c048d621 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.13.7-h2b335a9_100_cp313.conda#724dcf9960e933838247971da07fe5cf -https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.7-py313hd8ed1ab_100.conda#c5623ddbd37c5dafa7754a83f97de01e -https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.7-h4df99d1_100.conda#47a123ca8e727d886a2c6d0c71658f8c -https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda#aaa2a381ccc56eac91d63b6c1240312f -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_5.conda#4e02a49aaa9d5190cb630fa43528fbe6 -https://conda.anaconda.org/conda-forge/noarch/pip-25.2-pyh145f28c_0.conda#e7ab34d5a93e0819b62563c78635d937 -https://conda.anaconda.org/conda-forge/linux-64/polars-default-1.33.1-py39hf521cc8_0.conda#900f486d119d5c83d14c812068a3ecad -https://conda.anaconda.org/conda-forge/linux-64/polars-1.33.1-default_h755bcc6_0.conda#1884a1a6acc457c8e4b59b0f6450e140 diff --git a/modules/local/genorm/expression_ratio/environment.yml b/modules/local/genorm/expression_ratio/environment.yml new file mode 100644 index 00000000..4fa78808 --- /dev/null +++ b/modules/local/genorm/expression_ratio/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::polars==1.35.2 diff --git a/modules/local/genorm/expression_ratio/main.nf b/modules/local/genorm/expression_ratio/main.nf index 5eab94eb..fd031ee0 100644 --- a/modules/local/genorm/expression_ratio/main.nf +++ b/modules/local/genorm/expression_ratio/main.nf @@ -2,7 +2,7 @@ process EXPRESSION_RATIO { label 'process_low' - conda "${moduleDir}/spec-file.txt" + 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/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" diff --git a/modules/local/genorm/expression_ratio/spec-file.txt b/modules/local/genorm/expression_ratio/spec-file.txt deleted file mode 100644 index 0ffb80b1..00000000 --- a/modules/local/genorm/expression_ratio/spec-file.txt +++ /dev/null @@ -1,32 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_5.conda#dcd5ff1940cd38f6df777cac86819d60 -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_5.conda#264fbfba7fb20acf3b29cde153e345ce -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda#0be7c6e070c19105f966d3758448d018 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda#4211416ecba1866fab0c6470986c22d6 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda#0b367fad34931cb79e0d6b7e5c06bb1c -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.1-he9a06e4_0.conda#af930c65e9a79a3423d6d36e265cef65 -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda#74784ee3d225fc3dca89edb635b4e5cc -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda#ffffb341206dd0dab0c36053c048d621 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.13.7-h2b335a9_100_cp313.conda#724dcf9960e933838247971da07fe5cf -https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.7-py313hd8ed1ab_100.conda#c5623ddbd37c5dafa7754a83f97de01e -https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.7-h4df99d1_100.conda#47a123ca8e727d886a2c6d0c71658f8c -https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda#aaa2a381ccc56eac91d63b6c1240312f -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_5.conda#4e02a49aaa9d5190cb630fa43528fbe6 -https://conda.anaconda.org/conda-forge/noarch/pip-25.2-pyh145f28c_0.conda#e7ab34d5a93e0819b62563c78635d937 -https://conda.anaconda.org/conda-forge/linux-64/polars-default-1.33.1-py39hf521cc8_0.conda#900f486d119d5c83d14c812068a3ecad -https://conda.anaconda.org/conda-forge/linux-64/polars-1.33.1-default_h755bcc6_0.conda#1884a1a6acc457c8e4b59b0f6450e140 diff --git a/modules/local/genorm/make_chunks/environment.yml b/modules/local/genorm/make_chunks/environment.yml new file mode 100644 index 00000000..4fa78808 --- /dev/null +++ b/modules/local/genorm/make_chunks/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::polars==1.35.2 diff --git a/modules/local/genorm/make_chunks/main.nf b/modules/local/genorm/make_chunks/main.nf index cc959731..fc877c24 100644 --- a/modules/local/genorm/make_chunks/main.nf +++ b/modules/local/genorm/make_chunks/main.nf @@ -2,7 +2,7 @@ process MAKE_CHUNKS { label 'process_medium' - conda "${moduleDir}/spec-file.txt" + 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/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" diff --git a/modules/local/genorm/make_chunks/spec-file.txt b/modules/local/genorm/make_chunks/spec-file.txt deleted file mode 100644 index 0ffb80b1..00000000 --- a/modules/local/genorm/make_chunks/spec-file.txt +++ /dev/null @@ -1,32 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_5.conda#dcd5ff1940cd38f6df777cac86819d60 -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_5.conda#264fbfba7fb20acf3b29cde153e345ce -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda#0be7c6e070c19105f966d3758448d018 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda#4211416ecba1866fab0c6470986c22d6 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda#0b367fad34931cb79e0d6b7e5c06bb1c -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.1-he9a06e4_0.conda#af930c65e9a79a3423d6d36e265cef65 -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda#74784ee3d225fc3dca89edb635b4e5cc -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda#ffffb341206dd0dab0c36053c048d621 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.13.7-h2b335a9_100_cp313.conda#724dcf9960e933838247971da07fe5cf -https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.7-py313hd8ed1ab_100.conda#c5623ddbd37c5dafa7754a83f97de01e -https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.7-h4df99d1_100.conda#47a123ca8e727d886a2c6d0c71658f8c -https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda#aaa2a381ccc56eac91d63b6c1240312f -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_5.conda#4e02a49aaa9d5190cb630fa43528fbe6 -https://conda.anaconda.org/conda-forge/noarch/pip-25.2-pyh145f28c_0.conda#e7ab34d5a93e0819b62563c78635d937 -https://conda.anaconda.org/conda-forge/linux-64/polars-default-1.33.1-py39hf521cc8_0.conda#900f486d119d5c83d14c812068a3ecad -https://conda.anaconda.org/conda-forge/linux-64/polars-1.33.1-default_h755bcc6_0.conda#1884a1a6acc457c8e4b59b0f6450e140 diff --git a/modules/local/genorm/ratio_standard_variation/environment.yml b/modules/local/genorm/ratio_standard_variation/environment.yml new file mode 100644 index 00000000..4fa78808 --- /dev/null +++ b/modules/local/genorm/ratio_standard_variation/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::polars==1.35.2 diff --git a/modules/local/genorm/ratio_standard_variation/main.nf b/modules/local/genorm/ratio_standard_variation/main.nf index 60645212..e92cd098 100644 --- a/modules/local/genorm/ratio_standard_variation/main.nf +++ b/modules/local/genorm/ratio_standard_variation/main.nf @@ -2,7 +2,7 @@ process RATIO_STANDARD_VARIATION { label 'process_low' - conda "${moduleDir}/spec-file.txt" + 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/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" diff --git a/modules/local/genorm/ratio_standard_variation/spec-file.txt b/modules/local/genorm/ratio_standard_variation/spec-file.txt deleted file mode 100644 index 0ffb80b1..00000000 --- a/modules/local/genorm/ratio_standard_variation/spec-file.txt +++ /dev/null @@ -1,32 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_5.conda#dcd5ff1940cd38f6df777cac86819d60 -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_5.conda#264fbfba7fb20acf3b29cde153e345ce -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda#0be7c6e070c19105f966d3758448d018 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda#4211416ecba1866fab0c6470986c22d6 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda#0b367fad34931cb79e0d6b7e5c06bb1c -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.1-he9a06e4_0.conda#af930c65e9a79a3423d6d36e265cef65 -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda#74784ee3d225fc3dca89edb635b4e5cc -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda#ffffb341206dd0dab0c36053c048d621 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.13.7-h2b335a9_100_cp313.conda#724dcf9960e933838247971da07fe5cf -https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.7-py313hd8ed1ab_100.conda#c5623ddbd37c5dafa7754a83f97de01e -https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.7-h4df99d1_100.conda#47a123ca8e727d886a2c6d0c71658f8c -https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda#aaa2a381ccc56eac91d63b6c1240312f -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_5.conda#4e02a49aaa9d5190cb630fa43528fbe6 -https://conda.anaconda.org/conda-forge/noarch/pip-25.2-pyh145f28c_0.conda#e7ab34d5a93e0819b62563c78635d937 -https://conda.anaconda.org/conda-forge/linux-64/polars-default-1.33.1-py39hf521cc8_0.conda#900f486d119d5c83d14c812068a3ecad -https://conda.anaconda.org/conda-forge/linux-64/polars-1.33.1-default_h755bcc6_0.conda#1884a1a6acc457c8e4b59b0f6450e140 diff --git a/modules/local/geo/getaccessions/environment.yml b/modules/local/geo/getaccessions/environment.yml new file mode 100644 index 00000000..d2475405 --- /dev/null +++ b/modules/local/geo/getaccessions/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: + - conda-forge::pandas==2.3.3 + - conda-forge::requests==2.32.5 + - conda-forge::tenacity==9.1.2 + - conda-forge::nltk==3.9.2 + - conda-forge::tqdm==4.67.1 + - conda-forge::xmltodict==1.0.2 + - conda-forge::biopython==1.86 diff --git a/modules/local/geo/getaccessions/main.nf b/modules/local/geo/getaccessions/main.nf index 18ad7af3..078a4407 100644 --- a/modules/local/geo/getaccessions/main.nf +++ b/modules/local/geo/getaccessions/main.nf @@ -4,7 +4,7 @@ process GEO_GETACCESSIONS { tag "${species}" - conda "${moduleDir}/spec-file.txt" + 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/ca/caae35ec5dc72367102a616a47b6f1a7b3de9ff272422f2c08895b8bb5f0566c/data': 'community.wave.seqera.io/library/biopython_nltk_pandas_parallelbar_pruned:5fc501b07f8e0428' }" @@ -24,10 +24,9 @@ process GEO_GETACCESSIONS { tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('requests'), eval('python3 -c "import requests; print(requests.__version__)"'), topic: versions tuple val("${task.process}"), val('nltk'), eval('python3 -c "import nltk; print(nltk.__version__)"'), topic: versions - tuple val("${task.process}"), val('pyyaml'), eval('python3 -c "import yaml; print(yaml.__version__)"'), topic: versions tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions - tuple val("${task.process}"), val('xmltodict'), eval('python3 -c "import xmltodict; print(xmltodict.__version__)"'), topic: versions tuple val("${task.process}"), val('biopython'), eval('python3 -c "import Bio; print(Bio.__version__)"'), topic: versions + tuple val("${task.process}"), val('tqdm'), eval('python3 -c "import tqdm; print(tqdm.__version__)"'), topic: versions script: def keywords_string = keywords.split(',').collect { it.trim() }.join(' ') diff --git a/modules/local/geo/getaccessions/spec-file.txt b/modules/local/geo/getaccessions/spec-file.txt deleted file mode 100644 index ede7a99d..00000000 --- a/modules/local/geo/getaccessions/spec-file.txt +++ /dev/null @@ -1,74 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://repo.anaconda.com/pkgs/main/linux-64/_libgcc_mutex-0.1-main.conda#c3473ff8bdb3d124ed5ff11ec380d6f9 -https://repo.anaconda.com/pkgs/main/linux-64/_openmp_mutex-5.1-1_gnu.conda#71d281e9c2192cb3fa425655a8defb85 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_4.conda#f406dcbb2e7bef90d793e50e79a2882b -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_4.conda#8a4ab7ff06e4db0be22485332666da0f -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_4.conda#53e876bc2d2648319e94c33c57b9ec74 -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_2.conda#dfc5aae7b043d9f56ba99514d5e60625 -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-34_h59b9bed_openblas.conda#064c22bac20fecf2a99838f9b979374c -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-34_he106b2a_openblas.conda#148b531b5457ad666ed76ceb4c766505 -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-34_h7ac8fdf_openblas.conda#f05a31377b4d9a8d8740f47d1e70b70e -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_4.conda#3c376af8888c386b9d3d1c2701e2f3ab -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_4.conda#28771437ffcd9f3417c66012dc49a3be -https://repo.anaconda.com/pkgs/main/linux-64/bzip2-1.0.8-h5eee18b_6.conda#f21a3ff51c1b271977f53ce956a69297 -https://repo.anaconda.com/pkgs/main/linux-64/libstdcxx-ng-11.2.0-h1234567_1.conda#57623d10a70e09e1d048c2b2b6f4e2dd -https://repo.anaconda.com/pkgs/main/linux-64/expat-2.7.1-h6a678d5_0.conda#269942a9f3f943e2e5d8a2516a861f7c -https://repo.anaconda.com/pkgs/main/linux-64/ld_impl_linux-64-2.40-h12ee557_0.conda#ee672b5f635340734f58d618b7bca024 -https://repo.anaconda.com/pkgs/main/linux-64/libffi-3.4.4-h6a678d5_1.conda#70646cc713f0c43926cfdcfe9b695fe0 -https://repo.anaconda.com/pkgs/main/linux-64/libmpdec-4.0.0-h5eee18b_0.conda#feb10f42b1a7b523acbf85461be41a3e -https://repo.anaconda.com/pkgs/main/linux-64/libuuid-1.41.5-h5eee18b_0.conda#4a6a2354414c9080327274aa514e5299 -https://repo.anaconda.com/pkgs/main/linux-64/ncurses-6.5-h7934f7d_0.conda#0abfc090299da4bb031b84c64309757b -https://repo.anaconda.com/pkgs/main/linux-64/ca-certificates-2025.7.15-h06a4308_0.conda#a65eaddc4f9529b9c908f544ca50e7e0 -https://repo.anaconda.com/pkgs/main/linux-64/openssl-3.0.17-h5eee18b_0.conda#c032152f4080dd61875d5047641c8bf2 -https://repo.anaconda.com/pkgs/main/linux-64/python_abi-3.13-0_cp313.conda#d4009c49dd2b54ffded7f1365b5f6505 -https://repo.anaconda.com/pkgs/main/linux-64/readline-8.3-hc2a1206_0.conda#8578e006d4ef5cb98a6cda232b3490f6 -https://repo.anaconda.com/pkgs/main/linux-64/zlib-1.2.13-h5eee18b_1.conda#92e42d8310108b0a440fb2e60b2b2a25 -https://repo.anaconda.com/pkgs/main/linux-64/sqlite-3.50.2-hb25bd0a_1.conda#6ac08aa6b5f14911039aa04b2b2c3350 -https://repo.anaconda.com/pkgs/main/linux-64/pthread-stubs-0.3-h0ce48e5_1.conda#973a642312d2a28927aaf5b477c67250 -https://repo.anaconda.com/pkgs/main/linux-64/xorg-libxau-1.0.12-h9b100fa_0.conda#a8005a9f6eb903e113cd5363e8a11459 -https://repo.anaconda.com/pkgs/main/linux-64/xorg-libxdmcp-1.1.5-h9b100fa_0.conda#c284a09ddfba81d9c4e740110f09ea06 -https://repo.anaconda.com/pkgs/main/linux-64/libxcb-1.17.0-h9b100fa_0.conda#fdf0d380fa3809a301e2dbc0d5183883 -https://repo.anaconda.com/pkgs/main/linux-64/xorg-xorgproto-2024.1-h5eee18b_1.conda#412a0d97a7a51d23326e57226189da92 -https://repo.anaconda.com/pkgs/main/linux-64/xorg-libx11-1.8.12-h9b100fa_1.conda#6298b27afae6f49f03765b2a03df2fcb -https://repo.anaconda.com/pkgs/main/linux-64/tk-8.6.15-h54e0aa7_0.conda#1fa91e0c4fc9c9435eda3f1a25a676fd -https://repo.anaconda.com/pkgs/main/noarch/tzdata-2025b-h04d1e81_0.conda#1d027393db3427ab22a02aa44a56f143 -https://repo.anaconda.com/pkgs/main/linux-64/xz-5.6.4-h5eee18b_1.conda#3581505fa450962d631bd82b8616350e -https://repo.anaconda.com/pkgs/main/linux-64/python-3.13.5-h4612cfd_100_cp313.conda#1adf42b71c42a4a540eae2c0026f02c3 -https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.2-py313hf6604e3_2.conda#67d27f74a90f5f0336035203f91a0abc -https://conda.anaconda.org/conda-forge/linux-64/biopython-1.85-py313h07c4f96_2.conda#376f132b855fa7879f361c8c523c0768 -https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py313h7033f15_4.conda#bc8624c405856b1d047dd0a81829b08c -https://conda.anaconda.org/conda-forge/noarch/certifi-2025.8.3-pyhd8ed1ab_0.conda#11f59985f49df4620890f3e746ed7102 -https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda#12c566707c80111f9799308d9e265aef -https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py313hfab6e84_0.conda#ce6386a5892ef686d6d680c345c40ad1 -https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.3-pyhd8ed1ab_0.conda#7e7d5ef1b9ed630e4a1c358d6bc62284 -https://conda.anaconda.org/conda-forge/noarch/click-8.2.1-pyh707e725_0.conda#94b550b8d3a614dbd326af798c7dfb40 -https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda#962b9857ee8e7018c22f2776ffa0b2d7 -https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda#0a802cb9888dd14eeefc611f05c40b6e -https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda#8e6923fc12f1fe8f8c4e5c9f343256ac -https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda#164fc43f0b53b6e3a7bc7dce5e4f1dc9 -https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda#39a4f67be3286c86d696df570b1201b7 -https://repo.anaconda.com/pkgs/main/linux-64/setuptools-78.1.1-py313h06a4308_0.conda#8f8e1c1e3af9d2d371aaa0ee8316ae7c -https://conda.anaconda.org/conda-forge/noarch/joblib-1.5.2-pyhd8ed1ab_0.conda#4e717929cfa0d49cef92d911e31d0e90 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_4.conda#3baf8976c96134738bba224e9ef6b1e5 -https://conda.anaconda.org/conda-forge/noarch/nltk-3.9.1-pyhd8ed1ab_1.conda#85fd21c82d46f871d3820c17270e575d -https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda#3339e3b65d58accf4ca4fb8748ab16b3 -https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 -https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 -https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 -https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.2-py313h08cd8bf_0.conda#5f4cc42e08d6d862b7b919a3c8959e0b -https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda#9efbfdc37242619130ea42b1cc4ed861 -https://conda.anaconda.org/conda-forge/noarch/parallelbar-2.5-pyhd8ed1ab_0.conda#984636bb8e6681a56b71fc09a911c3b3 -https://repo.anaconda.com/pkgs/main/linux-64/wheel-0.45.1-py313h06a4308_0.conda#29057e876eedce0e37c2388c138a19f9 -https://repo.anaconda.com/pkgs/main/noarch/pip-25.2-pyhc872135_0.conda#b829d36091ab08d18cafe8994ac6e02b -https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda#461219d1a5bd61342293efa2c0c90eac -https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda#a77f85f77be52ff59391544bfe73390a -https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py313h8060acc_2.conda#50992ba61a8a1f8c2d346168ae1c86df -https://conda.anaconda.org/conda-forge/linux-64/regex-2025.7.34-py313h07c4f96_1.conda#bad6ae3c034586b998bd8901ca76915a -https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py313h07c4f96_3.conda#0720da5e63f3c93647350cc217fdf2bc -https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda#436c165519e140cb08d246a4472a9d6a -https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda#db0c6b99149880c8ba515cf4abe93ee4 -https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.2-pyhd8ed1ab_0.conda#5d99943f2ae3cc69e1ada12ce9d4d701 -https://conda.anaconda.org/conda-forge/noarch/xmltodict-0.14.2-pyhd8ed1ab_1.conda#96ef17b8734b174d35346da0762f0137 diff --git a/modules/local/geo/getdata/environment.yml b/modules/local/geo/getdata/environment.yml new file mode 100644 index 00000000..1a45cb95 --- /dev/null +++ b/modules/local/geo/getdata/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: + - conda-forge::r-base==4.4.3 + - conda-forge::r-optparse==1.7.5 + - conda-forge::r-dplyr==1.1.4 + - bioconda::bioconductor-geoquery==2.74.0 diff --git a/modules/local/geo/getdata/main.nf b/modules/local/geo/getdata/main.nf index 0a85f845..f848dd1c 100644 --- a/modules/local/geo/getdata/main.nf +++ b/modules/local/geo/getdata/main.nf @@ -6,7 +6,7 @@ process GEO_GETDATA { maxForks 8 // limiting to 8 threads at a time to avoid 429 errors with the NCBI server - conda "${moduleDir}/spec-file.txt" + 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/4c/4cb08d96e62942e7b6288abf2cfd30e813521a022459700e610325a3a7c0b1c8/data': 'community.wave.seqera.io/library/bioconductor-geoquery_r-base_r-dplyr_r-optparse:fcd002470b7d6809' }" diff --git a/modules/local/geo/getdata/spec-file.txt b/modules/local/geo/getdata/spec-file.txt deleted file mode 100644 index 8bfecd20..00000000 --- a/modules/local/geo/getdata/spec-file.txt +++ /dev/null @@ -1,201 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_4.conda#3baf8976c96134738bba224e9ef6b1e5 -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/noarch/_r-mutex-1.0.1-anacondar_1.tar.bz2#19f9db5f4f1b7f5ef5f6d67207f25f38 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_4.conda#f406dcbb2e7bef90d793e50e79a2882b -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_4.conda#28771437ffcd9f3417c66012dc49a3be -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62ee74e96c5ebb0af99386de58cf9553 -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda#0be7c6e070c19105f966d3758448d018 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda#4211416ecba1866fab0c6470986c22d6 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda#0b367fad34931cb79e0d6b7e5c06bb1c -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda#74784ee3d225fc3dca89edb635b4e5cc -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda#ffffb341206dd0dab0c36053c048d621 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.13.7-h2b335a9_100_cp313.conda#724dcf9960e933838247971da07fe5cf -https://conda.anaconda.org/conda-forge/noarch/argcomplete-3.6.2-pyhd8ed1ab_0.conda#eb9d4263271ca287d2e0cf5a86da2d3a -https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-5.14.0-he073ed8_2.conda#0dedbff35a50868200993a2ccf051390 -https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.34-h087de78_2.conda#79592e1be84fccb8a117d9e7b9d01753 -https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.44-h4bf12b8_1.conda#e45cfedc8ca5630e02c106ea36d2c5c6 -https://conda.anaconda.org/conda-forge/linux-64/bwidget-1.10.1-ha770c72_1.conda#983b92277d78c0d0ec498e460caa0e6d -https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.50-h421ea60_1.conda#7af8e91b0deb5f8e25d1a595dea79614 -https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.13.3-h48d6fc4_1.conda#3c255be50a506c50765a93a6644f32fe -https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.13.3-ha770c72_1.conda#51f5be229d83ecd401fb369ab96ae669 -https://conda.anaconda.org/conda-forge/linux-64/freetype-2.13.3-ha770c72_1.conda#9ccd736d31e0c6e41f54e704e5312811 -https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.15.0-h7e30c49_1.conda#8f5b0b297b59e1ac160ad4beec99dbee -https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2#0c96522c6bdaed4b1566d11387caaf45 -https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2#34893075a5c9e55cdafac56607368fc6 -https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2#4d59c254e01d9cde7957100457e2d5fb -https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda#49023d73832ef61042f6a237cb2687e7 -https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29 -https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_4.conda#3c376af8888c386b9d3d1c2701e2f3ab -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_4.conda#2d34729cbc1da0ec988e57b13b712067 -https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 -https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda#915f5995e94f60e9a4826e0b0920ee88 -https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.45-hc749103_0.conda#b90bece58b4c2bf25969b70f3be42d25 -https://conda.anaconda.org/conda-forge/linux-64/libglib-2.84.3-hf39c6af_0.conda#467f23819b1ea2b89c3fc94d65082301 -https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda#b3c17d95b5a10c6e64a21fa17573e70e -https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb9d3cd8_0.conda#f6ebe2cb3f82ba6c057dde5d9debe4f7 -https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb9d3cd8_0.conda#8035c64cb77ed555e3f150b7b3972480 -https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda#92ed62436b625154323d40d5f2f11dd7 -https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_1.conda#c01af13bdc553d1a8fbfff6e8db075f0 -https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda#fb901ff28063514abb6046c9ec2c4a45 -https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda#1c74ff8c35dcadf952a16f752ca5aa49 -https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.12-h4f16b4b_0.conda#db038ce880f100acc74dba10302b5630 -https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.6-hb9d3cd8_0.conda#febbab7d15033c913d53c7a2c102309d -https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda#96d57aba173e878a2089d5638016dc5e -https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda#09262e66b19567aff4f592fb53b28760 -https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda#b38117a3c920364aff79f870c984b4a3 -https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda#c277e0a4d549b03ac1e9d6cbbe3d017b -https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda#3f43953b7d3fb3aaa1d0d0723d91e368 -https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.5-hb9d3cd8_0.conda#f7f0d6cc2dc986d42ac2689ec88192be -https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda#172bf1cd1ff8629f2b1179945ed45055 -https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda#b499ce4b026493a13774bcf0f4c33849 -https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda#eecce068c7e4eddeb169591baac20ac4 -https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda#6432cb5d4ac0046c3ac0a8a0f95842f9 -https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.14.1-h332b0f4_0.conda#45f6713cb00f124af300342512219182 -https://conda.anaconda.org/conda-forge/linux-64/curl-8.14.1-h332b0f4_0.conda#60279087a10b4ab59a70daa838894e4b -https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-15.1.0-h4c094af_104.conda#05eec361e8eca1ad47bad0f8b97a9d67 -https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-15.1.0-h97b714f_4.conda#9577e03ec70b7986ab78a3f057af0df8 -https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-15.1.0-h4393ad2_4.conda#bd50f28da1e011caf83ebfe967dbcc94 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_4.conda#8a4ab7ff06e4db0be22485332666da0f -https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-15.1.0-h3b9cdf2_4.conda#82f37031ba4df0e97a222646ddcfd673 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_4.conda#53e876bc2d2648319e94c33c57b9ec74 -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_2.conda#dfc5aae7b043d9f56ba99514d5e60625 -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-34_h59b9bed_openblas.conda#064c22bac20fecf2a99838f9b979374c -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-34_he106b2a_openblas.conda#148b531b5457ad666ed76ceb4c766505 -https://conda.anaconda.org/conda-forge/linux-64/gsl-2.7-he838d99_0.tar.bz2#fec079ba39c9cca093bf4c00001825de -https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-15.1.0-h4c094af_104.conda#608049d7d920f3c559197d4c5445d243 -https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-15.1.0-h6a1bac1_4.conda#f880f89a51a8f93ecdc3b82c4627dc99 -https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.24-h86f0d12_0.conda#64f0c503da58ec25ebd359e4d990afa8 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-15.1.0-h69a702a_4.conda#b1a97c0f2c4f1bb2b8872a21fc7e17a7 -https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.0-hb9d3cd8_0.conda#9fa334557db9f63da6c9285fd2a48638 -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-34_h7ac8fdf_openblas.conda#f05a31377b4d9a8d8740f47d1e70b70e -https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda#9344155d33912347b37f0ae6c410a835 -https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda#aea31d2e5b1091feca96fcfe945c3cf9 -https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.0-h8261f1e_6.conda#b6093922931b535a7ba566b6f384fbe6 -https://conda.anaconda.org/conda-forge/linux-64/make-4.4.1-hb9d3cd8_2.conda#33405d2a66b1411db9f7242c8b97c9e7 -https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.10-h36c2ea0_0.tar.bz2#ac7bc6a654f8f41b352b38f4051135f8 -https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-hecca717_2.conda#2cd94587f3a401ae05e03a6caf09539d -https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-11.4.5-h15599e2_0.conda#1276ae4aa3832a449fcb4253c30da4bc -https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.4-hadf4263_0.conda#79f71230c069a287efe3a8614069ddf1 -https://conda.anaconda.org/conda-forge/linux-64/sed-4.9-h6688a6e_0.conda#171afc5f7ca0408bbccbcb69ade85f92 -https://conda.anaconda.org/conda-forge/linux-64/tktable-2.10-h8d826fa_7.conda#3ac51142c19ba95ae0fadefa333c9afb -https://conda.anaconda.org/conda-forge/linux-64/xorg-libxt-1.3.1-hb9d3cd8_0.conda#279b0de5f6ba95457190a1c459a64e31 -https://conda.anaconda.org/conda-forge/linux-64/r-base-4.4.3-h85845a0_2.conda#d1573e5f701f21560e1fc6b206f0dc55 -https://conda.anaconda.org/bioconda/noarch/bioconductor-biocgenerics-0.52.0-r44hdfd78af_3.tar.bz2#8a9defade51c2c2a6b90a4474dcdfdfc -https://conda.anaconda.org/bioconda/linux-64/bioconductor-biobase-2.66.0-r44h3df3fcb_0.tar.bz2#56f651b4dbe8625ba510c7c3233da8a9 -https://conda.anaconda.org/bioconda/linux-64/bioconductor-s4vectors-0.44.0-r44h3df3fcb_2.tar.bz2#13bdbde9c9496802b7974d006f22fe11 -https://conda.anaconda.org/bioconda/linux-64/bioconductor-iranges-2.40.0-r44h3df3fcb_2.tar.bz2#f2e8f02a2987ccaf39a22c36fa0e9fa8 -https://conda.anaconda.org/conda-forge/linux-64/oniguruma-6.9.10-hb9d3cd8_0.conda#6ce853cb231f18576d2db5c2d4cb473e -https://conda.anaconda.org/conda-forge/linux-64/jq-1.8.1-h73b1eb8_0.conda#2714e43bfc035f7ef26796632aa1b523 -https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda#a77f85f77be52ff59391544bfe73390a -https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py313h8060acc_2.conda#50992ba61a8a1f8c2d346168ae1c86df -https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e -https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda#b0dd904de08b7db706167240bf37b164 -https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.13.3-pyha770c72_0.conda#146402bf0f11cbeb8f781fa4309a95d3 -https://conda.anaconda.org/conda-forge/noarch/xmltodict-0.14.2-pyhd8ed1ab_1.conda#96ef17b8734b174d35346da0762f0137 -https://conda.anaconda.org/conda-forge/noarch/yq-3.4.3-pyhe01879c_2.conda#18cefe7c50c1228da474ea0e95a8e646 -https://conda.anaconda.org/bioconda/noarch/bioconductor-data-packages-20250625-hdfd78af_0.tar.bz2#34d7066b99d7e6769305dcebf0a9de87 -https://conda.anaconda.org/bioconda/noarch/bioconductor-genomeinfodbdata-1.2.13-r44hdfd78af_0.tar.bz2#06d453df3bc59956a3ffac7674652f44 -https://conda.anaconda.org/conda-forge/linux-64/r-curl-7.0.0-r44h10955f1_0.conda#7f9ea9206c1eba58b56cdb49894c1a10 -https://conda.anaconda.org/conda-forge/linux-64/r-jsonlite-2.0.0-r44h2b5f3a1_0.conda#741243137a52f978739eff83126dc2bb -https://conda.anaconda.org/conda-forge/linux-64/r-mime-0.13-r44h2b5f3a1_0.conda#58856b0f45ffe6477a5a92cc2fc0843c -https://conda.anaconda.org/conda-forge/linux-64/r-sys-3.4.3-r44h2b5f3a1_0.conda#7771befc8f294d762f12a48dffa1650a -https://conda.anaconda.org/conda-forge/linux-64/r-askpass-1.2.1-r44h2b5f3a1_0.conda#6e2597e8d6c7e8c2d7dec6d50103d958 -https://conda.anaconda.org/conda-forge/linux-64/r-openssl-2.3.3-r44he8289e2_0.conda#216e57ec49b5079a1a5387f1482cad0f -https://conda.anaconda.org/conda-forge/noarch/r-r6-2.6.1-r44hc72bb7e_0.conda#08d1985cbe6bd96a818e127de51f9905 -https://conda.anaconda.org/conda-forge/noarch/r-httr-1.4.7-r44hc72bb7e_1.conda#9dd48155c67d87a2adc68ddb7c3ab508 -https://conda.anaconda.org/bioconda/linux-64/bioconductor-ucsc.utils-1.2.0-r44h9ee0642_1.tar.bz2#1653b8e9eb24949cc045e21ad21927b6 -https://conda.anaconda.org/bioconda/noarch/bioconductor-genomeinfodb-1.42.0-r44hdfd78af_2.tar.bz2#52f21aeff5a62bf53a37a4ad4d197e06 -https://conda.anaconda.org/bioconda/linux-64/bioconductor-zlibbioc-1.52.0-r44h3df3fcb_2.tar.bz2#1f08126acbb2d9a8835aba0b1a7bf9e2 -https://conda.anaconda.org/bioconda/linux-64/bioconductor-xvector-0.46.0-r44h15a9599_2.tar.bz2#1ce6efc6cb59e8ce10ec4cb722ece013 -https://conda.anaconda.org/conda-forge/noarch/r-crayon-1.5.3-r44hc72bb7e_1.conda#89626d77a94256b8304bb8fc33d3364c -https://conda.anaconda.org/bioconda/linux-64/bioconductor-biostrings-2.74.0-r44h3df3fcb_1.tar.bz2#964a5ef8fe0d0e9a599efab3fa7c3a71 -https://conda.anaconda.org/conda-forge/linux-64/r-png-0.1_8-r44h21f035c_2.conda#7726f2ccf765e2e70168df48920084d1 -https://conda.anaconda.org/bioconda/noarch/bioconductor-keggrest-1.46.0-r44hdfd78af_0.tar.bz2#dd1a6a8509dc43cec9a22754eec1bef3 -https://conda.anaconda.org/conda-forge/noarch/r-dbi-1.2.3-r44hc72bb7e_1.conda#15f0ce8bf5c00f734af73165feb59e13 -https://conda.anaconda.org/conda-forge/linux-64/r-bit-4.6.0-r44h2b5f3a1_0.conda#02e9bed2800ee2cc9e7506e808e6ef06 -https://conda.anaconda.org/conda-forge/linux-64/r-bit64-4.6.0_1-r44h2b5f3a1_0.conda#8875d4a17c3252891859c834c3925707 -https://conda.anaconda.org/conda-forge/linux-64/r-rlang-1.1.6-r44h93ab643_0.conda#b154e08c49d92f0f59b9236f189a8264 -https://conda.anaconda.org/conda-forge/linux-64/r-cli-3.6.5-r44h93ab643_0.conda#f1c2722622bb979e389c132212de9220 -https://conda.anaconda.org/conda-forge/linux-64/r-glue-1.8.0-r44h2b5f3a1_0.conda#990ecdd6246dfcce8cea4eb5a97ca735 -https://conda.anaconda.org/conda-forge/noarch/r-lifecycle-1.0.4-r44hc72bb7e_1.conda#66d0ba7c05d0abfa11d126ba8e2907aa -https://conda.anaconda.org/conda-forge/linux-64/r-vctrs-0.6.5-r44h0d4f4ea_1.conda#399bc7872bdb40b5531d887858253e76 -https://conda.anaconda.org/conda-forge/noarch/r-blob-1.2.4-r44hc72bb7e_2.conda#4f9161cb110a1e0d95c7631294fbba40 -https://conda.anaconda.org/conda-forge/noarch/r-cpp11-0.5.2-r44h785f33e_1.conda#c8f41b1d8dbbc1b057d282e59cbc46ca -https://conda.anaconda.org/conda-forge/linux-64/r-fastmap-1.2.0-r44ha18555a_1.conda#314840b54bb3397ea0dd118bab7bcb1e -https://conda.anaconda.org/conda-forge/linux-64/r-cachem-1.1.0-r44hb1dbf0f_1.conda#a8ac6cdc444baf323c90c47789f62387 -https://conda.anaconda.org/conda-forge/noarch/r-memoise-2.0.1-r44hc72bb7e_3.conda#3b5d996b00a781c3a08a05687ce55075 -https://conda.anaconda.org/conda-forge/noarch/r-pkgconfig-2.0.3-r44hc72bb7e_4.conda#17d36e686fb918637ca43fc4c036549e -https://conda.anaconda.org/conda-forge/noarch/r-plogr-0.2.0-r44hc72bb7e_1006.conda#4ff9218577c4dcf961a80afdaa851927 -https://conda.anaconda.org/conda-forge/linux-64/r-rsqlite-2.4.3-r44h3697838_0.conda#e2b405b74d9e946818fdb1674e66d53c -https://conda.anaconda.org/bioconda/noarch/bioconductor-annotationdbi-1.68.0-r44hdfd78af_0.tar.bz2#cdec31da7826d82f22cdebc22c8755d2 -https://conda.anaconda.org/conda-forge/linux-64/r-ellipsis-0.3.2-r44hb1dbf0f_3.conda#a96cb6b4b61efd45ffe47019af5eb879 -https://conda.anaconda.org/conda-forge/noarch/r-generics-0.1.4-r44hc72bb7e_0.conda#c02ed249dc33336dfa20ca3eeaf3d1ee -https://conda.anaconda.org/conda-forge/linux-64/r-magrittr-2.0.3-r44hb1dbf0f_3.conda#a53562b6400cbbf0323fb97881ba61b3 -https://conda.anaconda.org/conda-forge/linux-64/r-fansi-1.0.6-r44hb1dbf0f_1.conda#c4c0d4b82b54899c61c6f3e09b1bcc5c -https://conda.anaconda.org/conda-forge/linux-64/r-utf8-1.2.6-r44h2b5f3a1_0.conda#e463d3779b87ad5615816f1c3cde1135 -https://conda.anaconda.org/conda-forge/noarch/r-pillar-1.11.0-r44hc72bb7e_0.conda#6fc3eef9e2b83886004b85758b33d61c -https://conda.anaconda.org/conda-forge/linux-64/r-tibble-3.3.0-r44h2b5f3a1_0.conda#e61406c01509e1a942d9b01de165b454 -https://conda.anaconda.org/conda-forge/noarch/r-withr-3.0.2-r44hc72bb7e_0.conda#7c7e6e8f6fc8d0fd3baf24e8a5ed8ff5 -https://conda.anaconda.org/conda-forge/noarch/r-tidyselect-1.2.1-r44hc72bb7e_1.conda#aa5f953d9b0709ee347bb4e4dd5acfea -https://conda.anaconda.org/conda-forge/linux-64/r-dplyr-1.1.4-r44h0d4f4ea_1.conda#1c060647efac55dce8530efc27b39825 -https://conda.anaconda.org/conda-forge/linux-64/r-purrr-1.1.0-r44h54b55ab_0.conda#c092fa236b23d0b345dbae05cb5cee5f -https://conda.anaconda.org/conda-forge/linux-64/r-stringi-1.8.7-r44h3c328a7_0.conda#f040df1163b8069796dafbf17ccf5990 -https://conda.anaconda.org/conda-forge/noarch/r-stringr-1.5.1-r44h785f33e_1.conda#f1fdeed70529cb6e74277d758b5304fd -https://conda.anaconda.org/conda-forge/linux-64/r-tidyr-1.3.1-r44h0d4f4ea_1.conda#28992fad4ad3081d55f9d7bcd1c7a1ef -https://conda.anaconda.org/conda-forge/noarch/r-dbplyr-2.5.0-r44hc72bb7e_1.conda#14bca827a234b44dd594c345cb9380aa -https://conda.anaconda.org/conda-forge/linux-64/r-filelock-1.0.3-r44hb1dbf0f_1.conda#a1db32be1b2d013336f4c408ae2fd94a -https://conda.anaconda.org/bioconda/noarch/bioconductor-biocfilecache-2.14.0-r44hdfd78af_0.tar.bz2#3f79b6f2157a77fce7eca6cd95082212 -https://conda.anaconda.org/conda-forge/linux-64/r-digest-0.6.37-r44h0d4f4ea_0.conda#a02d79cfe9ed0e17ca2984fad70121ff -https://conda.anaconda.org/conda-forge/linux-64/r-rappdirs-0.3.3-r44hb1dbf0f_3.conda#91e4bf38b98bbb03e6babe4537b840b7 -https://conda.anaconda.org/conda-forge/noarch/r-httr2-1.2.1-r44hc72bb7e_0.conda#44f7c9b3563348fe4f7c2ca7a15daf2f -https://conda.anaconda.org/conda-forge/noarch/r-hms-1.1.3-r44hc72bb7e_2.conda#e3f892da67e8364c23c449565eb7970b -https://conda.anaconda.org/conda-forge/noarch/r-assertthat-0.2.1-r44hc72bb7e_5.conda#9e9eee147ae329aaf3ec1cd3a1a7027c -https://conda.anaconda.org/conda-forge/noarch/r-prettyunits-1.2.0-r44hc72bb7e_1.conda#9e8e45220a8e13eb137c2ff752b7b941 -https://conda.anaconda.org/conda-forge/noarch/r-progress-1.2.3-r44hc72bb7e_1.conda#a14cff20682eb2f1a94ed9fb0646d73c -https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.8-h04c0eec_1.conda#10bcbd05e1c1c9d652fccb42b776a9fa -https://conda.anaconda.org/conda-forge/linux-64/r-xml2-1.4.0-r44hc6fd541_0.conda#a1a0f84c9ffbe227236069ff24a562c0 -https://conda.anaconda.org/bioconda/noarch/bioconductor-biomart-2.62.0-r44hdfd78af_0.tar.bz2#39cfd13060df28086f6c8e4e35ba35d8 -https://conda.anaconda.org/conda-forge/linux-64/r-matrixstats-1.5.0-r44h2b5f3a1_0.conda#ef9118c2585cea360e5e780b374a2e29 -https://conda.anaconda.org/bioconda/noarch/bioconductor-matrixgenerics-1.18.0-r44hdfd78af_0.tar.bz2#d1b86fcb6d7e4d3c9fe67817c739b5a7 -https://conda.anaconda.org/conda-forge/noarch/r-abind-1.4_5-r44hc72bb7e_1006.conda#bb3b3bb6a65a4c572ed072ca52c98a2b -https://conda.anaconda.org/conda-forge/linux-64/r-lattice-0.22_7-r44h2b5f3a1_0.conda#3d5d499e979c3e2e8314e5af04653ece -https://conda.anaconda.org/conda-forge/linux-64/r-matrix-1.7_4-r44h0e4624f_0.conda#b47f779332393213ebb99909739a65b8 -https://conda.anaconda.org/bioconda/linux-64/bioconductor-s4arrays-1.6.0-r44h3df3fcb_1.tar.bz2#a6774527b21da1eb7a99b3839301ab57 -https://conda.anaconda.org/bioconda/linux-64/bioconductor-sparsearray-1.6.0-r44h3df3fcb_1.tar.bz2#143242d9cf4199b8f26c2552b61d2049 -https://conda.anaconda.org/bioconda/linux-64/bioconductor-delayedarray-0.32.0-r44h3df3fcb_1.tar.bz2#2a69a0cd9896594a301067804e65b8d0 -https://conda.anaconda.org/bioconda/linux-64/bioconductor-genomicranges-1.58.0-r44h3df3fcb_2.tar.bz2#2200bf0109f17b793a1e0140f74e0d1a -https://conda.anaconda.org/conda-forge/linux-64/r-statmod-1.5.0-r44ha36c22a_2.conda#c430837afa5aa965f3dc841499ad4813 -https://conda.anaconda.org/bioconda/linux-64/bioconductor-limma-3.62.1-r44h15a9599_1.tar.bz2#f98d28949c8fbfcfa8e7073512d4bbee -https://conda.anaconda.org/bioconda/noarch/bioconductor-summarizedexperiment-1.36.0-r44hdfd78af_0.tar.bz2#144e795fdb25d213899e249bcb538bc4 -https://conda.anaconda.org/conda-forge/linux-64/r-data.table-1.17.8-r44h1c8cec4_0.conda#3e38239b6017a49fd35d789a74e13607 -https://conda.anaconda.org/conda-forge/noarch/r-r.methodss3-1.8.2-r44hc72bb7e_3.conda#77a8638e6efd89803446295bf52b81b8 -https://conda.anaconda.org/conda-forge/noarch/r-r.oo-1.27.1-r44hc72bb7e_0.conda#4b4237f0386dbb8761882cf54c43864f -https://conda.anaconda.org/conda-forge/noarch/r-r.utils-2.13.0-r44hc72bb7e_0.conda#0b97b7fb7400db6e236ed80f4e454154 -https://conda.anaconda.org/conda-forge/noarch/r-clipr-0.8.0-r44hc72bb7e_3.conda#a1361a4e31db3a567f1f4792281f66a6 -https://conda.anaconda.org/conda-forge/linux-64/r-tzdb-0.5.0-r44h3697838_1.conda#d9f598a55e3e347d34e035bcebca0cf6 -https://conda.anaconda.org/conda-forge/linux-64/r-vroom-1.6.5-r44h0d4f4ea_1.conda#38ab4b98e4e6d5fd67fc686e8b616fc6 -https://conda.anaconda.org/conda-forge/linux-64/r-readr-2.1.5-r44h0d4f4ea_1.conda#cf7a9a09e3825c416769dca906692a9a -https://conda.anaconda.org/conda-forge/linux-64/r-xml-3.99_0.17-r44h5bae778_2.conda#bdbd8d1e692e2b2d05a261dc7cad3a6f -https://conda.anaconda.org/conda-forge/noarch/r-rentrez-1.2.4-r44h785f33e_0.conda#95c3f6a7d23f959c8f3a8a0cb1b5d273 -https://conda.anaconda.org/conda-forge/noarch/r-selectr-0.4_2-r44hc72bb7e_4.conda#b3b0ff7cd9ae293a27126643d052cd55 -https://conda.anaconda.org/conda-forge/noarch/r-rvest-1.0.5-r44hc72bb7e_0.conda#e652da30224aa33c5806c217f584912b -https://conda.anaconda.org/bioconda/noarch/bioconductor-geoquery-2.74.0-r44hdfd78af_0.tar.bz2#edef0f2edf5e269df73ccc92ea9a5d17 -https://conda.anaconda.org/conda-forge/noarch/pip-25.2-pyh145f28c_0.conda#e7ab34d5a93e0819b62563c78635d937 -https://conda.anaconda.org/conda-forge/noarch/r-getopt-1.20.4-r44ha770c72_1.conda#dc45a69329ba168c23002d2ebd774e0c -https://conda.anaconda.org/conda-forge/noarch/r-optparse-1.7.5-r44hc72bb7e_1.conda#dcc48b3a7acea00d133eb775e0d1c7e8 diff --git a/modules/local/get_candidate_genes/environment.yml b/modules/local/get_candidate_genes/environment.yml new file mode 100644 index 00000000..4fa78808 --- /dev/null +++ b/modules/local/get_candidate_genes/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::polars==1.35.2 diff --git a/modules/local/get_candidate_genes/main.nf b/modules/local/get_candidate_genes/main.nf index 9f97184d..ffd6d72c 100644 --- a/modules/local/get_candidate_genes/main.nf +++ b/modules/local/get_candidate_genes/main.nf @@ -2,7 +2,7 @@ process GET_CANDIDATE_GENES { label 'process_high_memory' - conda "${moduleDir}/spec-file.txt" + 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/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" diff --git a/modules/local/get_candidate_genes/spec-file.txt b/modules/local/get_candidate_genes/spec-file.txt deleted file mode 100644 index 0ffb80b1..00000000 --- a/modules/local/get_candidate_genes/spec-file.txt +++ /dev/null @@ -1,32 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_5.conda#dcd5ff1940cd38f6df777cac86819d60 -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_5.conda#264fbfba7fb20acf3b29cde153e345ce -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda#0be7c6e070c19105f966d3758448d018 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda#4211416ecba1866fab0c6470986c22d6 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda#0b367fad34931cb79e0d6b7e5c06bb1c -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.1-he9a06e4_0.conda#af930c65e9a79a3423d6d36e265cef65 -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda#74784ee3d225fc3dca89edb635b4e5cc -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda#ffffb341206dd0dab0c36053c048d621 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.13.7-h2b335a9_100_cp313.conda#724dcf9960e933838247971da07fe5cf -https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.7-py313hd8ed1ab_100.conda#c5623ddbd37c5dafa7754a83f97de01e -https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.7-h4df99d1_100.conda#47a123ca8e727d886a2c6d0c71658f8c -https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda#aaa2a381ccc56eac91d63b6c1240312f -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_5.conda#4e02a49aaa9d5190cb630fa43528fbe6 -https://conda.anaconda.org/conda-forge/noarch/pip-25.2-pyh145f28c_0.conda#e7ab34d5a93e0819b62563c78635d937 -https://conda.anaconda.org/conda-forge/linux-64/polars-default-1.33.1-py39hf521cc8_0.conda#900f486d119d5c83d14c812068a3ecad -https://conda.anaconda.org/conda-forge/linux-64/polars-1.33.1-default_h755bcc6_0.conda#1884a1a6acc457c8e4b59b0f6450e140 diff --git a/modules/local/gprofiler/idmapping/environment.yml b/modules/local/gprofiler/idmapping/environment.yml new file mode 100644 index 00000000..e8068c38 --- /dev/null +++ b/modules/local/gprofiler/idmapping/environment.yml @@ -0,0 +1,9 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::pandas==2.3.3 + - conda-forge::requests==2.32.5 + - conda-forge::tenacity==9.1.2 diff --git a/modules/local/gprofiler/idmapping/main.nf b/modules/local/gprofiler/idmapping/main.nf index ee5a8029..2e411fec 100644 --- a/modules/local/gprofiler/idmapping/main.nf +++ b/modules/local/gprofiler/idmapping/main.nf @@ -19,7 +19,7 @@ process GPROFILER_IDMAPPING { } } - conda "${moduleDir}/spec-file.txt" + 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/5c/5c28c8e613c062828aaee4b950029bc90a1a1aa94d5f61016a588c8ec7be8b65/data': 'community.wave.seqera.io/library/pandas_requests_tenacity:5ba56df089a9d718' }" diff --git a/modules/local/gprofiler/idmapping/spec-file.txt b/modules/local/gprofiler/idmapping/spec-file.txt deleted file mode 100644 index 3233c10b..00000000 --- a/modules/local/gprofiler/idmapping/spec-file.txt +++ /dev/null @@ -1,57 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda#3cd1a7238a0dd3d0860fdefc496cc854 -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda#9e60c55e725c20d23125a5f0dd69af5d -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda#6d11a5edae89fe413c0569f16d308f5a -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda#e66f2b8ad787e7beb0f846e4bd7e8493 -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62ee74e96c5ebb0af99386de58cf9553 -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_0.conda#e31316a586cac398b1fcdb10ace786b9 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda#db0bfbe7dd197b68ad5f30333bae6ce0 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda#d864d34357c3b65a4b731f78c0801dc4 -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.2-h6cd9bfd_0.conda#b04c7eda6d7dab1e6503135e7fad4d25 -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b -https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.6.15-hbd8a1cb_0.conda#72525f07d72806e3b639ad4504c30ce5 -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda#c87df2ab1448ba69169652ab9547082d -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.12.8-h9e4cc4f_1_cpython.conda#7fd2fd79436d9b473812f14e86746844 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda#0dfcdc155cf23812a0c9deada86fb723 -https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h2ec8cdc_3.conda#a32e0c069f6c3dcac635f7b0b0dac67e -https://conda.anaconda.org/conda-forge/noarch/certifi-2025.6.15-pyhd8ed1ab_0.conda#781d068df0cc2407d4db0ecfbb29225b -https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda#12c566707c80111f9799308d9e265aef -https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py312h06ac9bb_0.conda#a861504bbea4161a9170b85d4d2be840 -https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.2-pyhd8ed1ab_0.conda#40fe4284b8b5835a9073a645139f35af -https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda#0a802cb9888dd14eeefc611f05c40b6e -https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda#8e6923fc12f1fe8f8c4e5c9f343256ac -https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda#b4754fb1bdcb70c8fd54f918301582c6 -https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda#39a4f67be3286c86d696df570b1201b7 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda#530566b68c3b8ce7eec4cd047eae19fe -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda#bfbca721fd33188ef923dfe9ba172f29 -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda#323dc8f259224d13078aaf7ce96c3efe -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-32_h59b9bed_openblas.conda#2af9f3d5c2e39f417ce040f5a35c40c6 -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-32_he106b2a_openblas.conda#3d3f9355e52f269cd8bc2c440d8a5263 -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-32_h7ac8fdf_openblas.conda#6c3f04ccb6c578138e9f9899da0bd714 -https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.1-py312h6cf2f7f_0.conda#7e086a30150af2536a1059885368dcf0 -https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda#a451d576819089b0d672f18768be0f65 -https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 -https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 -https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 -https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.3-py312hf9745cd_3.conda#2979458c23c7755683a0598fb33e7666 -https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e -https://conda.anaconda.org/conda-forge/noarch/wheel-0.45.1-pyhd8ed1ab_1.conda#75cb7132eb58d97896e173ef12ac9986 -https://conda.anaconda.org/conda-forge/noarch/pip-25.1.1-pyh8b19718_0.conda#32d0781ace05105cc99af55d36cbec7c -https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda#461219d1a5bd61342293efa2c0c90eac -https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py312h66e93f0_2.conda#630db208bc7bbb96725ce9832c7423bb -https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda#436c165519e140cb08d246a4472a9d6a -https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda#a9b9368f3701a417eac9edbcae7cb737 -https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.2-pyhd8ed1ab_0.conda#5d99943f2ae3cc69e1ada12ce9d4d701 diff --git a/modules/local/merge_counts/environment.yml b/modules/local/merge_counts/environment.yml new file mode 100644 index 00000000..c55b56d4 --- /dev/null +++ b/modules/local/merge_counts/environment.yml @@ -0,0 +1,8 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::polars==1.35.2 + - conda-forge::tqdm==4.67.1 diff --git a/modules/local/merge_counts/main.nf b/modules/local/merge_counts/main.nf index 29d5286d..fab4714b 100644 --- a/modules/local/merge_counts/main.nf +++ b/modules/local/merge_counts/main.nf @@ -8,7 +8,7 @@ process MERGE_COUNTS { return 1.MB * result * multiplicator } - conda "${moduleDir}/spec-file.txt" + 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/90/90617e987f709570820b8e7752baf9004ba85917111425d4b44b429b27b201ca/data': 'community.wave.seqera.io/library/polars_tqdm:54b124dde91d1bf3' }" diff --git a/modules/local/merge_counts/spec-file.txt b/modules/local/merge_counts/spec-file.txt deleted file mode 100644 index 26484b2c..00000000 --- a/modules/local/merge_counts/spec-file.txt +++ /dev/null @@ -1,45 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://repo.anaconda.com/pkgs/main/linux-64/_libgcc_mutex-0.1-main.conda#c3473ff8bdb3d124ed5ff11ec380d6f9 -https://repo.anaconda.com/pkgs/main/linux-64/_openmp_mutex-5.1-1_gnu.conda#71d281e9c2192cb3fa425655a8defb85 -https://repo.anaconda.com/pkgs/main/linux-64/libgcc-15.2.0-h69a1729_7.conda#01fb1b8725fc7f66312b9d409758917a -https://repo.anaconda.com/pkgs/main/linux-64/libgcc-ng-15.2.0-h166f726_7.conda#2783efb2502b9caa7f08e25fd54df899 -https://repo.anaconda.com/pkgs/main/linux-64/bzip2-1.0.8-h5eee18b_6.conda#f21a3ff51c1b271977f53ce956a69297 -https://repo.anaconda.com/pkgs/main/linux-64/libstdcxx-15.2.0-h39759b7_7.conda#7dc7ec61ceea5de17f3e2c4c5f442fc6 -https://repo.anaconda.com/pkgs/main/linux-64/libstdcxx-ng-15.2.0-hc03a8fd_7.conda#cf200522c0b13d64bf81035358d05f5b -https://repo.anaconda.com/pkgs/main/linux-64/expat-2.7.3-h3385a95_0.conda#105822d24b4de9055e705a7d76549416 -https://repo.anaconda.com/pkgs/main/linux-64/ld_impl_linux-64-2.44-h153f514_2.conda#dffdc9a0e09d04051d4bd758e104f4b3 -https://repo.anaconda.com/pkgs/main/linux-64/libffi-3.4.4-h6a678d5_1.conda#70646cc713f0c43926cfdcfe9b695fe0 -https://repo.anaconda.com/pkgs/main/linux-64/libmpdec-4.0.0-h5eee18b_0.conda#feb10f42b1a7b523acbf85461be41a3e -https://repo.anaconda.com/pkgs/main/linux-64/libuuid-1.41.5-h5eee18b_0.conda#4a6a2354414c9080327274aa514e5299 -https://repo.anaconda.com/pkgs/main/linux-64/ncurses-6.5-h7934f7d_0.conda#0abfc090299da4bb031b84c64309757b -https://repo.anaconda.com/pkgs/main/linux-64/ca-certificates-2025.11.4-h06a4308_0.conda#f04cd5aa67216b77e8f664bb4c7098a4 -https://repo.anaconda.com/pkgs/main/linux-64/openssl-3.0.18-hd6dcaed_0.conda#3762b8999909b69745881cf4b8dd2816 -https://repo.anaconda.com/pkgs/main/linux-64/python_abi-3.13-1_cp313.conda#bea705c35663f9394ec82e87dc692c85 -https://repo.anaconda.com/pkgs/main/linux-64/readline-8.3-hc2a1206_0.conda#8578e006d4ef5cb98a6cda232b3490f6 -https://repo.anaconda.com/pkgs/main/linux-64/libzlib-1.3.1-hb25bd0a_0.conda#338ee51e19ee211b7fc994d4ba88c631 -https://repo.anaconda.com/pkgs/main/linux-64/zlib-1.3.1-hb25bd0a_0.conda#9f3a877e5e0fa0fb39253a59ff824861 -https://repo.anaconda.com/pkgs/main/linux-64/sqlite-3.51.0-h2a70700_0.conda#99a4278be9c6901ee6989b24fd213240 -https://repo.anaconda.com/pkgs/main/linux-64/pthread-stubs-0.3-h0ce48e5_1.conda#973a642312d2a28927aaf5b477c67250 -https://repo.anaconda.com/pkgs/main/linux-64/xorg-libxau-1.0.12-h9b100fa_0.conda#a8005a9f6eb903e113cd5363e8a11459 -https://repo.anaconda.com/pkgs/main/linux-64/xorg-libxdmcp-1.1.5-h9b100fa_0.conda#c284a09ddfba81d9c4e740110f09ea06 -https://repo.anaconda.com/pkgs/main/linux-64/libxcb-1.17.0-h9b100fa_0.conda#fdf0d380fa3809a301e2dbc0d5183883 -https://repo.anaconda.com/pkgs/main/linux-64/xorg-xorgproto-2024.1-h5eee18b_1.conda#412a0d97a7a51d23326e57226189da92 -https://repo.anaconda.com/pkgs/main/linux-64/xorg-libx11-1.8.12-h9b100fa_1.conda#6298b27afae6f49f03765b2a03df2fcb -https://repo.anaconda.com/pkgs/main/linux-64/tk-8.6.15-h54e0aa7_0.conda#1fa91e0c4fc9c9435eda3f1a25a676fd -https://repo.anaconda.com/pkgs/main/noarch/tzdata-2025b-h04d1e81_0.conda#1d027393db3427ab22a02aa44a56f143 -https://repo.anaconda.com/pkgs/main/linux-64/xz-5.6.4-h5eee18b_1.conda#3581505fa450962d631bd82b8616350e -https://repo.anaconda.com/pkgs/main/linux-64/python-3.13.9-h7e8bc2b_100_cp313.conda#9ea34b30a1bdb8f7c9d62c072697e681 -https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.9-py313hd8ed1ab_101.conda#367133808e89325690562099851529c8 -https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.9-h4df99d1_101.conda#f41e3c1125e292e6bfcea8392a3de3d8 -https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda#aaa2a381ccc56eac91d63b6c1240312f -https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda#962b9857ee8e7018c22f2776ffa0b2d7 -https://repo.anaconda.com/pkgs/main/linux-64/libgomp-15.2.0-h4751f2c_7.conda#82025ed6da944bd419d42d9b1ff116aa -https://repo.anaconda.com/pkgs/main/linux-64/setuptools-80.9.0-py313h06a4308_0.conda#42ffd8d5a0c04d5e55431e3d4f6e8408 -https://repo.anaconda.com/pkgs/main/linux-64/wheel-0.45.1-py313h06a4308_0.conda#29057e876eedce0e37c2388c138a19f9 -https://repo.anaconda.com/pkgs/main/noarch/pip-25.3-pyhc872135_0.conda#f713912a259ec613b3832c3bc842e9d4 -https://conda.anaconda.org/conda-forge/linux-64/polars-runtime-32-1.35.1-py310hffdcd12_0.conda#093d1242f534e7c383b4d67ab48c7c3d -https://conda.anaconda.org/conda-forge/noarch/polars-1.35.1-pyh6a1acc5_0.conda#dcb4da1773fc1e8c9e2321a648f34382 -https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda#9efbfdc37242619130ea42b1cc4ed861 diff --git a/modules/local/normalisation/compute_cpm/environment.yml b/modules/local/normalisation/compute_cpm/environment.yml new file mode 100644 index 00000000..104de0e2 --- /dev/null +++ b/modules/local/normalisation/compute_cpm/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::pandas==2.3.3 diff --git a/modules/local/normalisation/compute_cpm/main.nf b/modules/local/normalisation/compute_cpm/main.nf index fbc9fbe2..a10af424 100644 --- a/modules/local/normalisation/compute_cpm/main.nf +++ b/modules/local/normalisation/compute_cpm/main.nf @@ -4,7 +4,7 @@ process NORMALISATION_COMPUTE_CPM { tag "${meta.dataset}" - conda "${moduleDir}/spec-file.txt" + 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/3d/3d7126100b0eb7cb53dfb50291707ea8dda3b9738b76551ab73605d0acbe114b/data': 'community.wave.seqera.io/library/pandas:2.3.3--5a902bf824a79745' }" diff --git a/modules/local/normalisation/compute_cpm/spec-file.txt b/modules/local/normalisation/compute_cpm/spec-file.txt deleted file mode 100644 index f79332cf..00000000 --- a/modules/local/normalisation/compute_cpm/spec-file.txt +++ /dev/null @@ -1,42 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda#f7b4d76975aac7e5d9e6ad13845f92fe -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda#c0374badb3a5d4b1372db28d19462c53 -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda#f0991f0f84902f6b6009b4d2350a83aa -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda#280ea6eee9e2ddefde25ff799c4f0363 -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda#5b767048b1b3ee9a954b06f4084f93dc -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda#f627678cf829bd70bccf141a19c3ad3e -https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-bootstrap_ha15bf96_3.conda#3036ca5b895b7f5146c5a25486234a68 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-hcd61629_7.conda#f116940d825ffc9104400f0d7f1a4551 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_7.conda#8621a450add4e231f676646880703f49 -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda#be43915efc66345cccb3c310b6ed0374 -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-1_h4a7cf45_openblas.conda#8b39e1ae950f1b54a3959c58ca2c32b8 -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-1_h0358290_openblas.conda#a670bff9eb7963ea41b4e09a4e4ab608 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda#8b09ae86839581147ef2e5c5e229d164 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda#35f29eec58405aaf55e01cb470d8c26a -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-1_h47877c9_openblas.conda#dee12a83aa4aca5077ea23c0605de044 -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.0-hee844dc_0.conda#729a572a3ebb8c43933b30edcc628ceb -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda#80c07c68d2f6870250959dcc95b209d1 -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda#9ee58d5c534af06558933af3c845a780 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda#86bc20552bf46075e3d92b67f089172d -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.13.9-hc97d973_101_cp313.conda#4780fe896e961722d0623fa91d0d3378 -https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.5-py313hf6604e3_0.conda#15f43bcd12c90186e78801fafc53d89b -https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda#3339e3b65d58accf4ca4fb8748ab16b3 -https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 -https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 -https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 -https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.3-py313h08cd8bf_1.conda#9e87d4bda0c2711161d765332fa38781 -https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda#bf47878473e5ab9fdb4115735230e191 diff --git a/modules/local/normalisation/compute_tpm/environment.yml b/modules/local/normalisation/compute_tpm/environment.yml new file mode 100644 index 00000000..104de0e2 --- /dev/null +++ b/modules/local/normalisation/compute_tpm/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::pandas==2.3.3 diff --git a/modules/local/normalisation/compute_tpm/main.nf b/modules/local/normalisation/compute_tpm/main.nf index 84d1e66b..b9469d7d 100644 --- a/modules/local/normalisation/compute_tpm/main.nf +++ b/modules/local/normalisation/compute_tpm/main.nf @@ -4,7 +4,7 @@ process NORMALISATION_COMPUTE_TPM { tag "${meta.dataset}" - conda "${moduleDir}/spec-file.txt" + 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/3d/3d7126100b0eb7cb53dfb50291707ea8dda3b9738b76551ab73605d0acbe114b/data': 'community.wave.seqera.io/library/pandas:2.3.3--5a902bf824a79745' }" diff --git a/modules/local/normalisation/compute_tpm/spec-file.txt b/modules/local/normalisation/compute_tpm/spec-file.txt deleted file mode 100644 index f79332cf..00000000 --- a/modules/local/normalisation/compute_tpm/spec-file.txt +++ /dev/null @@ -1,42 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda#f7b4d76975aac7e5d9e6ad13845f92fe -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda#c0374badb3a5d4b1372db28d19462c53 -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda#f0991f0f84902f6b6009b4d2350a83aa -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda#280ea6eee9e2ddefde25ff799c4f0363 -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda#5b767048b1b3ee9a954b06f4084f93dc -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda#f627678cf829bd70bccf141a19c3ad3e -https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-bootstrap_ha15bf96_3.conda#3036ca5b895b7f5146c5a25486234a68 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-hcd61629_7.conda#f116940d825ffc9104400f0d7f1a4551 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_7.conda#8621a450add4e231f676646880703f49 -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda#be43915efc66345cccb3c310b6ed0374 -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-1_h4a7cf45_openblas.conda#8b39e1ae950f1b54a3959c58ca2c32b8 -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-1_h0358290_openblas.conda#a670bff9eb7963ea41b4e09a4e4ab608 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda#8b09ae86839581147ef2e5c5e229d164 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda#35f29eec58405aaf55e01cb470d8c26a -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-1_h47877c9_openblas.conda#dee12a83aa4aca5077ea23c0605de044 -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.0-hee844dc_0.conda#729a572a3ebb8c43933b30edcc628ceb -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda#80c07c68d2f6870250959dcc95b209d1 -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda#9ee58d5c534af06558933af3c845a780 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda#86bc20552bf46075e3d92b67f089172d -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.13.9-hc97d973_101_cp313.conda#4780fe896e961722d0623fa91d0d3378 -https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.5-py313hf6604e3_0.conda#15f43bcd12c90186e78801fafc53d89b -https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda#3339e3b65d58accf4ca4fb8748ab16b3 -https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 -https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 -https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 -https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.3-py313h08cd8bf_1.conda#9e87d4bda0c2711161d765332fa38781 -https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda#bf47878473e5ab9fdb4115735230e191 diff --git a/modules/local/normfinder/environment.yml b/modules/local/normfinder/environment.yml new file mode 100644 index 00000000..220cc1ee --- /dev/null +++ b/modules/local/normfinder/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: + - conda-forge::polars==1.35.2 + - conda-forge::tqdm==4.67.1 + - conda-forge::numpy==2.3.5 + - conda-forge::numba==0.62.1 diff --git a/modules/local/normfinder/main.nf b/modules/local/normfinder/main.nf index ff160f8d..7f12146b 100644 --- a/modules/local/normfinder/main.nf +++ b/modules/local/normfinder/main.nf @@ -2,7 +2,7 @@ process NORMFINDER { label 'process_high_memory' - conda "${moduleDir}/spec-file.txt" + 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/0e/0e0445114887dd260f1632afe116b1e81e02e1acc74a86adca55099469b490d9/data': 'community.wave.seqera.io/library/numba_numpy_polars_tqdm:6923cfab6fc04dec' }" diff --git a/modules/local/normfinder/spec-file.txt b/modules/local/normfinder/spec-file.txt deleted file mode 100644 index 07a547e8..00000000 --- a/modules/local/normfinder/spec-file.txt +++ /dev/null @@ -1,43 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_5.conda#dcd5ff1940cd38f6df777cac86819d60 -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_5.conda#264fbfba7fb20acf3b29cde153e345ce -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda#0be7c6e070c19105f966d3758448d018 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda#4211416ecba1866fab0c6470986c22d6 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda#0b367fad34931cb79e0d6b7e5c06bb1c -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.1-he9a06e4_0.conda#af930c65e9a79a3423d6d36e265cef65 -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda#74784ee3d225fc3dca89edb635b4e5cc -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda#ffffb341206dd0dab0c36053c048d621 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.13.7-h2b335a9_100_cp313.conda#724dcf9960e933838247971da07fe5cf -https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.7-py313hd8ed1ab_100.conda#c5623ddbd37c5dafa7754a83f97de01e -https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.7-h4df99d1_100.conda#47a123ca8e727d886a2c6d0c71658f8c -https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda#aaa2a381ccc56eac91d63b6c1240312f -https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda#962b9857ee8e7018c22f2776ffa0b2d7 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_5.conda#fbd4008644add05032b6764807ee2cba -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_5.conda#0c91408b3dec0b97e8a3c694845bd63b -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_2.conda#dfc5aae7b043d9f56ba99514d5e60625 -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-35_h4a7cf45_openblas.conda#6da7e852c812a84096b68158574398d0 -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-35_h0358290_openblas.conda#8aa3389d36791ecd31602a247b1f3641 -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-35_h47877c9_openblas.conda#aa0b36b71d44f74686f13b9bfabec891 -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_5.conda#4e02a49aaa9d5190cb630fa43528fbe6 -https://conda.anaconda.org/conda-forge/linux-64/llvmlite-0.44.0-py313hfdae721_2.conda#dd0d7947635c0c524608eab7db55dcc9 -https://conda.anaconda.org/conda-forge/linux-64/numpy-2.2.6-py313h17eae1a_0.conda#7a2d2f9adecd86ed5c29c2115354f615 -https://conda.anaconda.org/conda-forge/linux-64/numba-0.61.2-py313h50b8c88_1.conda#53c79b7cdee329ed4c77cafe27600cdb -https://conda.anaconda.org/conda-forge/noarch/pip-25.2-pyh145f28c_0.conda#e7ab34d5a93e0819b62563c78635d937 -https://conda.anaconda.org/conda-forge/linux-64/polars-default-1.33.1-py39hf521cc8_0.conda#900f486d119d5c83d14c812068a3ecad -https://conda.anaconda.org/conda-forge/linux-64/polars-1.33.1-default_h755bcc6_0.conda#1884a1a6acc457c8e4b59b0f6450e140 -https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda#9efbfdc37242619130ea42b1cc4ed861 diff --git a/modules/local/old/clean_count_data/main.nf b/modules/local/old/clean_count_data/main.nf index 3d988f62..357ca9eb 100644 --- a/modules/local/old/clean_count_data/main.nf +++ b/modules/local/old/clean_count_data/main.nf @@ -4,7 +4,7 @@ process CLEAN_COUNT_DATA { tag "${meta.dataset}" - conda "${moduleDir}/spec-file.txt" + 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/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" diff --git a/modules/local/old/deseq2/main.nf b/modules/local/old/deseq2/main.nf index 7a517561..18038106 100644 --- a/modules/local/old/deseq2/main.nf +++ b/modules/local/old/deseq2/main.nf @@ -4,7 +4,7 @@ process NORMALISATION_DESEQ2 { tag "${meta.dataset}" - conda "${moduleDir}/spec-file.txt" + 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/ce/cef7164b168e74e5db11dcd9acf6172d47ed6753e4814c68f39835d0c6c22f6d/data': 'community.wave.seqera.io/library/bioconductor-deseq2_r-base_r-optparse:c84cd7ffdb298fa7' }" diff --git a/modules/local/old/download_genome_annotation/main.nf b/modules/local/old/download_genome_annotation/main.nf index f28e7e33..c4914fe7 100644 --- a/modules/local/old/download_genome_annotation/main.nf +++ b/modules/local/old/download_genome_annotation/main.nf @@ -4,7 +4,7 @@ process DOWNLOAD_GENOME_ANNOTATION { tag "$accession" - conda "${moduleDir}/spec-file.txt" + 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/a6/a6b13690259900baef6865722cb3a319103acc83b5bcab67504c88bde1e3a9f6/data': 'community.wave.seqera.io/library/ncbi-datasets-cli_unzip:785aabe86637bae4' }" diff --git a/modules/local/old/edger/main.nf b/modules/local/old/edger/main.nf index 5ff95b51..ceb6e2d8 100644 --- a/modules/local/old/edger/main.nf +++ b/modules/local/old/edger/main.nf @@ -4,7 +4,7 @@ process NORMALISATION_EDGER { tag "${meta.dataset}" - conda "${moduleDir}/spec-file.txt" + 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/89/89bbc9544e18b624ed6d0a30e701cf8cec63e063cc9b5243e1efde362fe92228/data': 'community.wave.seqera.io/library/bioconductor-edger_r-base_r-optparse:400aaabddeea1574' }" diff --git a/modules/local/old/get_annotation_accession/main.nf b/modules/local/old/get_annotation_accession/main.nf index ab2733da..0236126c 100644 --- a/modules/local/old/get_annotation_accession/main.nf +++ b/modules/local/old/get_annotation_accession/main.nf @@ -4,7 +4,7 @@ process GET_ANNOTATION_ACCESSION { tag "$species" - conda "${moduleDir}/spec-file.txt" + 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/b4/b4d686ef63e22bc4d461178fc241cefddd2aa3436e189d3787c8e019448f056e/data': 'community.wave.seqera.io/library/requests_tenacity_tqdm:126dbed8ef3ff96f' }" diff --git a/modules/local/quantile_normalisation/environment.yml b/modules/local/quantile_normalisation/environment.yml new file mode 100644 index 00000000..6c7a8ca7 --- /dev/null +++ b/modules/local/quantile_normalisation/environment.yml @@ -0,0 +1,9 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::pandas==2.3.3 + - conda-forge::scikit-learn==1.7.2 + - conda-forge::pyarrow==22.0.0 diff --git a/modules/local/quantile_normalisation/main.nf b/modules/local/quantile_normalisation/main.nf index 6a062f3f..adb9134a 100644 --- a/modules/local/quantile_normalisation/main.nf +++ b/modules/local/quantile_normalisation/main.nf @@ -4,7 +4,7 @@ process QUANTILE_NORMALISATION { tag "${meta.dataset}" - conda "${moduleDir}/spec-file.txt" + 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/2d/2df931a4ea181fe1ea9527abe0fd4aff9453d6ea56d56aee7c4ac5dceed611e3/data': 'community.wave.seqera.io/library/pandas_pyarrow_python_scikit-learn:6f85e3c4d1706e81' }" diff --git a/modules/local/quantile_normalisation/spec-file.txt b/modules/local/quantile_normalisation/spec-file.txt deleted file mode 100644 index 702e03ac..00000000 --- a/modules/local/quantile_normalisation/spec-file.txt +++ /dev/null @@ -1,110 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda#3cd1a7238a0dd3d0860fdefc496cc854 -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda#9e60c55e725c20d23125a5f0dd69af5d -https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.10.6-hb9d3cd8_0.conda#d7d4680337a14001b0e043e96529409b -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.6.15-hbd8a1cb_0.conda#72525f07d72806e3b639ad4504c30ce5 -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda#c87df2ab1448ba69169652ab9547082d -https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.8.1-h1a47875_3.conda#55a8561fdbbbd34f50f57d9be12ed084 -https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.3.0-h4e1184b_5.conda#3f4c1197462a6df2be6dc8241828fe93 -https://conda.anaconda.org/conda-forge/linux-64/s2n-1.5.11-h072c03f_0.conda#5e8060d52f676a40edef0006a75c718f -https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.15.3-h173a860_6.conda#9a063178f1af0a898526cc24ba7be486 -https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.9.2-hefd7a92_4.conda#5ce4df662d32d3123ea8da15571b6f51 -https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.2.2-h4e1184b_0.conda#dcd498d493818b776a77fbc242fbf8e4 -https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.8.1-h205f482_0.conda#9c500858e88df50af3cc883d194de78a -https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.2.2-h4e1184b_4.conda#74e8c3e4df4ceae34aa2959df4b28101 -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda#6d11a5edae89fe413c0569f16d308f5a -https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.5.0-h7959bf6_11.conda#9b3fb60fe57925a92f399bc3fc42eccf -https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.11.0-h11f4f37_12.conda#96c3e0221fa2da97619ee82faa341a73 -https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.7.9-he1b24dc_1.conda#caafc32928a5f7f3f7ef67d287689144 -https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.29.9-he0e7f3f_2.conda#8a4e6fc8a3b285536202b5456a74a940 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda#e66f2b8ad787e7beb0f846e4bd7e8493 -https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.1-h166bdaf_0.tar.bz2#30186d27e2c9fa62b45fb1476b7200e3 -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda#c277e0a4d549b03ac1e9d6cbbe3d017b -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_3.conda#57541755b5a51691955012b8e197c06c -https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda#3f43953b7d3fb3aaa1d0d0723d91e368 -https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.5-hb9d3cd8_0.conda#f7f0d6cc2dc986d42ac2689ec88192be -https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda#172bf1cd1ff8629f2b1179945ed45055 -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.64.0-h161d5f1_0.conda#19e57602824042dfd0446292ef90488b -https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda#eecce068c7e4eddeb169591baac20ac4 -https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda#6432cb5d4ac0046c3ac0a8a0f95842f9 -https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.14.1-h332b0f4_0.conda#45f6713cb00f124af300342512219182 -https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.489-h4d475cb_0.conda#b775e9f46dfa94b228a81d8e8c6d8b1d -https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.14.0-h5cfcd09_0.conda#0a8838771cc2e985cd295e01ae83baf1 -https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.10.0-h113e628_0.conda#73f73f60854f325a55f1d31459f2ab73 -https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 -https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h4ce23a2_1.conda#e796ff8ddc598affdf7c173d6145f087 -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.8-h4bc477f_0.conda#14dbe05b929e329dbaa6f2d0aa19466d -https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.8.0-h736e048_1.conda#13de36be8de3ae3f05ba127631599213 -https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.13.0-h3cf044e_1.conda#7eb66060455c7a47d9dcdbfa9f46579b -https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.12.0-ha633028_1.conda#7c1980f89dd41b097549782121a73490 -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62ee74e96c5ebb0af99386de58cf9553 -https://conda.anaconda.org/conda-forge/linux-64/gflags-2.2.2-h5888daf_1005.conda#d411fc29e338efb48c5fd4576d71d881 -https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hbabe93e_0.conda#ff862eebdfeb2fd048ae9dc92510baca -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_0.conda#e31316a586cac398b1fcdb10ace786b9 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda#db0bfbe7dd197b68ad5f30333bae6ce0 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda#ede4673863426c0883c0063d853bbd85 -https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda#d864d34357c3b65a4b731f78c0801dc4 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.2-h6cd9bfd_0.conda#b04c7eda6d7dab1e6503135e7fad4d25 -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b -https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda#a0116df4f4ed05c303811a837d5b39d8 -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.12.8-h9e4cc4f_1_cpython.conda#7fd2fd79436d9b473812f14e86746844 -https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda#4de79c071274a53dcaf2a8c749d1499e -https://conda.anaconda.org/conda-forge/noarch/joblib-1.5.1-pyhd8ed1ab_0.conda#fb1c14694de51a476ce8636d92b6f42c -https://conda.anaconda.org/conda-forge/linux-64/libabseil-20240722.0-cxx17_hbbce691_4.conda#488f260ccda0afaf08acb286db439c2f -https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.1.0-hb9d3cd8_3.conda#cb98af5db26e3f482bebb80ce9d947d3 -https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.1.0-hb9d3cd8_3.conda#1c6eecffad553bde44c5238770cfb7da -https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.1.0-hb9d3cd8_3.conda#3facafe58f3858eb95527c7d3a3fc578 -https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-5.28.3-h6128344_1.conda#d8703f1ffe5a06356f06467f1d0b9464 -https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2024.07.02-hbbce691_2.conda#b2fede24428726dd867611664fb372e8 -https://conda.anaconda.org/conda-forge/linux-64/re2-2024.07.02-h9925aae_2.conda#e84ddf12bde691e8ec894b00ea829ddf -https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.67.1-h25350d4_2.conda#bfcedaf5f9b003029cc6abe9431f66bf -https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.35.0-h2b5623c_0.conda#1040ab07d7af9f23cf2466ffe4e58db1 -https://conda.anaconda.org/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2#c965a5aa0d5c1c37ffc62dff36e28400 -https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-2.35.0-h0121fbd_0.conda#34e2243e0428aac6b3e903ef99b6d57d -https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-headers-1.18.0-ha770c72_1.conda#4fb055f57404920a43b147031471e03b -https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h3f2d84a_0.conda#d76872d096d063e226482c99337209dc -https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.1-hb9d3cd8_2.conda#c9f075ab2f33b3bbee9e62d4ad0a6cd8 -https://conda.anaconda.org/conda-forge/linux-64/prometheus-cpp-1.3.0-ha5d0236_0.conda#a83f6a2fdc079e643237887a37460668 -https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-1.18.0-hfcad708_1.conda#1f5a5d66e77a39dc5bd639ec953705cf -https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.10.0-h202a827_0.conda#0f98f3e95272d118f7931b6bef69bfe5 -https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda#9de5350a85c4a20c685259b889aa6393 -https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.1-h8bd8927_1.conda#3b3e64af585eadfb52bb90b553db5edf -https://conda.anaconda.org/conda-forge/linux-64/orc-2.0.3-h12ee42a_2.conda#4f6f9f3f80354ad185e276c120eac3f0 -https://conda.anaconda.org/conda-forge/linux-64/libarrow-19.0.0-hfa2a6e7_9_cpu.conda#9e09f9cd5c0eb584be78ebea4e0db151 -https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-19.0.0-hcb10f89_9_cpu.conda#cd6e5cd25096e02ddc591f0bc7d0354b -https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda#a1cfcc585f0c42bf8d5546bb1dfb668d -https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.21.0-h0e7cc3e_0.conda#dcb95c0a98ba9ff737f7ae482aef7833 -https://conda.anaconda.org/conda-forge/linux-64/libparquet-19.0.0-h081d1f1_9_cpu.conda#de0b82dc1e9f6f9fb66306f0a15e16fa -https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-19.0.0-hcb10f89_9_cpu.conda#da890e33d20acb4713e73ed841d5088b -https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-19.0.0-h08228c5_9_cpu.conda#9d6c1688d87aeb9fc4513bde60d2f2f3 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda#530566b68c3b8ce7eec4cd047eae19fe -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda#bfbca721fd33188ef923dfe9ba172f29 -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda#323dc8f259224d13078aaf7ce96c3efe -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-32_h59b9bed_openblas.conda#2af9f3d5c2e39f417ce040f5a35c40c6 -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-32_he106b2a_openblas.conda#3d3f9355e52f269cd8bc2c440d8a5263 -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-32_h7ac8fdf_openblas.conda#6c3f04ccb6c578138e9f9899da0bd714 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda#0dfcdc155cf23812a0c9deada86fb723 -https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.1-py312h6cf2f7f_0.conda#7e086a30150af2536a1059885368dcf0 -https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda#a451d576819089b0d672f18768be0f65 -https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 -https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 -https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 -https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.3-py312hf9745cd_3.conda#2979458c23c7755683a0598fb33e7666 -https://conda.anaconda.org/conda-forge/noarch/wheel-0.45.1-pyhd8ed1ab_1.conda#75cb7132eb58d97896e173ef12ac9986 -https://conda.anaconda.org/conda-forge/noarch/pip-25.1.1-pyh8b19718_0.conda#32d0781ace05105cc99af55d36cbec7c -https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-19.0.0-py312h01725c0_0_cpu.conda#7ab1143b9ac1af5cc4a630706f643627 -https://conda.anaconda.org/conda-forge/linux-64/pyarrow-19.0.0-py312h7900ff3_0.conda#14f86e63b5c214dd9fb34e5472d4bafc -https://conda.anaconda.org/conda-forge/linux-64/scipy-1.16.0-py312hf734454_0.conda#7513ac56209d27a85ffa1582033f10a8 -https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda#9d64911b31d57ca443e9f1e36b04385f -https://conda.anaconda.org/conda-forge/linux-64/scikit-learn-1.6.1-py312h7a48858_0.conda#102727f71df02a51e9e173f2e6f87d57 diff --git a/modules/local/rename_gene_ids/environment.yml b/modules/local/rename_gene_ids/environment.yml new file mode 100644 index 00000000..d44b366f --- /dev/null +++ b/modules/local/rename_gene_ids/environment.yml @@ -0,0 +1,8 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::pandas==2.3.3 + - conda-forge::polars==1.35.2 diff --git a/modules/local/rename_gene_ids/main.nf b/modules/local/rename_gene_ids/main.nf index 5886029b..7813adb9 100644 --- a/modules/local/rename_gene_ids/main.nf +++ b/modules/local/rename_gene_ids/main.nf @@ -4,7 +4,7 @@ process RENAME_GENE_IDS { tag "${meta.dataset}" - conda "${moduleDir}/spec-file.txt" + 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/c9/c9b43e446f2c3b794644fd4c1c86ab09ba0afafc0c02e3fcdf45509ffc89fc4d/data': 'community.wave.seqera.io/library/pandas_polars:29ea1468b5490a67' }" @@ -20,6 +20,7 @@ process RENAME_GENE_IDS { tuple val(meta.dataset), env("NB_MAPPED"), env("NB_UNMAPPED"), topic: id_mapping_stats tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions + tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions script: def mapping_arg = gene_id_mapping_file ? "--mappings $gene_id_mapping_file" : "" diff --git a/modules/local/rename_gene_ids/spec-file.txt b/modules/local/rename_gene_ids/spec-file.txt deleted file mode 100644 index d850c144..00000000 --- a/modules/local/rename_gene_ids/spec-file.txt +++ /dev/null @@ -1,48 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda#f7b4d76975aac7e5d9e6ad13845f92fe -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda#c0374badb3a5d4b1372db28d19462c53 -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda#51a19bba1b8ebfb60df25cde030b7ebc -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda#5b767048b1b3ee9a954b06f4084f93dc -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda#edb0dca6bc32e4f4789199455a1dbeb8 -https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda#6432cb5d4ac0046c3ac0a8a0f95842f9 -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_104.conda#a6abd2796fc332536735f68ba23f7901 -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda#8b09ae86839581147ef2e5c5e229d164 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda#35f29eec58405aaf55e01cb470d8c26a -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda#1a580f7796c7bf6393fddb8bbbde58dc -https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda#c7e925f37e3b40d893459e625f6a53f1 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda#280ea6eee9e2ddefde25ff799c4f0363 -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda#f627678cf829bd70bccf141a19c3ad3e -https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda#8b189310083baabfb622af68fd9d3ae3 -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.0-hee844dc_0.conda#729a572a3ebb8c43933b30edcc628ceb -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda#80c07c68d2f6870250959dcc95b209d1 -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda#47e340acb35de30501a76c7c799c41d7 -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda#f0991f0f84902f6b6009b4d2350a83aa -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda#9ee58d5c534af06558933af3c845a780 -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda#94305520c52a4aa3f6c2b1ff6008d9f8 -https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda#86bc20552bf46075e3d92b67f089172d -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a -https://conda.anaconda.org/conda-forge/linux-64/python-3.13.9-hc97d973_101_cp313.conda#4780fe896e961722d0623fa91d0d3378 -https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.9-py313hd8ed1ab_101.conda#367133808e89325690562099851529c8 -https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.9-h4df99d1_101.conda#f41e3c1125e292e6bfcea8392a3de3d8 -https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda#aaa2a381ccc56eac91d63b6c1240312f -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-hcd61629_7.conda#f116940d825ffc9104400f0d7f1a4551 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_7.conda#8621a450add4e231f676646880703f49 -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda#be43915efc66345cccb3c310b6ed0374 -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-2_h4a7cf45_openblas.conda#6146bf1b7f58113d54614c6ec683c14a -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-2_h0358290_openblas.conda#a84b2b7ed34206d14739fb8d29cd2799 -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-2_h47877c9_openblas.conda#9fb20e74a7436dc94dd39d9a9decddc3 -https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.5-py313hf6604e3_0.conda#15f43bcd12c90186e78801fafc53d89b -https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda#3339e3b65d58accf4ca4fb8748ab16b3 -https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 -https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda#88476ae6ebd24f39261e0854ac244f33 -https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda#bc8e3267d44011051f2eb14d22fb0960 -https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.3-py313h08cd8bf_1.conda#9e87d4bda0c2711161d765332fa38781 -https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda#bf47878473e5ab9fdb4115735230e191 -https://conda.anaconda.org/conda-forge/linux-64/polars-runtime-32-1.35.2-py310hffdcd12_0.conda#2b90c3aaf73a5b6028b068cf3c76e0b7 -https://conda.anaconda.org/conda-forge/noarch/polars-1.35.2-pyh6a1acc5_0.conda#24e8f78d79881b3c035f89f4b83c565c diff --git a/nextflow.config b/nextflow.config index efd6f000..966df39a 100644 --- a/nextflow.config +++ b/nextflow.config @@ -120,6 +120,7 @@ profiles { micromamba { conda.enabled = true conda.useMicromamba = true + conda.channels = ['conda-forge', 'bioconda'] docker.enabled = false singularity.enabled = false podman.enabled = false From 660f52a8bbc3be3ed6e6d26d7f065dc79e5bb4f7 Mon Sep 17 00:00:00 2001 From: Olivier Date: Thu, 27 Nov 2025 15:34:15 +0100 Subject: [PATCH 212/258] add script to normalise CEL files from microarray data --- bin/normalise_microarray.R | 128 ++++++++++++++++++ .../normalise_microarray/environment.yml | 11 ++ 2 files changed, 139 insertions(+) create mode 100755 bin/normalise_microarray.R create mode 100644 modules/local/normalise_microarray/environment.yml diff --git a/bin/normalise_microarray.R b/bin/normalise_microarray.R new file mode 100755 index 00000000..5245909e --- /dev/null +++ b/bin/normalise_microarray.R @@ -0,0 +1,128 @@ +#!/usr/bin/env Rscript + +# Written by Olivier Coen. Released under the MIT license. + +suppressPackageStartupMessages(library("affy")) +suppressPackageStartupMessages(library("optparse")) +suppressPackageStartupMessages(library("AnnotationDbi")) +suppressPackageStartupMessages(library("dplyr")) + +# Load library +library(affy) +library(optparse) +library(AnnotationDbi) +library(dplyr) +library(tibble) + +options(error = traceback) + +# we need to install the affy package manually while disabling threading +# when installed through conda, we get: ERROR; return code from pthread_create() is 22 +if (!requireNamespace("affy", quietly = TRUE)) { + BiocManager::install("affy", configure.args="--disable-threading", force = TRUE, quiet = TRUE) +} + + +##################################################### +##################################################### +# ARG PARSER +##################################################### +##################################################### + +get_args <- function() { + option_list <- list( + make_option("--input", help = "Folder containing CEL files"), + make_option("--target-gene-id-db", dest = "target_gene_id_db", help = "Target database for gene IDs (ENSEMBL or ENTREZID)") + ) + + args <- parse_args(OptionParser( + option_list = option_list, + description = "Normalize microarray data using RMA" + )) + return(args) +} + +get_probe_id_mapping <- function(data, annot_db, target_gene_id_db, stringent) { + + probe_ids <- rownames(data) + annotations <- AnnotationDbi::select( + annot_db, + keys = probe_ids, + columns = c(target_gene_id_db), + keytype = "PROBEID" + ) + + if (stringent) { + annotations <- annotations %>% + group_by(PROBEID) %>% + filter(n_distinct(.data[[target_gene_id_db]], na.rm = TRUE) == 1) %>% + ungroup() + } + + return(annotations) +} + +replace_probe_ids_by_target_ids <- function(data, annotations, target_gene_id_db) { + data <- as.data.frame(data) + data$PROBEID <- rownames(data) + + data <- merge(annotations, data, by = "PROBEID", all.x = TRUE) + + # computing mean of probe values for each gene + data <- data %>% + group_by(.data[[target_gene_id_db]]) %>% + summarise(across(where(is.numeric), function(x) mean(x, na.rm = TRUE))) %>% + ungroup() + + data <- tibble::column_to_rownames(data, var = target_gene_id_db) + return(data) +} + + +##################################################### +##################################################### +# MAIN +##################################################### +##################################################### + + +main <- function() { + + args <- get_args() + + # Read CEL files from a directory + message("Reading CEL files from", args$input) + data <- ReadAffy(celfile.path = args$input) + + message("Installing annotation database") + db_name <- paste0(annotation(data), ".db") + if (!requireNamespace(db_name, quietly = TRUE)) { + BiocManager::install(db_name, quiet = TRUE) + } + library(db_name, character.only = TRUE) + + # Normalize using RMA (most common method) + eset <- rma(data) + # Extract normalized expression values + message("Extracting normalized expression values") + normalised_data <- exprs(eset) + + annotations <- get_probe_id_mapping( + normalised_data, + annot_db = get(db_name), # Get the database object using get() + target_gene_id_db = args$target_gene_id_db, + stringent = TRUE + ) + + normalised_data_df <- replace_probe_ids_by_target_ids(normalised_data, annotations, args$target_gene_id_db) + + # cleaning colnames + colnames(normalised_data_df) <- sub("\\..*", "", colnames(normalised_data_df)) + + # Save results + message("Saving results to normalised_expression.csv") + write.csv(normalised_data, "normalised_expression.csv") + +} + +main() diff --git a/modules/local/normalise_microarray/environment.yml b/modules/local/normalise_microarray/environment.yml new file mode 100644 index 00000000..651528a4 --- /dev/null +++ b/modules/local/normalise_microarray/environment.yml @@ -0,0 +1,11 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::r-biocmanager + - conda-forge::r-optparse + - conda-forge::r-dplyr + - bioconda::bioconductor-genomeinfodb + - bioconda::bioconductor-annotationdbi From a853af35594324037380d65099971fdb3e434be8 Mon Sep 17 00:00:00 2001 From: Olivier Date: Wed, 3 Dec 2025 13:06:47 +0100 Subject: [PATCH 213/258] limit number of polars threads when not using containers --- bin/clean_gene_ids.py | 8 ++-- bin/download_geo_data.R | 8 +++- bin/normalise_microarray.R | 3 +- conf/base.config | 2 +- conf/local.config | 2 +- modules/local/aggregate_results/main.nf | 6 +++ modules/local/clean_gene_ids/main.nf | 6 +++ modules/local/compute_base_statistics/main.nf | 6 +++ .../local/compute_stability_scores/main.nf | 6 +++ modules/local/dash_app/main.nf | 6 +++ .../local/genorm/compute_m_measure/main.nf | 6 +++ modules/local/genorm/cross_join/main.nf | 6 +++ modules/local/genorm/expression_ratio/main.nf | 6 +++ modules/local/genorm/make_chunks/main.nf | 6 +++ .../genorm/ratio_standard_variation/main.nf | 6 +++ modules/local/get_candidate_genes/main.nf | 6 +++ modules/local/merge_counts/main.nf | 6 +++ modules/local/normfinder/main.nf | 7 +++ modules/local/old/clean_count_data/main.nf | 6 +++ modules/local/rename_gene_ids/main.nf | 6 +++ .../main.nf | 43 ++++++++++++------- 21 files changed, 133 insertions(+), 24 deletions(-) diff --git a/bin/clean_gene_ids.py b/bin/clean_gene_ids.py index d3655a16..c5786141 100755 --- a/bin/clean_gene_ids.py +++ b/bin/clean_gene_ids.py @@ -36,16 +36,16 @@ def parse_args(): return parser.parse_args() -def parse_table(file: Path, **kwargs): +def parse_table(file: Path): if file.suffix == ".csv": - return pd.read_csv(file, header=0, **kwargs) + return pd.read_csv(file, header=0, index_col=0) else: # .tsv - return pd.read_csv(file, header=0, sep="\t", **kwargs) + return pd.read_csv(file, header=0, sep="\t", index_col=0) def parse_count_table(file: Path): # transitting to pandas dataframe helps to avoid parsing errors - df = parse_table(file, index_col=0) + df = parse_table(file) # whatever the name of the first col, rename it to "gene_id" df.index.rename(config.GENE_ID_COLNAME, inplace=True) df.index = df.index.astype(str) diff --git a/bin/download_geo_data.R b/bin/download_geo_data.R index 498e885a..4b8dcc1e 100755 --- a/bin/download_geo_data.R +++ b/bin/download_geo_data.R @@ -131,6 +131,7 @@ download_geo_data_with_retries <- function(accession, max_retries = 3, wait_time get_experiment_data <- function(geo_data) { data <- geo_data[[1]] experiment_data <- experimentData(data) + #print(experiment_data) return(experiment_data) } @@ -348,6 +349,7 @@ make_overall_design <- function(geo_data, series) { for (i in 1:length(geo_data)) { data <- geo_data[[ i ]] metadata <- pData(data) + #print(metadata) # make design dataframe # keep only samples corresponding to the species of interest design_df <- make_design(metadata, series) @@ -477,6 +479,10 @@ get_all_rnaseq_counts <- function(platform) { message(paste("Multiple columns found for sample", sample)) } + # in case there is already a gene_id column, remove it + if ("gene_id" %in% names(counts)) { + counts <- counts[, -which(names(counts) == "gene_id")] + } # setting the row names (gene ids) as a column counts <- tibble::rownames_to_column(counts, var = "gene_id") # adding to list @@ -571,8 +577,6 @@ check_rnaseq_normalisation_state <- function(counts, platform) { } }, error = function(e) { - print(head(counts)) - print(e) write_warning(paste(platform$id, ": COULD NOT COMPUTE FLOOR")) return("unknown") }) diff --git a/bin/normalise_microarray.R b/bin/normalise_microarray.R index 5245909e..f9343ebf 100755 --- a/bin/normalise_microarray.R +++ b/bin/normalise_microarray.R @@ -118,10 +118,11 @@ main <- function() { # cleaning colnames colnames(normalised_data_df) <- sub("\\..*", "", colnames(normalised_data_df)) + colnames(normalised_data_df) <- sub("-", "_", colnames(normalised_data_df)) # Save results message("Saving results to normalised_expression.csv") - write.csv(normalised_data, "normalised_expression.csv") + write.csv(normalised_data_df, "normalised_expression.csv") } diff --git a/conf/base.config b/conf/base.config index 4de37115..23879b16 100644 --- a/conf/base.config +++ b/conf/base.config @@ -25,7 +25,7 @@ process { sleep(Math.pow(2, task.attempt) * 200 as long) 'retry' } else { // after 3 retries, ignore the error - 'terminate' + 'ignore' } } maxRetries = 10 diff --git a/conf/local.config b/conf/local.config index 6b038a94..7eaf470d 100644 --- a/conf/local.config +++ b/conf/local.config @@ -10,7 +10,7 @@ profiles { } executor { - cpus = 16 + cpus = 8 memory = 25.GB } } diff --git a/modules/local/aggregate_results/main.nf b/modules/local/aggregate_results/main.nf index e6f493b4..55d5c954 100644 --- a/modules/local/aggregate_results/main.nf +++ b/modules/local/aggregate_results/main.nf @@ -28,7 +28,13 @@ process AGGREGATE_RESULTS { def metadata_files_arg = metadata_files ? "--metadata " + "$metadata_files" : "" def rnaseq_dataset_stat_file_arg = rnaseq_dataset_stat_file ? "--rnaseq $rnaseq_dataset_stat_file" : "" def microarray_dataset_stat_file_arg = microarray_dataset_stat_file ? "--microarray $microarray_dataset_stat_file" : "" + def is_using_containers = workflow.containerEngine ? true : false """ + # limiting number of threads when using conda / micromamba + if [ "${is_using_containers}" == "false" ]; then + export POLARS_MAX_THREADS=${task.cpus} + fi + aggregate_results.py \\ --counts $count_file \\ --stats $stat_file \\ diff --git a/modules/local/clean_gene_ids/main.nf b/modules/local/clean_gene_ids/main.nf index b3fc38ad..6269c2c8 100644 --- a/modules/local/clean_gene_ids/main.nf +++ b/modules/local/clean_gene_ids/main.nf @@ -20,7 +20,13 @@ process CLEAN_GENE_IDS { tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions script: + def is_using_containers = workflow.containerEngine ? true : false """ + # limiting number of threads when using conda / micromamba + if [ "${is_using_containers}" == "false" ]; then + export POLARS_MAX_THREADS=${task.cpus} + fi + clean_gene_ids.py \\ --count-file "$count_file" """ diff --git a/modules/local/compute_base_statistics/main.nf b/modules/local/compute_base_statistics/main.nf index 11bf56c3..22d7bd1a 100644 --- a/modules/local/compute_base_statistics/main.nf +++ b/modules/local/compute_base_statistics/main.nf @@ -27,7 +27,13 @@ process COMPUTE_BASE_STATISTICS { if ( platform != [] ) { args += " --platform $platform" } + def is_using_containers = workflow.containerEngine ? true : false """ + # limiting number of threads when using conda / micromamba + if [ "${is_using_containers}" == "false" ]; then + export POLARS_MAX_THREADS=${task.cpus} + fi + compute_base_statistics.py \\ --counts $count_file \\ $args diff --git a/modules/local/compute_stability_scores/main.nf b/modules/local/compute_stability_scores/main.nf index d5ae8996..19e67cfe 100644 --- a/modules/local/compute_stability_scores/main.nf +++ b/modules/local/compute_stability_scores/main.nf @@ -20,7 +20,13 @@ process COMPUTE_STABILITY_SCORES { script: def genorm_stability_file_arg = genorm_stability_file ? "--genorm-stability $genorm_stability_file" : "" + def is_using_containers = workflow.containerEngine ? true : false """ + # limiting number of threads when using conda / micromamba + if [ "${is_using_containers}" == "false" ]; then + export POLARS_MAX_THREADS=${task.cpus} + fi + compute_stability_scores.py \\ --stats $stat_file \\ --weights "$stability_score_weights" \\ diff --git a/modules/local/dash_app/main.nf b/modules/local/dash_app/main.nf index 4b966434..73dab84d 100644 --- a/modules/local/dash_app/main.nf +++ b/modules/local/dash_app/main.nf @@ -27,7 +27,13 @@ process DASH_APP { path "versions.yml", emit: versions script: + def is_using_containers = workflow.containerEngine ? true : false """ + # limiting number of threads when using conda / micromamba + if [ "${is_using_containers}" == "false" ]; then + export POLARS_MAX_THREADS=${task.cpus} + fi + mkdir -p data mv ${all_counts} ${whole_design} ${all_genes_summary} data/ cp -r ${moduleDir}/app/* . diff --git a/modules/local/genorm/compute_m_measure/main.nf b/modules/local/genorm/compute_m_measure/main.nf index a75c9c3f..e2048e0a 100644 --- a/modules/local/genorm/compute_m_measure/main.nf +++ b/modules/local/genorm/compute_m_measure/main.nf @@ -19,7 +19,13 @@ process COMPUTE_M_MEASURE { script: def args = "--task-attempts ${task.attempt}" + def is_using_containers = workflow.containerEngine ? true : false """ + # limiting number of threads when using conda / micromamba + if [ "${is_using_containers}" == "false" ]; then + export POLARS_MAX_THREADS=${task.cpus} + fi + compute_m_measures.py \\ --counts $count_file \\ --std-files "$files" $args diff --git a/modules/local/genorm/cross_join/main.nf b/modules/local/genorm/cross_join/main.nf index 98545e6d..9c6f4f5b 100644 --- a/modules/local/genorm/cross_join/main.nf +++ b/modules/local/genorm/cross_join/main.nf @@ -18,7 +18,13 @@ process CROSS_JOIN { script: def args = "--task-attempts ${task.attempt}" + def is_using_containers = workflow.containerEngine ? true : false """ + # limiting number of threads when using conda / micromamba + if [ "${is_using_containers}" == "false" ]; then + export POLARS_MAX_THREADS=${task.cpus} + fi + make_cross_join.py \\ --file1 count_chunk_file_1 \\ --file2 count_chunk_file_2 \\ diff --git a/modules/local/genorm/expression_ratio/main.nf b/modules/local/genorm/expression_ratio/main.nf index fd031ee0..5299a591 100644 --- a/modules/local/genorm/expression_ratio/main.nf +++ b/modules/local/genorm/expression_ratio/main.nf @@ -18,7 +18,13 @@ process EXPRESSION_RATIO { script: def args = "--task-attempts ${task.attempt}" + def is_using_containers = workflow.containerEngine ? true : false """ + # limiting number of threads when using conda / micromamba + if [ "${is_using_containers}" == "false" ]; then + export POLARS_MAX_THREADS=${task.cpus} + fi + make_pairwise_gene_expression_ratio.py --file $file """ diff --git a/modules/local/genorm/make_chunks/main.nf b/modules/local/genorm/make_chunks/main.nf index fc877c24..ea17520f 100644 --- a/modules/local/genorm/make_chunks/main.nf +++ b/modules/local/genorm/make_chunks/main.nf @@ -18,7 +18,13 @@ process MAKE_CHUNKS { script: def args = "--task-attempts ${task.attempt}" + def is_using_containers = workflow.containerEngine ? true : false """ + # limiting number of threads when using conda / micromamba + if [ "${is_using_containers}" == "false" ]; then + export POLARS_MAX_THREADS=${task.cpus} + fi + make_parquet_chunks.py --counts $count_file $args """ diff --git a/modules/local/genorm/ratio_standard_variation/main.nf b/modules/local/genorm/ratio_standard_variation/main.nf index e92cd098..624c26a8 100644 --- a/modules/local/genorm/ratio_standard_variation/main.nf +++ b/modules/local/genorm/ratio_standard_variation/main.nf @@ -18,7 +18,13 @@ process RATIO_STANDARD_VARIATION { script: def args = "--task-attempts ${task.attempt}" + def is_using_containers = workflow.containerEngine ? true : false """ + # limiting number of threads when using conda / micromamba + if [ "${is_using_containers}" == "false" ]; then + export POLARS_MAX_THREADS=${task.cpus} + fi + get_ratio_standard_variation.py --file $file $args """ diff --git a/modules/local/get_candidate_genes/main.nf b/modules/local/get_candidate_genes/main.nf index ffd6d72c..c91beaff 100644 --- a/modules/local/get_candidate_genes/main.nf +++ b/modules/local/get_candidate_genes/main.nf @@ -20,7 +20,13 @@ process GET_CANDIDATE_GENES { tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions script: + def is_using_containers = workflow.containerEngine ? true : false """ + # limiting number of threads when using conda / micromamba + if [ "${is_using_containers}" == "false" ]; then + export POLARS_MAX_THREADS=${task.cpus} + fi + get_candidate_genes.py \\ --counts $count_file \\ --stats $stat_file \\ diff --git a/modules/local/merge_counts/main.nf b/modules/local/merge_counts/main.nf index fab4714b..cddd5443 100644 --- a/modules/local/merge_counts/main.nf +++ b/modules/local/merge_counts/main.nf @@ -24,7 +24,13 @@ process MERGE_COUNTS { tuple val("${task.process}"), val('tqdm'), eval('python3 -c "import tqdm; print(tqdm.__version__)"'), topic: versions script: + def is_using_containers = workflow.containerEngine ? true : false """ + # limiting number of threads when using conda / micromamba + if [ "${is_using_containers}" == "false" ]; then + export POLARS_MAX_THREADS=${task.cpus} + fi + merge_counts.py \\ --counts "$count_files" """ diff --git a/modules/local/normfinder/main.nf b/modules/local/normfinder/main.nf index 7f12146b..f6c8b155 100644 --- a/modules/local/normfinder/main.nf +++ b/modules/local/normfinder/main.nf @@ -17,13 +17,20 @@ process NORMFINDER { tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions script: + def is_using_containers = workflow.containerEngine ? true : false """ + # limiting number of threads when using conda / micromamba + if [ "${is_using_containers}" == "false" ]; then + export POLARS_MAX_THREADS=${task.cpus} + fi + normfinder.py \ --counts $count_file \ --design $design_file """ stub: + """ touch stability_values.normfinder.csv """ diff --git a/modules/local/old/clean_count_data/main.nf b/modules/local/old/clean_count_data/main.nf index 357ca9eb..0500f78a 100644 --- a/modules/local/old/clean_count_data/main.nf +++ b/modules/local/old/clean_count_data/main.nf @@ -20,7 +20,13 @@ process CLEAN_COUNT_DATA { tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions script: + def is_using_containers = workflow.containerEngine ? true : false """ + # limiting number of threads when using conda / micromamba + if [ "${is_using_containers}" == "false" ]; then + export POLARS_MAX_THREADS=${task.cpus} + fi + clean_count_data.py \\ --counts $count_file \\ --ks-stats $ks_stats_file \\ diff --git a/modules/local/rename_gene_ids/main.nf b/modules/local/rename_gene_ids/main.nf index 7813adb9..58e09df7 100644 --- a/modules/local/rename_gene_ids/main.nf +++ b/modules/local/rename_gene_ids/main.nf @@ -24,7 +24,13 @@ process RENAME_GENE_IDS { script: def mapping_arg = gene_id_mapping_file ? "--mappings $gene_id_mapping_file" : "" + def is_using_containers = workflow.containerEngine ? true : false """ + # limiting number of threads when using conda / micromamba + if [ "${is_using_containers}" == "false" ]; then + export POLARS_MAX_THREADS=${task.cpus} + fi + rename_gene_ids.py \\ --count-file "$count_file" \\ $mapping_arg diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index 482e9a71..212e7e57 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -230,23 +230,36 @@ def parseInputDatasets(samplesheet) { // // Validate channels from input samplesheet // -def validateInputSamplesheet(input) { +def validateInputSamplesheet( ch_datasets ) { // checking that all microarray datasets (if any) are normalised - input.filter { - meta, file -> - meta.platform == 'microarray' && !meta.normalised - } - .count() - .map { count -> - if (count > 0) { - def error_text = [ - "Error: You provided at least one microarray dataset that is not normalised. ", - "Microarray datasets must already be normalised before being submitted. ", - "Please perform normalisation (typically using RMA for one-colour intensities / LOESS (limma) for two-colour intensities) and run again." - ].join(' ').trim() - error(error_text) + ch_datasets + .filter { + meta, file -> + meta.platform == 'microarray' && !meta.normalised + } + .count() + .map { count -> + if (count > 0) { + def error_text = [ + "Error: You provided at least one microarray dataset that is not normalised. ", + "Microarray datasets must already be normalised before being submitted. ", + "Please perform normalisation (typically using RMA for one-colour intensities / LOESS (limma) for two-colour intensities) and run again." + ].join(' ').trim() + error(error_text) + } + } + + // checking that all count files are well formated (same number of columns in header and rows) + ch_datasets + .map { meta, file -> + def header = file.withReader { reader -> reader.readLine() } + def separator = header.contains(',') ? "," : + header.contains('\t') ? "\t" : + " " + def first_row = file.splitCsv( header: false, skip: 1, limit: 1, sep: separator ) + + assert header.split(separator).size() == first_row[0].size() : "Header and first row do not have the same number of columns in file ${file}" } - } } // From d8b5827d4fba62e3e654df71a32230c9e17017bf Mon Sep 17 00:00:00 2001 From: Olivier Date: Thu, 4 Dec 2025 13:06:12 +0100 Subject: [PATCH 214/258] fix tests --- .../local/normalisation/compute_cpm/main.nf | 2 +- .../local/normalisation/compute_tpm/main.nf | 2 +- tests/default.nf.test.snap | 1300 ++++++++++++----- .../local/aggregate_results/main.nf.test.snap | 60 +- .../local/clean_count_data/main.nf.test | 57 - .../local/clean_count_data/main.nf.test.snap | 83 -- .../compute_base_statistics/main.nf.test.snap | 36 +- .../compute_dataset_statistics/main.nf.test | 30 + .../main.nf.test.snap | 25 + .../main.nf.test.snap | 20 +- .../local/dataset_statistics/main.nf.test | 54 - .../dataset_statistics/main.nf.test.snap | 112 -- .../genorm/expression_ratio/main.nf.test.snap | 20 +- .../genorm/make_chunks/main.nf.test.snap | 30 +- .../main.nf.test.snap | 20 +- .../local/geo/getaccessions/main.nf.test | 24 +- .../local/geo/getdata/main.nf.test.snap | 57 +- .../get_candidate_genes/main.nf.test.snap | 36 +- .../local/gprofiler/idmapping/main.nf.test | 4 +- .../gprofiler/idmapping/main.nf.test.snap | 29 + .../local/merge_counts/main.nf.test.snap | 18 +- .../{edger => compute_cpm}/main.nf.test | 39 +- .../compute_cpm/main.nf.test.snap | 190 +++ .../{deseq2 => compute_tpm}/main.nf.test | 44 +- .../compute_tpm/main.nf.test.snap | 190 +++ .../normalisation/deseq2/main.nf.test.snap | 70 - .../normalisation/edger/main.nf.test.snap | 70 - .../local/normfinder/main.nf.test.snap | 36 +- .../quantile_normalisation/main.nf.test.snap | 12 +- .../local/rename_gene_ids/main.nf.test | 4 +- .../local/rename_gene_ids/main.nf.test.snap | 50 +- .../download_public_datasets/main.nf.test | 70 + .../main.nf.test.snap | 91 ++ .../expression_normalisation/main.nf.test | 59 +- .../main.nf.test.snap | 70 +- .../expressionatlas_fetchdata/main.nf.test | 127 -- .../main.nf.test.snap | 268 ---- .../local/genorm/main.nf.test.snap | 6 +- .../local/geo_fetchdata/main.nf.test | 140 -- .../local/geo_fetchdata/main.nf.test.snap | 148 -- .../local/get_public_accessions/main.nf.test | 200 +++ .../get_public_accessions/main.nf.test.snap | 110 ++ .../normalisation/base/gene_lengths.csv | 13 + .../normalisation/many_zeros/gene_lengths.csv | 6 + .../normalisation/one_group/gene_lengths.csv | 6 + .../exclude_one_geo_accession.txt} | 0 .../exclude_two_geo_accessions.txt} | 0 47 files changed, 2179 insertions(+), 1859 deletions(-) delete mode 100644 tests/modules/local/clean_count_data/main.nf.test delete mode 100644 tests/modules/local/clean_count_data/main.nf.test.snap create mode 100644 tests/modules/local/compute_dataset_statistics/main.nf.test create mode 100644 tests/modules/local/compute_dataset_statistics/main.nf.test.snap delete mode 100644 tests/modules/local/dataset_statistics/main.nf.test delete mode 100644 tests/modules/local/dataset_statistics/main.nf.test.snap rename tests/modules/local/normalisation/{edger => compute_cpm}/main.nf.test (57%) create mode 100644 tests/modules/local/normalisation/compute_cpm/main.nf.test.snap rename tests/modules/local/normalisation/{deseq2 => compute_tpm}/main.nf.test (57%) create mode 100644 tests/modules/local/normalisation/compute_tpm/main.nf.test.snap delete mode 100644 tests/modules/local/normalisation/deseq2/main.nf.test.snap delete mode 100644 tests/modules/local/normalisation/edger/main.nf.test.snap create mode 100644 tests/subworkflows/local/download_public_datasets/main.nf.test create mode 100644 tests/subworkflows/local/download_public_datasets/main.nf.test.snap delete mode 100644 tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test delete mode 100644 tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test.snap delete mode 100644 tests/subworkflows/local/geo_fetchdata/main.nf.test delete mode 100644 tests/subworkflows/local/geo_fetchdata/main.nf.test.snap create mode 100644 tests/subworkflows/local/get_public_accessions/main.nf.test create mode 100644 tests/subworkflows/local/get_public_accessions/main.nf.test.snap create mode 100644 tests/test_data/normalisation/base/gene_lengths.csv create mode 100644 tests/test_data/normalisation/many_zeros/gene_lengths.csv create mode 100644 tests/test_data/normalisation/one_group/gene_lengths.csv rename tests/test_data/{geo/get_accessions/exclude_one_accession.txt => public_accessions/exclude_one_geo_accession.txt} (100%) rename tests/test_data/{geo/get_accessions/exclude_two_accessions.txt => public_accessions/exclude_two_geo_accessions.txt} (100%) diff --git a/modules/local/normalisation/compute_cpm/main.nf b/modules/local/normalisation/compute_cpm/main.nf index a10af424..06fc5a0c 100644 --- a/modules/local/normalisation/compute_cpm/main.nf +++ b/modules/local/normalisation/compute_cpm/main.nf @@ -13,7 +13,7 @@ process NORMALISATION_COMPUTE_CPM { tuple val(meta), path(count_file) output: - tuple val(meta), path('*.cpm.csv'), optional: true, emit: counts + tuple val(meta), path('*.cpm.csv'), optional: true, emit: counts tuple val(meta.dataset), path("failure_reason.txt"), optional: true, topic: normalisation_failure_reason tuple val(meta.dataset), path("warning_reason.txt"), optional: true, topic: normalisation_warning_reason tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions diff --git a/modules/local/normalisation/compute_tpm/main.nf b/modules/local/normalisation/compute_tpm/main.nf index b9469d7d..561ee580 100644 --- a/modules/local/normalisation/compute_tpm/main.nf +++ b/modules/local/normalisation/compute_tpm/main.nf @@ -14,7 +14,7 @@ process NORMALISATION_COMPUTE_TPM { path gene_lengths_file output: - tuple val(meta), path('*.tpm.csv'), optional: true, emit: counts + tuple val(meta), path('*.tpm.csv'), optional: true, emit: counts tuple val(meta.dataset), path("failure_reason.txt"), optional: true, topic: normalisation_failure_reason tuple val(meta.dataset), path("warning_reason.txt"), optional: true, topic: normalisation_warning_reason tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions diff --git a/tests/default.nf.test.snap b/tests/default.nf.test.snap index ffa5d8e4..985c18a9 100644 --- a/tests/default.nf.test.snap +++ b/tests/default.nf.test.snap @@ -8,12 +8,20 @@ "aggregate_results/all_genes_summary.csv", "aggregate_results/top_stable_genes_summary.csv", "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", - "collect_gene_ids", - "collect_gene_ids/all_gene_ids.txt", + "clean_gene_ids", + "clean_gene_ids/SRP254919.salmon.merged.gene_counts.top1000cov.assay.cleaned.csv", + "collect_statistics", + "collect_statistics/ratio_zeros.transposed.csv", + "collect_statistics/skewness.transposed.csv", "compute_base_statistics", "compute_base_statistics/stats_all_genes.csv", "compute_base_statistics_for_rnaseq", "compute_base_statistics_for_rnaseq/rnaseq.stats_all_genes.csv", + "compute_dataset_statistics", + "compute_dataset_statistics/ratio_zeros.txt", + "compute_dataset_statistics/skewness.txt", + "compute_gene_transcript_lengths", + "compute_gene_transcript_lengths/gene_transcript_lengths.csv", "compute_stability_scores", "compute_stability_scores/stats_with_scores.csv", "dash_app", @@ -24,9 +32,9 @@ "dash_app/data/all_counts.parquet", "dash_app/data/all_genes_summary.csv", "dash_app/data/whole_design.csv", + "dash_app/environment.yml", "dash_app/file_system_backend", "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", - "dash_app/spec-file.txt", "dash_app/src", "dash_app/src/callbacks", "dash_app/src/callbacks/common.py", @@ -48,13 +56,23 @@ "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", "dash_app/versions.yml", + "download_ensembl_annotation", + "download_ensembl_annotation/Mus_musculus.GRCm39.115.chr.gff3.gz", "errors", - "geo", "get_candidate_genes", "get_candidate_genes/candidate_counts.parquet", "idmapping", - "idmapping/gene_metadata.csv", - "idmapping/mapped_gene_ids.csv", + "idmapping/collected_gene_ids", + "idmapping/collected_gene_ids/all_gene_ids.txt", + "idmapping/global_gene_id_mapping.csv", + "idmapping/global_gene_metadata.csv", + "idmapping/gprofiler", + "idmapping/gprofiler/gene_metadata.csv", + "idmapping/gprofiler/mapped_gene_ids.csv", + "idmapping/original_gene_ids.txt", + "idmapping/renamed", + "idmapping/renamed/SRP254919.salmon.merged.gene_counts.top1000cov.assay.cleaned.renamed.csv", + "idmapping/renamed/warning_reason.txt", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", "merge_all_counts", @@ -73,6 +91,9 @@ "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", "multiqc/multiqc_data/multiqc_gene_statistics.txt", "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", + "multiqc/multiqc_data/multiqc_ratio_zeros.txt", + "multiqc/multiqc_data/multiqc_renaming_warning_reasons.txt", + "multiqc/multiqc_data/multiqc_skewness.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", "multiqc/multiqc_plots", @@ -80,47 +101,61 @@ "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", "multiqc/multiqc_plots/pdf/gene_statistics.pdf", "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", + "multiqc/multiqc_plots/pdf/ratio_zeros.pdf", + "multiqc/multiqc_plots/pdf/renaming_warning_reasons.pdf", + "multiqc/multiqc_plots/pdf/skewness.pdf", "multiqc/multiqc_plots/png", "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", "multiqc/multiqc_plots/png/gene_statistics.png", "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", + "multiqc/multiqc_plots/png/ratio_zeros.png", + "multiqc/multiqc_plots/png/renaming_warning_reasons.png", + "multiqc/multiqc_plots/png/skewness.png", "multiqc/multiqc_plots/svg", "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", "multiqc/multiqc_plots/svg/gene_statistics.svg", "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", + "multiqc/multiqc_plots/svg/ratio_zeros.svg", + "multiqc/multiqc_plots/svg/renaming_warning_reasons.svg", + "multiqc/multiqc_plots/svg/skewness.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", "normalised", "normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay", - "normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay/normalisation_deseq2", - "normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay/normalisation_deseq2/SRP254919.salmon.merged.gene_counts.top1000cov.assay.renamed.cpm.csv", + "normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay/quantile_normalised", + "normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay/quantile_normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay/tpm", + "normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay/tpm/SRP254919.salmon.merged.gene_counts.top1000cov.assay.cleaned.renamed.tpm.csv", "normfinder", "normfinder/stability_values.normfinder.csv", "pipeline_info", "pipeline_info/software_mqc_versions.yml", - "quantile_normalised", - "quantile_normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay", - "quantile_normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay/SRP254919.salmon.merged.gene_counts.top1000cov.assay.renamed.cpm.quant_norm.parquet", - "rename_gene_ids", - "rename_gene_ids/SRP254919.salmon.merged.gene_counts.top1000cov.assay.renamed.csv", - "rename_gene_ids/warning_reason.txt", + "statistics", + "statistics/id_mapping_stats.csv", + "statistics/ratio_zeros.csv", + "statistics/skewness.csv", "warnings", "warnings/renaming_warning_reasons.tsv" ], [ - "all_genes_summary.csv:md5,3e247792dc392db19887cf7423569495", - "top_stable_genes_summary.csv:md5,3e247792dc392db19887cf7423569495", - "top_stable_genes_transposed_counts_filtered.csv:md5,5ec0c8fa780b269c163d962eb01adea8", - "all_gene_ids.txt:md5,8ad5bcdc524384021376a78bc2565a99", - "stats_all_genes.csv:md5,8fc34fdea55993cad7ef2db2faf651c0", - "rnaseq.stats_all_genes.csv:md5,faf7993cd157c40141ddb323aa7ac36c", - "stats_with_scores.csv:md5,60b002a9be7769de2a90d246ed57b7f5", + "all_genes_summary.csv:md5,54b96cbfb5e464ec086c7e1308701e02", + "top_stable_genes_summary.csv:md5,54b96cbfb5e464ec086c7e1308701e02", + "top_stable_genes_transposed_counts_filtered.csv:md5,af21e36c540965846b73245678b74f36", + "SRP254919.salmon.merged.gene_counts.top1000cov.assay.cleaned.csv:md5,c5f077545a14e2078194217ff227d3fc", + "ratio_zeros.transposed.csv:md5,735d304aca8999a5f6e015de297ac1cd", + "skewness.transposed.csv:md5,3ff31df5b69ba9b12181024de818e54e", + "stats_all_genes.csv:md5,4e67b237e0c420363cd5e90add47eb21", + "rnaseq.stats_all_genes.csv:md5,5bcc5671388dc946d8f4e276de23c584", + "ratio_zeros.txt:md5,fa050b20490f0186d5aae53c3355b520", + "skewness.txt:md5,6b257ec852978ea594bec11668ada22f", + "gene_transcript_lengths.csv:md5,09e2d2a8881df9aa96ee71802e9c3451", + "stats_with_scores.csv:md5,d9236329475b316e1e1983ccb76f0c1b", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_genes_summary.csv:md5,3e247792dc392db19887cf7423569495", + "all_genes_summary.csv:md5,54b96cbfb5e464ec086c7e1308701e02", "whole_design.csv:md5,f29515bc2c783e593fb9028127342593", + "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", - "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", "genes.py:md5,680cb5f4e107a3b091821917d72a555c", "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", @@ -137,23 +172,31 @@ "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", "style.py:md5,b9f1207f06464e43c05e6e3912f12731", "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "gene_metadata.csv:md5,e7b851264bc7df54a125c94c716ccea8", - "mapped_gene_ids.csv:md5,5177a0ac44d712aac833f780edbf8503", - "whole_gene_id_mapping.csv:md5,c938897a22f53321c60dd91e8919632e", - "whole_gene_metadata.csv:md5,9e6318fedc235ee80109daccc483fdf6", + "Mus_musculus.GRCm39.115.chr.gff3.gz:md5,66a5d70eeb2ce9685ca871fc7b0f4f96", + "all_gene_ids.txt:md5,6e8078e6c239924656b22f923948cc4e", + "global_gene_id_mapping.csv:md5,d907d4ba88b4f56a268ea6f9477e76bc", + "global_gene_metadata.csv:md5,48de6014d2f8461a5c1abc7647d202dc", + "gene_metadata.csv:md5,c69fa32bf2cc150958d3b0dbe809d946", + "mapped_gene_ids.csv:md5,9a27b6f030f45d39f05cd8fbf3388383", + "original_gene_ids.txt:md5,e6c4d2e009b8af56746d15806786f8b2", + "SRP254919.salmon.merged.gene_counts.top1000cov.assay.cleaned.renamed.csv:md5,c855d1848a74842378c812556ddd2e1f", + "warning_reason.txt:md5,b13d82afc1a3752e78dd796fb1c53d52", + "whole_gene_id_mapping.csv:md5,78934d2ac5fe7d863f114c5703f57a06", + "whole_gene_metadata.csv:md5,bd76860b422e45eca7cd583212a977d2", "whole_design.csv:md5,f29515bc2c783e593fb9028127342593", - "SRP254919.salmon.merged.gene_counts.top1000cov.assay.renamed.cpm.csv:md5,e2ab50b0ae172252c167f9417522f299", - "stability_values.normfinder.csv:md5,0cd79a0ea440668c1b953a14f7b1d9cd", - "SRP254919.salmon.merged.gene_counts.top1000cov.assay.renamed.csv:md5,dccda08cf7401f15b822f4a476c240c8", - "warning_reason.txt:md5,938cbf4fb283483dd717106f3bfdee07", - "renaming_warning_reasons.tsv:md5,404d8e09a572a5e6aa94ec75d4c4a1dd" + "SRP254919.salmon.merged.gene_counts.top1000cov.assay.cleaned.renamed.tpm.csv:md5,5b517b410a643c4e6fbffb55dc1cd1a7", + "stability_values.normfinder.csv:md5,000e97bed8b1c468ec71704d6accc804", + "id_mapping_stats.csv:md5,2b1b9478e4eafbfd43a078894d9be122", + "ratio_zeros.csv:md5,5a667d505cbd2cc7057ee47b70536c2e", + "skewness.csv:md5,582683980eadf84d32853a21f9dce230", + "renaming_warning_reasons.tsv:md5,0a11a59b5b547a39ab7a0e4dac622173" ] ], "meta": { "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nextflow": "25.10.2" }, - "timestamp": "2025-11-20T19:30:06.162229336" + "timestamp": "2025-12-04T11:38:53.191359064" }, "-profile test_eatlas_only_with_keywords": { "content": [ @@ -164,12 +207,20 @@ "aggregate_results/all_genes_summary.csv", "aggregate_results/top_stable_genes_summary.csv", "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", - "collect_gene_ids", - "collect_gene_ids/all_gene_ids.txt", + "clean_gene_ids", + "clean_gene_ids/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.csv", + "collect_statistics", + "collect_statistics/ratio_zeros.transposed.csv", + "collect_statistics/skewness.transposed.csv", "compute_base_statistics", "compute_base_statistics/stats_all_genes.csv", "compute_base_statistics_for_rnaseq", "compute_base_statistics_for_rnaseq/rnaseq.stats_all_genes.csv", + "compute_dataset_statistics", + "compute_dataset_statistics/ratio_zeros.txt", + "compute_dataset_statistics/skewness.txt", + "compute_gene_transcript_lengths", + "compute_gene_transcript_lengths/gene_transcript_lengths.csv", "compute_stability_scores", "compute_stability_scores/stats_with_scores.csv", "dash_app", @@ -180,9 +231,9 @@ "dash_app/data/all_counts.parquet", "dash_app/data/all_genes_summary.csv", "dash_app/data/whole_design.csv", + "dash_app/environment.yml", "dash_app/file_system_backend", "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", - "dash_app/spec-file.txt", "dash_app/src", "dash_app/src/callbacks", "dash_app/src/callbacks/common.py", @@ -204,21 +255,22 @@ "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", "dash_app/versions.yml", + "download_ensembl_annotation", + "download_ensembl_annotation/Beta_vulgaris.RefBeet-1.2.2.62.gff3.gz", "errors", - "expression_atlas", - "expression_atlas/accessions", - "expression_atlas/accessions/accessions.txt", - "expression_atlas/accessions/selected_experiments.metadata.tsv", - "expression_atlas/accessions/species_experiments.metadata.tsv", - "expression_atlas/datasets", - "expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", - "expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", - "geo", "get_candidate_genes", "get_candidate_genes/candidate_counts.parquet", "idmapping", - "idmapping/gene_metadata.csv", - "idmapping/mapped_gene_ids.csv", + "idmapping/collected_gene_ids", + "idmapping/collected_gene_ids/all_gene_ids.txt", + "idmapping/global_gene_id_mapping.csv", + "idmapping/global_gene_metadata.csv", + "idmapping/gprofiler", + "idmapping/gprofiler/gene_metadata.csv", + "idmapping/gprofiler/mapped_gene_ids.csv", + "idmapping/original_gene_ids.txt", + "idmapping/renamed", + "idmapping/renamed/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", "merge_all_counts", @@ -236,59 +288,84 @@ "multiqc/multiqc_data/multiqc_data.json", "multiqc/multiqc_data/multiqc_eatlas_all_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", "multiqc/multiqc_data/multiqc_gene_statistics.txt", "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", + "multiqc/multiqc_data/multiqc_ratio_zeros.txt", + "multiqc/multiqc_data/multiqc_skewness.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", "multiqc/multiqc_plots", "multiqc/multiqc_plots/pdf", "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", "multiqc/multiqc_plots/pdf/gene_statistics.pdf", "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", + "multiqc/multiqc_plots/pdf/ratio_zeros.pdf", + "multiqc/multiqc_plots/pdf/skewness.pdf", "multiqc/multiqc_plots/png", "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", + "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", "multiqc/multiqc_plots/png/gene_statistics.png", "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", + "multiqc/multiqc_plots/png/ratio_zeros.png", + "multiqc/multiqc_plots/png/skewness.png", "multiqc/multiqc_plots/svg", "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", "multiqc/multiqc_plots/svg/gene_statistics.svg", "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", + "multiqc/multiqc_plots/svg/ratio_zeros.svg", + "multiqc/multiqc_plots/svg/skewness.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", "normalised", "normalised/E_MTAB_8187_rnaseq", - "normalised/E_MTAB_8187_rnaseq/normalisation_deseq2", - "normalised/E_MTAB_8187_rnaseq/normalisation_deseq2/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normalised/E_MTAB_8187_rnaseq/quantile_normalised", + "normalised/E_MTAB_8187_rnaseq/quantile_normalised/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/E_MTAB_8187_rnaseq/tpm", + "normalised/E_MTAB_8187_rnaseq/tpm/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", "normfinder", "normfinder/stability_values.normfinder.csv", "pipeline_info", "pipeline_info/software_mqc_versions.yml", - "quantile_normalised", - "quantile_normalised/E_MTAB_8187_rnaseq", - "quantile_normalised/E_MTAB_8187_rnaseq/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", - "rename_gene_ids", - "rename_gene_ids/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv", - "rename_gene_ids/warning_reason.txt", - "warnings", - "warnings/renaming_warning_reasons.tsv" + "public_data", + "public_data/expression_atlas", + "public_data/expression_atlas/accessions", + "public_data/expression_atlas/accessions/accessions.txt", + "public_data/expression_atlas/accessions/selected_experiments.metadata.tsv", + "public_data/expression_atlas/accessions/species_experiments.metadata.tsv", + "public_data/expression_atlas/datasets", + "public_data/expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", + "public_data/expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", + "statistics", + "statistics/id_mapping_stats.csv", + "statistics/ratio_zeros.csv", + "statistics/skewness.csv", + "warnings" ], [ - "all_genes_summary.csv:md5,94feecb8ce32bcdee6c3e51de6e0dc75", - "top_stable_genes_summary.csv:md5,f3fc83cec676b75472d30e54c5ca9b6a", - "top_stable_genes_transposed_counts_filtered.csv:md5,86b247718f957d3a9364cf1a08189dc1", - "all_gene_ids.txt:md5,e4652fea77f29d9f31bf9353d6dfa9c2", - "stats_all_genes.csv:md5,f6699c8549e7a4cb9105f021d53590e1", - "rnaseq.stats_all_genes.csv:md5,9750eb04fe96150c411aa673a8dad3e2", - "stats_with_scores.csv:md5,d24a8d1ac6c7258346b664d132cf866a", + "all_genes_summary.csv:md5,32be3aeed5cd2715847ccc2fd45c09a0", + "top_stable_genes_summary.csv:md5,2e34161e2beb2f1e0921dbf6c0ce55ba", + "top_stable_genes_transposed_counts_filtered.csv:md5,5083c04020d1c504c86689e14f731ef7", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.csv:md5,c7f0662425535e1c04bbc58b22ba74eb", + "ratio_zeros.transposed.csv:md5,4a4dd03b969fbc1ae1d524bfdd90f92b", + "skewness.transposed.csv:md5,bc24ec43d4f638380fd7b0c726ddc157", + "stats_all_genes.csv:md5,43037fa4411410c93551f3d148cd428e", + "rnaseq.stats_all_genes.csv:md5,db157ff5e8c39681d82b9c75a36d3c75", + "ratio_zeros.txt:md5,ae64e9788a4e48e276ecdea3b33abb3f", + "skewness.txt:md5,ad6408796ac8bc51f29018c33f53cb55", + "gene_transcript_lengths.csv:md5,458c7dfd3598bdcbcb6ceb76ccba189f", + "stats_with_scores.csv:md5,3386b99dee6df83b3ea4ad7295f3fb97", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_genes_summary.csv:md5,94feecb8ce32bcdee6c3e51de6e0dc75", + "all_genes_summary.csv:md5,32be3aeed5cd2715847ccc2fd45c09a0", "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", + "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", - "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", "genes.py:md5,680cb5f4e107a3b091821917d72a555c", "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", @@ -305,36 +382,68 @@ "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", "style.py:md5,b9f1207f06464e43c05e6e3912f12731", "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", + "Beta_vulgaris.RefBeet-1.2.2.62.gff3.gz:md5,6f2c45809441c8776e6578000db2b0e4", + "all_gene_ids.txt:md5,1e5fd5cf759295190a0f0d5355011f54", + "global_gene_id_mapping.csv:md5,c664fae78b7433ded148bd94bcd23c67", + "global_gene_metadata.csv:md5,68d3a0bf612bef3f20d370012400dca5", + "gene_metadata.csv:md5,be6187aa4ae724f7e37e5a07099c40af", + "mapped_gene_ids.csv:md5,f26e6d00ff48a07fe28c056bbd6cde8c", + "original_gene_ids.txt:md5,8b21a07b1b6ac1f3d5aa6e2d40b28fc0", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,ed1600e42df05fc885a16a66b77324a1", + "whole_gene_id_mapping.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", + "whole_gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", + "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,b407990bb10106de943f9e93cda6323e", + "stability_values.normfinder.csv:md5,c762f74840a6e604d43c8f727a5826f5", "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", - "gene_metadata.csv:md5,79edd662afe94aad5125708c2d93abc5", - "mapped_gene_ids.csv:md5,5cb327ac5eec9ad4926c2745e67c621e", - "whole_gene_id_mapping.csv:md5,b061b542a0c3eb0ca5712ed3c4837bc4", - "whole_gene_metadata.csv:md5,727820034d9c2fdd54be98b55848aee0", - "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,73780e145259992a172ffa7ebdc8ddbd", - "stability_values.normfinder.csv:md5,78acf60ce7914a64bb1a1b80fc223170", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv:md5,d3efe7dc241425b9f3c190fe36c6e595", - "warning_reason.txt:md5,0042ef8923246d99e5ef2a9068caa1a8", - "renaming_warning_reasons.tsv:md5,794de71c61302d26675414b20f0ddf11" + "id_mapping_stats.csv:md5,9907d7b6c08067e23cc1b69d5966c998", + "ratio_zeros.csv:md5,d3b518709a097d9e41a05142b524f03c", + "skewness.csv:md5,b38aabc94d60d93b979c3cef3a922299" ] ], "meta": { "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nextflow": "25.10.2" }, - "timestamp": "2025-11-20T19:37:24.912985842" + "timestamp": "2025-12-04T11:42:55.68050177" }, "-profile test_skip_id_mapping": { "content": [ [ + "collect_statistics", + "collect_statistics/ratio_zeros.transposed.csv", + "collect_statistics/skewness.transposed.csv", + "compute_base_statistics", + "compute_base_statistics/stats_all_genes.csv", + "compute_base_statistics_for_microarray", + "compute_base_statistics_for_microarray/microarray.stats_all_genes.csv", + "compute_base_statistics_for_rnaseq", + "compute_base_statistics_for_rnaseq/rnaseq.stats_all_genes.csv", + "compute_dataset_statistics", + "compute_dataset_statistics/ratio_zeros.txt", + "compute_dataset_statistics/skewness.txt", + "compute_gene_transcript_lengths", + "compute_gene_transcript_lengths/gene_transcript_lengths.csv", + "compute_stability_scores", + "compute_stability_scores/stats_with_scores.csv", + "download_ensembl_annotation", + "download_ensembl_annotation/Solanum_tuberosum.SolTub_3.0.62.gff3.gz", "errors", - "geo", + "get_candidate_genes", + "get_candidate_genes/candidate_counts.parquet", "idmapping", + "merge_all_counts", + "merge_all_counts/all_counts.parquet", + "merge_microarray_counts", + "merge_microarray_counts/all_counts.parquet", + "merge_rnaseq_counts", + "merge_rnaseq_counts/all_counts.parquet", "merged_datasets", + "merged_datasets/whole_design.csv", "multiqc", "multiqc/multiqc_data", "multiqc/multiqc_data/llms-full.txt", @@ -342,23 +451,63 @@ "multiqc/multiqc_data/multiqc.parquet", "multiqc/multiqc_data/multiqc_citations.txt", "multiqc/multiqc_data/multiqc_data.json", + "multiqc/multiqc_data/multiqc_ratio_zeros.txt", + "multiqc/multiqc_data/multiqc_skewness.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_plots", + "multiqc/multiqc_plots/pdf", + "multiqc/multiqc_plots/pdf/ratio_zeros.pdf", + "multiqc/multiqc_plots/pdf/skewness.pdf", + "multiqc/multiqc_plots/png", + "multiqc/multiqc_plots/png/ratio_zeros.png", + "multiqc/multiqc_plots/png/skewness.png", + "multiqc/multiqc_plots/svg", + "multiqc/multiqc_plots/svg/ratio_zeros.svg", + "multiqc/multiqc_plots/svg/skewness.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", + "normalised", + "normalised/microarray.normalised", + "normalised/microarray.normalised/quantile_normalised", + "normalised/microarray.normalised/quantile_normalised/microarray.normalised.quant_norm.parquet", + "normalised/rnaseq.raw", + "normalised/rnaseq.raw/quantile_normalised", + "normalised/rnaseq.raw/quantile_normalised/rnaseq.raw.tpm.quant_norm.parquet", + "normalised/rnaseq.raw/tpm", + "normalised/rnaseq.raw/tpm/rnaseq.raw.tpm.csv", + "normfinder", + "normfinder/stability_values.normfinder.csv", "pipeline_info", "pipeline_info/software_mqc_versions.yml", + "statistics", + "statistics/ratio_zeros.csv", + "statistics/skewness.csv", "warnings" ], [ - + "ratio_zeros.transposed.csv:md5,08ac547e8c31c2eefeead06c58f1fa3d", + "skewness.transposed.csv:md5,19618bb423fd67de0505f41710bff55f", + "stats_all_genes.csv:md5,c6b5b2687e436d44f1d2310e8a3f9ae5", + "microarray.stats_all_genes.csv:md5,42524c0c0c6da5a516759e4383b5d733", + "rnaseq.stats_all_genes.csv:md5,db8fb501793a807a66798c38b5415beb", + "ratio_zeros.txt:md5,97e541c27b33caea07d5d1632c2b69af", + "skewness.txt:md5,1b5f5d7422e74dccb09d7ec752da3b8f", + "gene_transcript_lengths.csv:md5,217aa7c1e227ce2f78a905138d8e5b39", + "stats_with_scores.csv:md5,6810d14306d7d6f8b8f25a20e20f9a24", + "Solanum_tuberosum.SolTub_3.0.62.gff3.gz:md5,cca99141f43d57d697f6df75de790e05", + "whole_design.csv:md5,70d6c2673e619ca52d2774fb3e368382", + "rnaseq.raw.tpm.csv:md5,3938c48a7d3e5b33de0d16db7e786c78", + "stability_values.normfinder.csv:md5,04e67e4538a9bc4d8c8a02ca573c2daf", + "ratio_zeros.csv:md5,d206e45c16e6bd13de75ea6d20bbd30d", + "skewness.csv:md5,14e2a88e24c48522b03d6cbe8f276023" ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-20T20:56:47.368093375" + "timestamp": "2025-12-04T10:19:20.611079049" }, "-profile test": { "content": [ @@ -368,12 +517,21 @@ "aggregate_results/all_genes_summary.csv", "aggregate_results/top_stable_genes_summary.csv", "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", - "collect_gene_ids", - "collect_gene_ids/all_gene_ids.txt", + "clean_gene_ids", + "clean_gene_ids/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.csv", + "clean_gene_ids/beta_vulgaris.rnaseq.raw.counts.cleaned.csv", + "collect_statistics", + "collect_statistics/ratio_zeros.transposed.csv", + "collect_statistics/skewness.transposed.csv", "compute_base_statistics", "compute_base_statistics/stats_all_genes.csv", "compute_base_statistics_for_rnaseq", "compute_base_statistics_for_rnaseq/rnaseq.stats_all_genes.csv", + "compute_dataset_statistics", + "compute_dataset_statistics/ratio_zeros.txt", + "compute_dataset_statistics/skewness.txt", + "compute_gene_transcript_lengths", + "compute_gene_transcript_lengths/gene_transcript_lengths.csv", "compute_stability_scores", "compute_stability_scores/stats_with_scores.csv", "dash_app", @@ -384,9 +542,9 @@ "dash_app/data/all_counts.parquet", "dash_app/data/all_genes_summary.csv", "dash_app/data/whole_design.csv", + "dash_app/environment.yml", "dash_app/file_system_backend", "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", - "dash_app/spec-file.txt", "dash_app/src", "dash_app/src/callbacks", "dash_app/src/callbacks/common.py", @@ -408,29 +566,24 @@ "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", "dash_app/versions.yml", + "download_ensembl_annotation", + "download_ensembl_annotation/Beta_vulgaris.RefBeet-1.2.2.62.gff3.gz", "errors", - "errors/normalisation_failure_reasons.tsv", - "expression_atlas", - "expression_atlas/accessions", - "expression_atlas/accessions/accessions.txt", - "expression_atlas/accessions/selected_experiments.metadata.tsv", - "expression_atlas/accessions/species_experiments.metadata.tsv", - "expression_atlas/datasets", - "expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", - "expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", "geo", - "geo/accessions", - "geo/accessions/accessions.txt", - "geo/accessions/geo_all_datasets.metadata.tsv", - "geo/accessions/geo_rejected_datasets.metadata.tsv", - "geo/accessions/geo_selected_datasets.metadata.tsv", - "geo/datasets", - "geo/datasets/warning_reason.txt", "get_candidate_genes", "get_candidate_genes/candidate_counts.parquet", "idmapping", - "idmapping/gene_metadata.csv", - "idmapping/mapped_gene_ids.csv", + "idmapping/collected_gene_ids", + "idmapping/collected_gene_ids/all_gene_ids.txt", + "idmapping/global_gene_id_mapping.csv", + "idmapping/global_gene_metadata.csv", + "idmapping/gprofiler", + "idmapping/gprofiler/gene_metadata.csv", + "idmapping/gprofiler/mapped_gene_ids.csv", + "idmapping/original_gene_ids.txt", + "idmapping/renamed", + "idmapping/renamed/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", + "idmapping/renamed/beta_vulgaris.rnaseq.raw.counts.cleaned.renamed.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", "merge_all_counts", @@ -448,80 +601,115 @@ "multiqc/multiqc_data/multiqc_data.json", "multiqc/multiqc_data/multiqc_eatlas_all_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", "multiqc/multiqc_data/multiqc_gene_statistics.txt", "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_geo_rejected_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_geo_selected_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_geo_warning_reasons.txt", "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", + "multiqc/multiqc_data/multiqc_ratio_zeros.txt", + "multiqc/multiqc_data/multiqc_skewness.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", "multiqc/multiqc_plots", "multiqc/multiqc_plots/pdf", "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", "multiqc/multiqc_plots/pdf/gene_statistics.pdf", "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/geo_rejected_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/geo_selected_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/geo_warning_reasons.pdf", "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", + "multiqc/multiqc_plots/pdf/ratio_zeros.pdf", + "multiqc/multiqc_plots/pdf/skewness.pdf", "multiqc/multiqc_plots/png", "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", + "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", "multiqc/multiqc_plots/png/gene_statistics.png", "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", "multiqc/multiqc_plots/png/geo_rejected_experiments_metadata.png", "multiqc/multiqc_plots/png/geo_selected_experiments_metadata.png", "multiqc/multiqc_plots/png/geo_warning_reasons.png", "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", + "multiqc/multiqc_plots/png/ratio_zeros.png", + "multiqc/multiqc_plots/png/skewness.png", "multiqc/multiqc_plots/svg", "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", "multiqc/multiqc_plots/svg/gene_statistics.svg", "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", "multiqc/multiqc_plots/svg/geo_rejected_experiments_metadata.svg", "multiqc/multiqc_plots/svg/geo_selected_experiments_metadata.svg", "multiqc/multiqc_plots/svg/geo_warning_reasons.svg", "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", + "multiqc/multiqc_plots/svg/ratio_zeros.svg", + "multiqc/multiqc_plots/svg/skewness.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", "normalised", "normalised/E_MTAB_8187_rnaseq", - "normalised/E_MTAB_8187_rnaseq/normalisation_deseq2", - "normalised/E_MTAB_8187_rnaseq/normalisation_deseq2/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normalised/E_MTAB_8187_rnaseq/quantile_normalised", + "normalised/E_MTAB_8187_rnaseq/quantile_normalised/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/E_MTAB_8187_rnaseq/tpm", + "normalised/E_MTAB_8187_rnaseq/tpm/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", "normalised/beta_vulgaris.rnaseq.raw.counts", - "normalised/beta_vulgaris.rnaseq.raw.counts/normalisation_deseq2", - "normalised/beta_vulgaris.rnaseq.raw.counts/normalisation_deseq2/failure_reason.txt", + "normalised/beta_vulgaris.rnaseq.raw.counts/quantile_normalised", + "normalised/beta_vulgaris.rnaseq.raw.counts/quantile_normalised/beta_vulgaris.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/beta_vulgaris.rnaseq.raw.counts/tpm", + "normalised/beta_vulgaris.rnaseq.raw.counts/tpm/beta_vulgaris.rnaseq.raw.counts.cleaned.renamed.tpm.csv", "normfinder", "normfinder/stability_values.normfinder.csv", "pipeline_info", "pipeline_info/software_mqc_versions.yml", - "quantile_normalised", - "quantile_normalised/E_MTAB_8187_rnaseq", - "quantile_normalised/E_MTAB_8187_rnaseq/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", - "rename_gene_ids", - "rename_gene_ids/E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv", - "rename_gene_ids/beta_vulgaris.rnaseq.raw.counts.renamed.csv", - "rename_gene_ids/warning_reason.txt", + "public_data", + "public_data/expression_atlas", + "public_data/expression_atlas/accessions", + "public_data/expression_atlas/accessions/accessions.txt", + "public_data/expression_atlas/accessions/selected_experiments.metadata.tsv", + "public_data/expression_atlas/accessions/species_experiments.metadata.tsv", + "public_data/expression_atlas/datasets", + "public_data/expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", + "public_data/expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", + "public_data/geo", + "public_data/geo/accessions", + "public_data/geo/accessions/accessions.txt", + "public_data/geo/accessions/geo_all_datasets.metadata.tsv", + "public_data/geo/accessions/geo_rejected_datasets.metadata.tsv", + "public_data/geo/accessions/geo_selected_datasets.metadata.tsv", + "public_data/geo/datasets", + "public_data/geo/datasets/warning_reason.txt", + "statistics", + "statistics/id_mapping_stats.csv", + "statistics/ratio_zeros.csv", + "statistics/skewness.csv", "warnings", - "warnings/geo_warning_reasons.csv", - "warnings/renaming_warning_reasons.tsv" + "warnings/geo_warning_reasons.csv" ], [ - "all_genes_summary.csv:md5,c9f6ecd47c480351c7a73950fccc5985", - "top_stable_genes_summary.csv:md5,f3fc83cec676b75472d30e54c5ca9b6a", - "top_stable_genes_transposed_counts_filtered.csv:md5,86b247718f957d3a9364cf1a08189dc1", - "all_gene_ids.txt:md5,1479fc03fa73fc2f11a2f25509537992", - "stats_all_genes.csv:md5,f6699c8549e7a4cb9105f021d53590e1", - "rnaseq.stats_all_genes.csv:md5,9750eb04fe96150c411aa673a8dad3e2", - "stats_with_scores.csv:md5,d24a8d1ac6c7258346b664d132cf866a", + "all_genes_summary.csv:md5,ec52af6957845539143919c0e6eec89c", + "top_stable_genes_summary.csv:md5,81d590008d5582658d307bf8c101e60a", + "top_stable_genes_transposed_counts_filtered.csv:md5,8069bd4eb5749c206912a57e35cf7357", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.csv:md5,c7f0662425535e1c04bbc58b22ba74eb", + "beta_vulgaris.rnaseq.raw.counts.cleaned.csv:md5,7fe33ef9f337b2bb66392ca78fd8d343", + "ratio_zeros.transposed.csv:md5,1cbb8d2b3f8ff38a523d3d80471dba3e", + "skewness.transposed.csv:md5,1851259185418be9d83a72bef48d7075", + "stats_all_genes.csv:md5,c9441dd7f473f50b37d7eb64ae2b6d52", + "rnaseq.stats_all_genes.csv:md5,4a46edac1a4158fb861e54bf3b34c5cd", + "ratio_zeros.txt:md5,ae64e9788a4e48e276ecdea3b33abb3f", + "skewness.txt:md5,ad6408796ac8bc51f29018c33f53cb55", + "gene_transcript_lengths.csv:md5,458c7dfd3598bdcbcb6ceb76ccba189f", + "stats_with_scores.csv:md5,6e457b48ca46c4a4965e108403346e94", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_genes_summary.csv:md5,c9f6ecd47c480351c7a73950fccc5985", - "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", + "all_genes_summary.csv:md5,ec52af6957845539143919c0e6eec89c", + "whole_design.csv:md5,3c1e14c9bd7ad250326b070a0dd4d81f", + "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", - "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", "genes.py:md5,680cb5f4e107a3b091821917d72a555c", "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", @@ -538,54 +726,49 @@ "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", "style.py:md5,b9f1207f06464e43c05e6e3912f12731", "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "normalisation_failure_reasons.tsv:md5,365a7f147247027d6346a95d80de4e9e", + "Beta_vulgaris.RefBeet-1.2.2.62.gff3.gz:md5,6f2c45809441c8776e6578000db2b0e4", + "all_gene_ids.txt:md5,be909a6a1fbbf2c66b3f646b3b24975a", + "global_gene_id_mapping.csv:md5,c664fae78b7433ded148bd94bcd23c67", + "global_gene_metadata.csv:md5,68d3a0bf612bef3f20d370012400dca5", + "gene_metadata.csv:md5,be6187aa4ae724f7e37e5a07099c40af", + "mapped_gene_ids.csv:md5,f26e6d00ff48a07fe28c056bbd6cde8c", + "original_gene_ids.txt:md5,8b21a07b1b6ac1f3d5aa6e2d40b28fc0", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,ed1600e42df05fc885a16a66b77324a1", + "beta_vulgaris.rnaseq.raw.counts.cleaned.renamed.csv:md5,d90b7475356d812ed289bc9fb3cb1acd", + "whole_gene_id_mapping.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", + "whole_gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", + "whole_design.csv:md5,3c1e14c9bd7ad250326b070a0dd4d81f", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,b407990bb10106de943f9e93cda6323e", + "beta_vulgaris.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,2c326f3e419341f955bb757fc8bf4357", + "stability_values.normfinder.csv:md5,c762f74840a6e604d43c8f727a5826f5", "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", "accessions.txt:md5,63a651d9df354aef24400cebe56dd5ec", - "geo_all_datasets.metadata.tsv:md5,83689531d044d7682413c4fa51ac4cff", + "geo_all_datasets.metadata.tsv:md5,06a371a9a891d50d40c94239f53db000", "geo_rejected_datasets.metadata.tsv:md5,0a66c9d519b4590e48b04e4c37d66416", - "geo_selected_datasets.metadata.tsv:md5,1d41ab3e480dde9aec6abd0a6270144b", + "geo_selected_datasets.metadata.tsv:md5,df6a09c2511ebb84c192b9eac3b02e02", "warning_reason.txt:md5,603e30b732b7a6b501b59adf9d0e8837", - "gene_metadata.csv:md5,3cfdc44c94bab17efb2db7600a82b921", - "mapped_gene_ids.csv:md5,3bcff962493cb088786764980de4e315", - "whole_gene_id_mapping.csv:md5,b061b542a0c3eb0ca5712ed3c4837bc4", - "whole_gene_metadata.csv:md5,3cf8bd030195e36ea10bf99a322712b1", - "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,73780e145259992a172ffa7ebdc8ddbd", - "failure_reason.txt:md5,7f26a85e66f925b9718d543c01e06c51", - "stability_values.normfinder.csv:md5,78acf60ce7914a64bb1a1b80fc223170", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.renamed.csv:md5,d3efe7dc241425b9f3c190fe36c6e595", - "beta_vulgaris.rnaseq.raw.counts.renamed.csv:md5,0a9c75be866ebd67c63a61a75bf25185", - "warning_reason.txt:md5,0042ef8923246d99e5ef2a9068caa1a8", - "geo_warning_reasons.csv:md5,b44f494b756cc0297f1d7b234bea1e13", - "renaming_warning_reasons.tsv:md5,41343bb944d9455d3c2a427196770f39" + "id_mapping_stats.csv:md5,ba2014297db7749253dae3922859ae0a", + "ratio_zeros.csv:md5,9794647ae1d7c87ec212c0c12b658d4e", + "skewness.csv:md5,386dcdb8064e85a1388aad5b72244692", + "geo_warning_reasons.csv:md5,b44f494b756cc0297f1d7b234bea1e13" ] ], "meta": { "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nextflow": "25.10.2" }, - "timestamp": "2025-11-20T19:28:36.747967413" + "timestamp": "2025-12-04T11:37:43.741998193" }, "-profile test_accessions_only": { "content": [ null, [ "errors", - "expression_atlas", - "expression_atlas/accessions", - "expression_atlas/accessions/accessions.txt", - "expression_atlas/accessions/selected_experiments.metadata.tsv", - "expression_atlas/accessions/species_experiments.metadata.tsv", "geo", - "geo/accessions", - "geo/accessions/accessions.txt", - "geo/accessions/geo_all_datasets.metadata.tsv", - "geo/accessions/geo_rejected_datasets.metadata.tsv", - "geo/accessions/geo_selected_datasets.metadata.tsv", "multiqc", "multiqc/multiqc_data", "multiqc/multiqc_data/llms-full.txt", @@ -623,6 +806,19 @@ "multiqc/versions.yml", "pipeline_info", "pipeline_info/software_mqc_versions.yml", + "public_data", + "public_data/expression_atlas", + "public_data/expression_atlas/accessions", + "public_data/expression_atlas/accessions/accessions.txt", + "public_data/expression_atlas/accessions/selected_experiments.metadata.tsv", + "public_data/expression_atlas/accessions/species_experiments.metadata.tsv", + "public_data/geo", + "public_data/geo/accessions", + "public_data/geo/accessions/accessions.txt", + "public_data/geo/accessions/geo_all_datasets.metadata.tsv", + "public_data/geo/accessions/geo_rejected_datasets.metadata.tsv", + "public_data/geo/accessions/geo_selected_datasets.metadata.tsv", + "statistics", "warnings" ], [ @@ -630,86 +826,28 @@ "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "accessions.txt:md5,a850f625a78be7b4b10ce08a5b638e23", - "geo_all_datasets.metadata.tsv:md5,e9eb980e088b46aa80aa50680f129fda", + "geo_all_datasets.metadata.tsv:md5,7fd4e7b26f1348f86e97aa16f3fd40c0", "geo_rejected_datasets.metadata.tsv:md5,4b4908f7ae40b84a1ac1bd5addfc8dd2", - "geo_selected_datasets.metadata.tsv:md5,fcbfbbaf55debe0c031d68f4139b881f" + "geo_selected_datasets.metadata.tsv:md5,62bdb8eb0e87155d149020e65d110e80" ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-20T19:30:49.791557377" + "timestamp": "2025-12-04T10:15:09.663028204" }, "-profile test_one_accession_low_gene_count": { "content": [ null, [ - "aggregate_results", - "aggregate_results/all_counts_filtered.parquet", - "aggregate_results/all_genes_summary.csv", - "aggregate_results/top_stable_genes_summary.csv", - "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", - "collect_gene_ids", - "collect_gene_ids/all_gene_ids.txt", - "compute_base_statistics", - "compute_base_statistics/stats_all_genes.csv", - "compute_base_statistics_for_rnaseq", - "compute_base_statistics_for_rnaseq/rnaseq.stats_all_genes.csv", - "compute_stability_scores", - "compute_stability_scores/stats_with_scores.csv", - "dash_app", - "dash_app/app.py", - "dash_app/assets", - "dash_app/assets/style.css", - "dash_app/data", - "dash_app/data/all_counts.parquet", - "dash_app/data/all_genes_summary.csv", - "dash_app/data/whole_design.csv", - "dash_app/file_system_backend", - "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", - "dash_app/spec-file.txt", - "dash_app/src", - "dash_app/src/callbacks", - "dash_app/src/callbacks/common.py", - "dash_app/src/callbacks/genes.py", - "dash_app/src/callbacks/samples.py", - "dash_app/src/components", - "dash_app/src/components/graphs.py", - "dash_app/src/components/icons.py", - "dash_app/src/components/right_sidebar.py", - "dash_app/src/components/settings", - "dash_app/src/components/settings/genes.py", - "dash_app/src/components/settings/samples.py", - "dash_app/src/components/stores.py", - "dash_app/src/components/tables.py", - "dash_app/src/components/tooltips.py", - "dash_app/src/components/top.py", - "dash_app/src/utils", - "dash_app/src/utils/config.py", - "dash_app/src/utils/data_management.py", - "dash_app/src/utils/style.py", - "dash_app/versions.yml", + "compute_gene_transcript_lengths", + "compute_gene_transcript_lengths/gene_transcript_lengths.csv", + "download_ensembl_annotation", + "download_ensembl_annotation/Arabidopsis_thaliana.TAIR10.62.gff3.gz", "errors", - "expression_atlas", - "expression_atlas/datasets", - "expression_atlas/datasets/E_GEOD_51720_rnaseq.design.csv", - "expression_atlas/datasets/E_GEOD_51720_rnaseq.rnaseq.raw.counts.csv", - "geo", - "geo/excluded_geo_accessions.txt", - "get_candidate_genes", - "get_candidate_genes/candidate_counts.parquet", "idmapping", - "idmapping/gene_metadata.csv", - "idmapping/mapped_gene_ids.csv", - "idmapping/whole_gene_id_mapping.csv", - "idmapping/whole_gene_metadata.csv", - "merge_all_counts", - "merge_all_counts/all_counts.parquet", - "merge_rnaseq_counts", - "merge_rnaseq_counts/all_counts.parquet", "merged_datasets", - "merged_datasets/whole_design.csv", "multiqc", "multiqc/multiqc_data", "multiqc/multiqc_data/llms-full.txt", @@ -717,106 +855,36 @@ "multiqc/multiqc_data/multiqc.parquet", "multiqc/multiqc_data/multiqc_citations.txt", "multiqc/multiqc_data/multiqc_data.json", - "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", - "multiqc/multiqc_data/multiqc_gene_statistics.txt", - "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", - "multiqc/multiqc_plots", - "multiqc/multiqc_plots/pdf", - "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", - "multiqc/multiqc_plots/pdf/gene_statistics.pdf", - "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", - "multiqc/multiqc_plots/png", - "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", - "multiqc/multiqc_plots/png/gene_statistics.png", - "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", - "multiqc/multiqc_plots/svg", - "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", - "multiqc/multiqc_plots/svg/gene_statistics.svg", - "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", - "normalised", - "normalised/E_GEOD_51720_rnaseq", - "normalised/E_GEOD_51720_rnaseq/normalisation_deseq2", - "normalised/E_GEOD_51720_rnaseq/normalisation_deseq2/E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", - "normfinder", - "normfinder/stability_values.normfinder.csv", "pipeline_info", "pipeline_info/software_mqc_versions.yml", - "quantile_normalised", - "quantile_normalised/E_GEOD_51720_rnaseq", - "quantile_normalised/E_GEOD_51720_rnaseq/E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", - "rename_gene_ids", - "rename_gene_ids/E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.csv", - "rename_gene_ids/warning_reason.txt", - "warnings", - "warnings/renaming_warning_reasons.tsv" + "statistics", + "warnings" ], [ - "all_genes_summary.csv:md5,56140489d6741194ad888f7eeb9e4658", - "top_stable_genes_summary.csv:md5,f06095d5043b6fce60841db00042311f", - "top_stable_genes_transposed_counts_filtered.csv:md5,9a73aa6c3b36b5eb6b63a6471d44e60e", - "all_gene_ids.txt:md5,62f966ba0a48f15c932dc269dc545300", - "stats_all_genes.csv:md5,292cfda45c3cdedb463f45e54b1be0fd", - "rnaseq.stats_all_genes.csv:md5,be2fee6b119c81a5916b50009317b011", - "stats_with_scores.csv:md5,f80f189d8b50173a40c9a234eae3bb90", - "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", - "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_genes_summary.csv:md5,56140489d6741194ad888f7eeb9e4658", - "whole_design.csv:md5,d3aa542c4ad07d0051a84482fe6cd81c", - "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", - "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", - "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", - "genes.py:md5,680cb5f4e107a3b091821917d72a555c", - "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", - "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", - "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", - "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", - "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", - "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", - "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", - "tables.py:md5,92e8cc03fd8c4192af9201a7ff3cd979", - "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", - "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.py:md5,b629adc64e497622f000945ee6e6aed2", - "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", - "style.py:md5,b9f1207f06464e43c05e6e3912f12731", - "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "E_GEOD_51720_rnaseq.design.csv:md5,80805afb29837b6fbb73a6aa6f3a461b", - "E_GEOD_51720_rnaseq.rnaseq.raw.counts.csv:md5,07cd448196fc2fea4663bd9705da2b98", - "excluded_geo_accessions.txt:md5,cdbec776e8b1d9dc7d0aa44aaf52aa50", - "gene_metadata.csv:md5,f641a146dc7f8faa4597e9dc67b20506", - "mapped_gene_ids.csv:md5,5dfac9c67e6bb8613f319cce464b3be8", - "whole_gene_id_mapping.csv:md5,f8933b4d441cdafc92b4bcee1de21bc5", - "whole_gene_metadata.csv:md5,4811b825f6fdc4b7fee3d3c8354de5d3", - "whole_design.csv:md5,d3aa542c4ad07d0051a84482fe6cd81c", - "E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,c8da94c002d3ee224db4f2764d615ed4", - "stability_values.normfinder.csv:md5,96cba2aa1960465d3ce43c4a54e21956", - "E_GEOD_51720_rnaseq.rnaseq.raw.counts.renamed.csv:md5,77d527c8a6acc65764107b438ffc6613", - "warning_reason.txt:md5,44180971030c5f396e79e8df28118231", - "renaming_warning_reasons.tsv:md5,dd423ff5f00af7273f44ba20e3ae5b5c" + "gene_transcript_lengths.csv:md5,06b4612031f4f300a6d67f36e7625492", + "Arabidopsis_thaliana.TAIR10.62.gff3.gz:md5,b02566c301d47461db70747b3adaa6ce" ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-20T19:35:25.802480432" + "timestamp": "2025-12-03T18:39:25.158526321" }, "-profile test_no_dataset_found": { "content": [ null, [ + "compute_gene_transcript_lengths", + "compute_gene_transcript_lengths/gene_transcript_lengths.csv", + "download_ensembl_annotation", + "download_ensembl_annotation/Marmota_marmota_marmota.marMar2.1.115.gff3.gz", "errors", - "expression_atlas", - "expression_atlas/accessions", - "expression_atlas/accessions/accessions.txt", - "expression_atlas/accessions/species_experiments.metadata.tsv", "geo", - "geo/accessions", - "geo/accessions/accessions.txt", "idmapping", "merged_datasets", "multiqc", @@ -832,9 +900,20 @@ "multiqc/versions.yml", "pipeline_info", "pipeline_info/software_mqc_versions.yml", + "public_data", + "public_data/expression_atlas", + "public_data/expression_atlas/accessions", + "public_data/expression_atlas/accessions/accessions.txt", + "public_data/expression_atlas/accessions/species_experiments.metadata.tsv", + "public_data/geo", + "public_data/geo/accessions", + "public_data/geo/accessions/accessions.txt", + "statistics", "warnings" ], [ + "gene_transcript_lengths.csv:md5,d03318b6a34355e8897e9d43f02c8deb", + "Marmota_marmota_marmota.marMar2.1.115.gff3.gz:md5,f67b6ec1b7de4c7fa606183982a3704b", "accessions.txt:md5,d41d8cd98f00b204e9800998ecf8427e", "species_experiments.metadata.tsv:md5,68b329da9893e34099c7d8ad5cb9c940", "accessions.txt:md5,d41d8cd98f00b204e9800998ecf8427e" @@ -844,31 +923,14 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-19T15:17:16.594642518" + "timestamp": "2025-12-03T18:43:26.049573567" }, "-profile test_download_only": { "content": [ null, [ "errors", - "expression_atlas", - "expression_atlas/accessions", - "expression_atlas/accessions/accessions.txt", - "expression_atlas/accessions/selected_experiments.metadata.tsv", - "expression_atlas/accessions/species_experiments.metadata.tsv", - "expression_atlas/datasets", - "expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", - "expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", "geo", - "geo/accessions", - "geo/accessions/accessions.txt", - "geo/accessions/geo_all_datasets.metadata.tsv", - "geo/accessions/geo_rejected_datasets.metadata.tsv", - "geo/accessions/geo_selected_datasets.metadata.tsv", - "geo/datasets", - "geo/datasets/GSE55951_GPL18429.microarray.normalised.counts.csv", - "geo/datasets/GSE55951_GPL18429.microarray.normalised.design.csv", - "geo/datasets/warning_reason.txt", "multiqc", "multiqc/multiqc_data", "multiqc/multiqc_data/llms-full.txt", @@ -910,6 +972,26 @@ "multiqc/versions.yml", "pipeline_info", "pipeline_info/software_mqc_versions.yml", + "public_data", + "public_data/expression_atlas", + "public_data/expression_atlas/accessions", + "public_data/expression_atlas/accessions/accessions.txt", + "public_data/expression_atlas/accessions/selected_experiments.metadata.tsv", + "public_data/expression_atlas/accessions/species_experiments.metadata.tsv", + "public_data/expression_atlas/datasets", + "public_data/expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", + "public_data/expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", + "public_data/geo", + "public_data/geo/accessions", + "public_data/geo/accessions/accessions.txt", + "public_data/geo/accessions/geo_all_datasets.metadata.tsv", + "public_data/geo/accessions/geo_rejected_datasets.metadata.tsv", + "public_data/geo/accessions/geo_selected_datasets.metadata.tsv", + "public_data/geo/datasets", + "public_data/geo/datasets/GSE55951_GPL18429.microarray.normalised.counts.csv", + "public_data/geo/datasets/GSE55951_GPL18429.microarray.normalised.design.csv", + "public_data/geo/datasets/warning_reason.txt", + "statistics", "warnings", "warnings/geo_warning_reasons.csv" ], @@ -920,9 +1002,9 @@ "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", "accessions.txt:md5,a850f625a78be7b4b10ce08a5b638e23", - "geo_all_datasets.metadata.tsv:md5,e9eb980e088b46aa80aa50680f129fda", + "geo_all_datasets.metadata.tsv:md5,7fd4e7b26f1348f86e97aa16f3fd40c0", "geo_rejected_datasets.metadata.tsv:md5,4b4908f7ae40b84a1ac1bd5addfc8dd2", - "geo_selected_datasets.metadata.tsv:md5,fcbfbbaf55debe0c031d68f4139b881f", + "geo_selected_datasets.metadata.tsv:md5,62bdb8eb0e87155d149020e65d110e80", "GSE55951_GPL18429.microarray.normalised.counts.csv:md5,18fd2d728ad2ec5cb78f994f73375144", "GSE55951_GPL18429.microarray.normalised.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d", "warning_reason.txt:md5,603e30b732b7a6b501b59adf9d0e8837", @@ -933,7 +1015,275 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-20T19:32:11.160027332" + "timestamp": "2025-12-04T10:16:23.021095391" + }, + "-profile test_gprofiler_target_database_entrez": { + "content": [ + [ + "aggregate_results", + "aggregate_results/all_counts_filtered.parquet", + "aggregate_results/all_genes_summary.csv", + "aggregate_results/top_stable_genes_summary.csv", + "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", + "clean_gene_ids", + "clean_gene_ids/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.csv", + "clean_gene_ids/GSE55951_GPL18429.microarray.normalised.counts.cleaned.csv", + "collect_statistics", + "collect_statistics/ratio_zeros.transposed.csv", + "collect_statistics/skewness.transposed.csv", + "compute_base_statistics", + "compute_base_statistics/stats_all_genes.csv", + "compute_base_statistics_for_microarray", + "compute_base_statistics_for_microarray/microarray.stats_all_genes.csv", + "compute_base_statistics_for_rnaseq", + "compute_base_statistics_for_rnaseq/rnaseq.stats_all_genes.csv", + "compute_dataset_statistics", + "compute_dataset_statistics/ratio_zeros.txt", + "compute_dataset_statistics/skewness.txt", + "compute_gene_transcript_lengths", + "compute_gene_transcript_lengths/gene_transcript_lengths.csv", + "compute_stability_scores", + "compute_stability_scores/stats_with_scores.csv", + "dash_app", + "dash_app/app.py", + "dash_app/assets", + "dash_app/assets/style.css", + "dash_app/data", + "dash_app/data/all_counts.parquet", + "dash_app/data/all_genes_summary.csv", + "dash_app/data/whole_design.csv", + "dash_app/environment.yml", + "dash_app/file_system_backend", + "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", + "dash_app/src", + "dash_app/src/callbacks", + "dash_app/src/callbacks/common.py", + "dash_app/src/callbacks/genes.py", + "dash_app/src/callbacks/samples.py", + "dash_app/src/components", + "dash_app/src/components/graphs.py", + "dash_app/src/components/icons.py", + "dash_app/src/components/right_sidebar.py", + "dash_app/src/components/settings", + "dash_app/src/components/settings/genes.py", + "dash_app/src/components/settings/samples.py", + "dash_app/src/components/stores.py", + "dash_app/src/components/tables.py", + "dash_app/src/components/tooltips.py", + "dash_app/src/components/top.py", + "dash_app/src/utils", + "dash_app/src/utils/config.py", + "dash_app/src/utils/data_management.py", + "dash_app/src/utils/style.py", + "dash_app/versions.yml", + "download_ensembl_annotation", + "download_ensembl_annotation/Beta_vulgaris.RefBeet-1.2.2.62.gff3.gz", + "errors", + "geo", + "get_candidate_genes", + "get_candidate_genes/candidate_counts.parquet", + "idmapping", + "idmapping/collected_gene_ids", + "idmapping/collected_gene_ids/all_gene_ids.txt", + "idmapping/global_gene_id_mapping.csv", + "idmapping/global_gene_metadata.csv", + "idmapping/gprofiler", + "idmapping/gprofiler/gene_metadata.csv", + "idmapping/gprofiler/mapped_gene_ids.csv", + "idmapping/original_gene_ids.txt", + "idmapping/renamed", + "idmapping/renamed/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", + "idmapping/renamed/GSE55951_GPL18429.microarray.normalised.counts.cleaned.renamed.csv", + "idmapping/renamed/warning_reason.txt", + "idmapping/whole_gene_id_mapping.csv", + "idmapping/whole_gene_metadata.csv", + "merge_all_counts", + "merge_all_counts/all_counts.parquet", + "merge_microarray_counts", + "merge_microarray_counts/all_counts.parquet", + "merge_rnaseq_counts", + "merge_rnaseq_counts/all_counts.parquet", + "merged_datasets", + "merged_datasets/whole_design.csv", + "multiqc", + "multiqc/multiqc_data", + "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_eatlas_all_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", + "multiqc/multiqc_data/multiqc_gene_statistics.txt", + "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_rejected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_selected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_geo_warning_reasons.txt", + "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", + "multiqc/multiqc_data/multiqc_ratio_zeros.txt", + "multiqc/multiqc_data/multiqc_renaming_warning_reasons.txt", + "multiqc/multiqc_data/multiqc_skewness.txt", + "multiqc/multiqc_data/multiqc_software_versions.txt", + "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_plots", + "multiqc/multiqc_plots/pdf", + "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", + "multiqc/multiqc_plots/pdf/gene_statistics.pdf", + "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_rejected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_selected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/geo_warning_reasons.pdf", + "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", + "multiqc/multiqc_plots/pdf/ratio_zeros.pdf", + "multiqc/multiqc_plots/pdf/renaming_warning_reasons.pdf", + "multiqc/multiqc_plots/pdf/skewness.pdf", + "multiqc/multiqc_plots/png", + "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", + "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", + "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", + "multiqc/multiqc_plots/png/gene_statistics.png", + "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_rejected_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_selected_experiments_metadata.png", + "multiqc/multiqc_plots/png/geo_warning_reasons.png", + "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", + "multiqc/multiqc_plots/png/ratio_zeros.png", + "multiqc/multiqc_plots/png/renaming_warning_reasons.png", + "multiqc/multiqc_plots/png/skewness.png", + "multiqc/multiqc_plots/svg", + "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", + "multiqc/multiqc_plots/svg/gene_statistics.svg", + "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_rejected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_selected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/geo_warning_reasons.svg", + "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", + "multiqc/multiqc_plots/svg/ratio_zeros.svg", + "multiqc/multiqc_plots/svg/renaming_warning_reasons.svg", + "multiqc/multiqc_plots/svg/skewness.svg", + "multiqc/multiqc_report.html", + "multiqc/versions.yml", + "normalised", + "normalised/E_MTAB_8187_rnaseq", + "normalised/E_MTAB_8187_rnaseq/quantile_normalised", + "normalised/E_MTAB_8187_rnaseq/quantile_normalised/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/E_MTAB_8187_rnaseq/tpm", + "normalised/E_MTAB_8187_rnaseq/tpm/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", + "normalised/GSE55951_GPL18429", + "normalised/GSE55951_GPL18429/quantile_normalised", + "normalised/GSE55951_GPL18429/quantile_normalised/GSE55951_GPL18429.microarray.normalised.counts.cleaned.renamed.quant_norm.parquet", + "normfinder", + "normfinder/stability_values.normfinder.csv", + "pipeline_info", + "pipeline_info/software_mqc_versions.yml", + "public_data", + "public_data/expression_atlas", + "public_data/expression_atlas/accessions", + "public_data/expression_atlas/accessions/accessions.txt", + "public_data/expression_atlas/accessions/selected_experiments.metadata.tsv", + "public_data/expression_atlas/accessions/species_experiments.metadata.tsv", + "public_data/expression_atlas/datasets", + "public_data/expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", + "public_data/expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", + "public_data/geo", + "public_data/geo/accessions", + "public_data/geo/accessions/accessions.txt", + "public_data/geo/accessions/geo_all_datasets.metadata.tsv", + "public_data/geo/accessions/geo_rejected_datasets.metadata.tsv", + "public_data/geo/accessions/geo_selected_datasets.metadata.tsv", + "public_data/geo/datasets", + "public_data/geo/datasets/GSE55951_GPL18429.microarray.normalised.counts.csv", + "public_data/geo/datasets/GSE55951_GPL18429.microarray.normalised.design.csv", + "public_data/geo/datasets/warning_reason.txt", + "statistics", + "statistics/id_mapping_stats.csv", + "statistics/ratio_zeros.csv", + "statistics/skewness.csv", + "warnings", + "warnings/geo_warning_reasons.csv", + "warnings/renaming_warning_reasons.tsv" + ], + [ + "all_genes_summary.csv:md5,534ab07b53769ccac9ef1e6e11208930", + "top_stable_genes_summary.csv:md5,d479f790ffce5fcf655075848b942469", + "top_stable_genes_transposed_counts_filtered.csv:md5,a7a651bac95ed27c24e29175312cb507", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.csv:md5,c7f0662425535e1c04bbc58b22ba74eb", + "GSE55951_GPL18429.microarray.normalised.counts.cleaned.csv:md5,5d3bbad7d8b0bf824f883d2e84a229b3", + "ratio_zeros.transposed.csv:md5,4d9cccc1eb3f96b5b8489df2f2adf4d1", + "skewness.transposed.csv:md5,df6ef6363c95095e1dffae4b83358e8f", + "stats_all_genes.csv:md5,de1075167d01aa5e411185662157b70b", + "microarray.stats_all_genes.csv:md5,e686dce74f7006c5af95e5fb595c0902", + "rnaseq.stats_all_genes.csv:md5,db157ff5e8c39681d82b9c75a36d3c75", + "ratio_zeros.txt:md5,ae64e9788a4e48e276ecdea3b33abb3f", + "skewness.txt:md5,ad6408796ac8bc51f29018c33f53cb55", + "gene_transcript_lengths.csv:md5,458c7dfd3598bdcbcb6ceb76ccba189f", + "stats_with_scores.csv:md5,6b8eefabd6992ed69d1b633e9f64c7b8", + "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", + "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", + "all_genes_summary.csv:md5,534ab07b53769ccac9ef1e6e11208930", + "whole_design.csv:md5,f2723ea20ce675a7027a4b4285df7a96", + "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", + "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", + "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", + "genes.py:md5,680cb5f4e107a3b091821917d72a555c", + "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", + "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", + "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", + "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", + "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", + "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", + "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", + "tables.py:md5,92e8cc03fd8c4192af9201a7ff3cd979", + "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", + "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", + "config.py:md5,b629adc64e497622f000945ee6e6aed2", + "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", + "style.py:md5,b9f1207f06464e43c05e6e3912f12731", + "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", + "Beta_vulgaris.RefBeet-1.2.2.62.gff3.gz:md5,6f2c45809441c8776e6578000db2b0e4", + "all_gene_ids.txt:md5,bfbaf9423c4b18047a2cc43bc8a81436", + "global_gene_id_mapping.csv:md5,f07bb1847f253d252208e1a0c8dfa372", + "global_gene_metadata.csv:md5,68d3a0bf612bef3f20d370012400dca5", + "gene_metadata.csv:md5,995a94e8b5b5fe870a1e8297f281d02f", + "mapped_gene_ids.csv:md5,03cff6ad2356752016c7467008f22cca", + "original_gene_ids.txt:md5,b1459baf97ad5857115c5b9dced18417", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,ed1600e42df05fc885a16a66b77324a1", + "GSE55951_GPL18429.microarray.normalised.counts.cleaned.renamed.csv:md5,b0780db1cbdd32da955dae3375750713", + "warning_reason.txt:md5,314a4ad5c5016c98cb3adb16fd069234", + "whole_gene_id_mapping.csv:md5,63f67fb73898870c360293d30362bc33", + "whole_gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", + "whole_design.csv:md5,f2723ea20ce675a7027a4b4285df7a96", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,b407990bb10106de943f9e93cda6323e", + "stability_values.normfinder.csv:md5,47c70a4e517d88da6402b79534e54767", + "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", + "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", + "accessions.txt:md5,a850f625a78be7b4b10ce08a5b638e23", + "geo_all_datasets.metadata.tsv:md5,7fd4e7b26f1348f86e97aa16f3fd40c0", + "geo_rejected_datasets.metadata.tsv:md5,4b4908f7ae40b84a1ac1bd5addfc8dd2", + "geo_selected_datasets.metadata.tsv:md5,62bdb8eb0e87155d149020e65d110e80", + "GSE55951_GPL18429.microarray.normalised.counts.csv:md5,18fd2d728ad2ec5cb78f994f73375144", + "GSE55951_GPL18429.microarray.normalised.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d", + "warning_reason.txt:md5,603e30b732b7a6b501b59adf9d0e8837", + "id_mapping_stats.csv:md5,a433448030d9dfe86c57f634752cebe6", + "ratio_zeros.csv:md5,47c251137e39613a86f53a9ce7fec780", + "skewness.csv:md5,e3551c3c495314b4e81bd0d9838527db", + "geo_warning_reasons.csv:md5,e08541568733e7eac853f73480679e15", + "renaming_warning_reasons.tsv:md5,ae651ff0a559e025e014412009eac136" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.10.2" + }, + "timestamp": "2025-12-04T11:48:31.290055961" }, "-profile test_full": { "content": [ @@ -944,12 +1294,20 @@ "aggregate_results/all_genes_summary.csv", "aggregate_results/top_stable_genes_summary.csv", "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", - "collect_gene_ids", - "collect_gene_ids/all_gene_ids.txt", + "clean_gene_ids", + "clean_gene_ids/E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.csv", + "collect_statistics", + "collect_statistics/ratio_zeros.transposed.csv", + "collect_statistics/skewness.transposed.csv", "compute_base_statistics", "compute_base_statistics/stats_all_genes.csv", "compute_base_statistics_for_rnaseq", "compute_base_statistics_for_rnaseq/rnaseq.stats_all_genes.csv", + "compute_dataset_statistics", + "compute_dataset_statistics/ratio_zeros.txt", + "compute_dataset_statistics/skewness.txt", + "compute_gene_transcript_lengths", + "compute_gene_transcript_lengths/gene_transcript_lengths.csv", "compute_m_measure", "compute_m_measure/m_measures.csv", "compute_stability_scores", @@ -1116,9 +1474,9 @@ "dash_app/data/all_counts.parquet", "dash_app/data/all_genes_summary.csv", "dash_app/data/whole_design.csv", + "dash_app/environment.yml", "dash_app/file_system_backend", "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", - "dash_app/spec-file.txt", "dash_app/src", "dash_app/src/callbacks", "dash_app/src/callbacks/common.py", @@ -1140,15 +1498,9 @@ "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", "dash_app/versions.yml", + "download_ensembl_annotation", + "download_ensembl_annotation/Arabidopsis_lyrata.v.1.0.62.gff3.gz", "errors", - "expression_atlas", - "expression_atlas/accessions", - "expression_atlas/accessions/accessions.txt", - "expression_atlas/accessions/selected_experiments.metadata.tsv", - "expression_atlas/accessions/species_experiments.metadata.tsv", - "expression_atlas/datasets", - "expression_atlas/datasets/E_MTAB_5072_rnaseq.design.csv", - "expression_atlas/datasets/E_MTAB_5072_rnaseq.rnaseq.raw.counts.csv", "expression_ratio", "expression_ratio/ratios.0.0.parquet", "expression_ratio/ratios.0.1.parquet", @@ -1304,18 +1656,19 @@ "expression_ratio/ratios.8.9.parquet", "expression_ratio/ratios.9.9.parquet", "geo", - "geo/accessions", - "geo/accessions/accessions.txt", - "geo/accessions/geo_all_datasets.metadata.tsv", - "geo/accessions/geo_rejected_datasets.metadata.tsv", - "geo/accessions/geo_selected_datasets.metadata.tsv", - "geo/datasets", - "geo/datasets/warning_reason.txt", "get_candidate_genes", "get_candidate_genes/candidate_counts.parquet", "idmapping", - "idmapping/gene_metadata.csv", - "idmapping/mapped_gene_ids.csv", + "idmapping/collected_gene_ids", + "idmapping/collected_gene_ids/all_gene_ids.txt", + "idmapping/global_gene_id_mapping.csv", + "idmapping/global_gene_metadata.csv", + "idmapping/gprofiler", + "idmapping/gprofiler/gene_metadata.csv", + "idmapping/gprofiler/mapped_gene_ids.csv", + "idmapping/original_gene_ids.txt", + "idmapping/renamed", + "idmapping/renamed/E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", "make_chunks", @@ -1358,6 +1711,8 @@ "multiqc/multiqc_data/multiqc_geo_selected_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_geo_warning_reasons.txt", "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", + "multiqc/multiqc_data/multiqc_ratio_zeros.txt", + "multiqc/multiqc_data/multiqc_skewness.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", "multiqc/multiqc_plots", @@ -1371,6 +1726,8 @@ "multiqc/multiqc_plots/pdf/geo_selected_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/geo_warning_reasons.pdf", "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", + "multiqc/multiqc_plots/pdf/ratio_zeros.pdf", + "multiqc/multiqc_plots/pdf/skewness.pdf", "multiqc/multiqc_plots/png", "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", @@ -1381,6 +1738,8 @@ "multiqc/multiqc_plots/png/geo_selected_experiments_metadata.png", "multiqc/multiqc_plots/png/geo_warning_reasons.png", "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", + "multiqc/multiqc_plots/png/ratio_zeros.png", + "multiqc/multiqc_plots/png/skewness.png", "multiqc/multiqc_plots/svg", "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", @@ -1391,19 +1750,37 @@ "multiqc/multiqc_plots/svg/geo_selected_experiments_metadata.svg", "multiqc/multiqc_plots/svg/geo_warning_reasons.svg", "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", + "multiqc/multiqc_plots/svg/ratio_zeros.svg", + "multiqc/multiqc_plots/svg/skewness.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", "normalised", "normalised/E_MTAB_5072_rnaseq", - "normalised/E_MTAB_5072_rnaseq/normalisation_deseq2", - "normalised/E_MTAB_5072_rnaseq/normalisation_deseq2/E_MTAB_5072_rnaseq.rnaseq.raw.counts.renamed.cpm.csv", + "normalised/E_MTAB_5072_rnaseq/quantile_normalised", + "normalised/E_MTAB_5072_rnaseq/quantile_normalised/E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/E_MTAB_5072_rnaseq/tpm", + "normalised/E_MTAB_5072_rnaseq/tpm/E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", "normfinder", "normfinder/stability_values.normfinder.csv", "pipeline_info", "pipeline_info/software_mqc_versions.yml", - "quantile_normalised", - "quantile_normalised/E_MTAB_5072_rnaseq", - "quantile_normalised/E_MTAB_5072_rnaseq/E_MTAB_5072_rnaseq.rnaseq.raw.counts.renamed.cpm.quant_norm.parquet", + "public_data", + "public_data/expression_atlas", + "public_data/expression_atlas/accessions", + "public_data/expression_atlas/accessions/accessions.txt", + "public_data/expression_atlas/accessions/selected_experiments.metadata.tsv", + "public_data/expression_atlas/accessions/species_experiments.metadata.tsv", + "public_data/expression_atlas/datasets", + "public_data/expression_atlas/datasets/E_MTAB_5072_rnaseq.design.csv", + "public_data/expression_atlas/datasets/E_MTAB_5072_rnaseq.rnaseq.raw.counts.csv", + "public_data/geo", + "public_data/geo/accessions", + "public_data/geo/accessions/accessions.txt", + "public_data/geo/accessions/geo_all_datasets.metadata.tsv", + "public_data/geo/accessions/geo_rejected_datasets.metadata.tsv", + "public_data/geo/accessions/geo_selected_datasets.metadata.tsv", + "public_data/geo/datasets", + "public_data/geo/datasets/warning_reason.txt", "ratio_standard_variation", "ratio_standard_variation/std.0.0.parquet", "ratio_standard_variation/std.0.1.parquet", @@ -1558,28 +1935,33 @@ "ratio_standard_variation/std.8.8.parquet", "ratio_standard_variation/std.8.9.parquet", "ratio_standard_variation/std.9.9.parquet", - "rename_gene_ids", - "rename_gene_ids/E_MTAB_5072_rnaseq.rnaseq.raw.counts.renamed.csv", - "rename_gene_ids/warning_reason.txt", + "statistics", + "statistics/id_mapping_stats.csv", + "statistics/ratio_zeros.csv", + "statistics/skewness.csv", "warnings", - "warnings/geo_warning_reasons.csv", - "warnings/renaming_warning_reasons.tsv" + "warnings/geo_warning_reasons.csv" ], [ - "all_genes_summary.csv:md5,fe294b66b7a19069e9c2ec11b74935de", - "top_stable_genes_summary.csv:md5,057ca1fc76aa5e99275ecf6228794991", - "top_stable_genes_transposed_counts_filtered.csv:md5,b50d420084bc0a93a978f0a75a4b5ba7", - "all_gene_ids.txt:md5,22625a4db6439656f16c65db8fb3a884", - "stats_all_genes.csv:md5,67dfa7ff8906d28113e7631b3806d208", - "rnaseq.stats_all_genes.csv:md5,707b8461f08de27e19b097c45c064af0", - "m_measures.csv:md5,e40a66b35178d8e64678c64afa72a2e1", - "stats_with_scores.csv:md5,52eef1966f7c03ecc42cd38561bfb9f6", + "all_genes_summary.csv:md5,d4b5d36061b8fd31ba27edc55f738a01", + "top_stable_genes_summary.csv:md5,b47891fe0414abd0a4b7c9137d53fc1d", + "top_stable_genes_transposed_counts_filtered.csv:md5,46d96c944cc9ca3dd83e16da52c528f2", + "E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.csv:md5,d0f6092dc2e4b4f8ecb4f594ec3ce953", + "ratio_zeros.transposed.csv:md5,ba08c111e0148809a0807130a9489220", + "skewness.transposed.csv:md5,6adfce705b65fc2aa878759bb491b0d4", + "stats_all_genes.csv:md5,4d4f56b0b517e95d20cc39e4c3c124e9", + "rnaseq.stats_all_genes.csv:md5,e0562faf910765c3e99589f9bdd26fd3", + "ratio_zeros.txt:md5,2c1811ac25e8cbb3d5bdd7f1882522da", + "skewness.txt:md5,e8d0870cf3f35d0bab5c5d73c3b4a818", + "gene_transcript_lengths.csv:md5,d5dbdbab0b6306896988ed8accec67af", + "m_measures.csv:md5,22cadc22042ae601b11e5f6ba81893d9", + "stats_with_scores.csv:md5,28f7d769cbcc1164da56cf009091171d", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_genes_summary.csv:md5,fe294b66b7a19069e9c2ec11b74935de", + "all_genes_summary.csv:md5,d4b5d36061b8fd31ba27edc55f738a01", "whole_design.csv:md5,c9bfd7bc8ca365222e03e67eb24b9a76", + "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", - "spec-file.txt:md5,dd7834fcfbd886febd8dc715ef0d6ed7", "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", "genes.py:md5,680cb5f4e107a3b091821917d72a555c", "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", @@ -1596,47 +1978,119 @@ "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", "style.py:md5,b9f1207f06464e43c05e6e3912f12731", "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", + "Arabidopsis_lyrata.v.1.0.62.gff3.gz:md5,35e546e88c7cd204870b18e888e17dae", + "all_gene_ids.txt:md5,ec39408cbfb18e25b282202122718367", + "global_gene_id_mapping.csv:md5,0271a33d981a74c62590ea3ef17dea9d", + "global_gene_metadata.csv:md5,20009019168849adeb1c1ba984a78c1e", + "gene_metadata.csv:md5,7e16813046376d2425ad2f36f601c72a", + "mapped_gene_ids.csv:md5,8e1393b156f79c2874960dc13afef854", + "original_gene_ids.txt:md5,566c596744746d6f3dd2d0552ea2ffc4", + "E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,6244da0761b4437a6de5cff49e4d2687", + "whole_gene_id_mapping.csv:md5,efc95a8e276be1eb0af9639f72e48145", + "whole_gene_metadata.csv:md5,1d342577587bc48c1eff077a594929fa", + "whole_design.csv:md5,c9bfd7bc8ca365222e03e67eb24b9a76", + "E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,4a2aa87d0bd2990c13e088f0117cc682", + "stability_values.normfinder.csv:md5,4dbd2449b2919edc7dd80f1876e2abf6", "accessions.txt:md5,561a967c16b2ef6c29fb643cd4002947", "selected_experiments.metadata.tsv:md5,385b4d7ed40829f1e764857e805d6dc4", "species_experiments.metadata.tsv:md5,385b4d7ed40829f1e764857e805d6dc4", "E_MTAB_5072_rnaseq.design.csv:md5,a1f33d126dde387a2d542381c44bc1f3", "E_MTAB_5072_rnaseq.rnaseq.raw.counts.csv:md5,c41c84899a380515d759b99eeccfe43e", "accessions.txt:md5,48a6870e7e7e7d481e9b28002e68880e", - "geo_all_datasets.metadata.tsv:md5,55ab279dea14a2d364bb86a1ca66a809", - "geo_rejected_datasets.metadata.tsv:md5,02202ad9c3d49a03dbb73c64035a8bbc", - "geo_selected_datasets.metadata.tsv:md5,e1eb953da65d90fc28a778cb17942c33", + "geo_all_datasets.metadata.tsv:md5,4a775b1e2045ddc3cf87d30173fcf86b", + "geo_rejected_datasets.metadata.tsv:md5,68ced9fbcab8af30a81cc5e23c68c179", + "geo_selected_datasets.metadata.tsv:md5,96ec5af87cd9ee034726b43ed8757d01", "warning_reason.txt:md5,46bd94872631702e89a304c0adb7a8c1", - "gene_metadata.csv:md5,03c8b32f0825b10cc339a5c10c784849", - "mapped_gene_ids.csv:md5,5d0b67f5bc2f925567819f3a14969c62", - "whole_gene_id_mapping.csv:md5,564c998be28353132cc83c33698e3c14", - "whole_gene_metadata.csv:md5,6498a7b61a11e805dc4d2f6c35a38219", - "whole_design.csv:md5,c9bfd7bc8ca365222e03e67eb24b9a76", - "E_MTAB_5072_rnaseq.rnaseq.raw.counts.renamed.cpm.csv:md5,7e67733fed4cba812bd77b2fc9365d06", - "stability_values.normfinder.csv:md5,7c1c3a2bd591c8b6b19db49284880d75", - "E_MTAB_5072_rnaseq.rnaseq.raw.counts.renamed.csv:md5,830ad1daaeaf7c8153b4bd53ad484037", - "warning_reason.txt:md5,ffc358470d35651bee1c82f1d474277d", - "geo_warning_reasons.csv:md5,b8e73aabeebad5fcc89f58badae7abd8", - "renaming_warning_reasons.tsv:md5,71b625eda4916cec330ac03cc9447484" + "id_mapping_stats.csv:md5,f0d869ee4b9896f37be649c371ffb747", + "ratio_zeros.csv:md5,15eb210c4ffff8a826e0c9f45d0ab4bd", + "skewness.csv:md5,6dbe4934961471c31fc6a1671577a8fc", + "geo_warning_reasons.csv:md5,06fc59caef4088ee67a9c27c3b5a2574" ] ], "meta": { "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nextflow": "25.10.2" }, - "timestamp": "2025-11-20T19:43:21.917886018" + "timestamp": "2025-12-04T11:53:54.330900136" }, "-profile test_dataset_custom_mapping": { "content": [ null, [ + "aggregate_results", + "aggregate_results/all_counts_filtered.parquet", + "aggregate_results/all_genes_summary.csv", + "aggregate_results/top_stable_genes_summary.csv", + "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", + "collect_statistics", + "collect_statistics/ratio_zeros.transposed.csv", + "collect_statistics/skewness.transposed.csv", + "compute_base_statistics", + "compute_base_statistics/stats_all_genes.csv", + "compute_base_statistics_for_microarray", + "compute_base_statistics_for_microarray/microarray.stats_all_genes.csv", + "compute_base_statistics_for_rnaseq", + "compute_base_statistics_for_rnaseq/rnaseq.stats_all_genes.csv", + "compute_dataset_statistics", + "compute_dataset_statistics/ratio_zeros.txt", + "compute_dataset_statistics/skewness.txt", + "compute_gene_transcript_lengths", + "compute_gene_transcript_lengths/gene_transcript_lengths.csv", + "compute_stability_scores", + "compute_stability_scores/stats_with_scores.csv", + "dash_app", + "dash_app/app.py", + "dash_app/assets", + "dash_app/assets/style.css", + "dash_app/data", + "dash_app/data/all_counts.parquet", + "dash_app/data/all_genes_summary.csv", + "dash_app/data/whole_design.csv", + "dash_app/environment.yml", + "dash_app/file_system_backend", + "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", + "dash_app/src", + "dash_app/src/callbacks", + "dash_app/src/callbacks/common.py", + "dash_app/src/callbacks/genes.py", + "dash_app/src/callbacks/samples.py", + "dash_app/src/components", + "dash_app/src/components/graphs.py", + "dash_app/src/components/icons.py", + "dash_app/src/components/right_sidebar.py", + "dash_app/src/components/settings", + "dash_app/src/components/settings/genes.py", + "dash_app/src/components/settings/samples.py", + "dash_app/src/components/stores.py", + "dash_app/src/components/tables.py", + "dash_app/src/components/tooltips.py", + "dash_app/src/components/top.py", + "dash_app/src/utils", + "dash_app/src/utils/config.py", + "dash_app/src/utils/data_management.py", + "dash_app/src/utils/style.py", + "dash_app/versions.yml", + "download_ensembl_annotation", + "download_ensembl_annotation/Solanum_tuberosum.SolTub_3.0.62.gff3.gz", "errors", - "geo", + "get_candidate_genes", + "get_candidate_genes/candidate_counts.parquet", "idmapping", "idmapping/global_gene_id_mapping.csv", "idmapping/global_gene_metadata.csv", + "idmapping/renamed", + "idmapping/renamed/microarray.normalised.renamed.csv", + "idmapping/renamed/rnaseq.raw.renamed.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", + "merge_all_counts", + "merge_all_counts/all_counts.parquet", + "merge_microarray_counts", + "merge_microarray_counts/all_counts.parquet", + "merge_rnaseq_counts", + "merge_rnaseq_counts/all_counts.parquet", "merged_datasets", + "merged_datasets/whole_design.csv", "multiqc", "multiqc/multiqc_data", "multiqc/multiqc_data/llms-full.txt", @@ -1644,25 +2098,107 @@ "multiqc/multiqc_data/multiqc.parquet", "multiqc/multiqc_data/multiqc_citations.txt", "multiqc/multiqc_data/multiqc_data.json", + "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", + "multiqc/multiqc_data/multiqc_gene_statistics.txt", + "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", + "multiqc/multiqc_data/multiqc_ratio_zeros.txt", + "multiqc/multiqc_data/multiqc_skewness.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_plots", + "multiqc/multiqc_plots/pdf", + "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", + "multiqc/multiqc_plots/pdf/gene_statistics.pdf", + "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", + "multiqc/multiqc_plots/pdf/ratio_zeros.pdf", + "multiqc/multiqc_plots/pdf/skewness.pdf", + "multiqc/multiqc_plots/png", + "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", + "multiqc/multiqc_plots/png/gene_statistics.png", + "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", + "multiqc/multiqc_plots/png/ratio_zeros.png", + "multiqc/multiqc_plots/png/skewness.png", + "multiqc/multiqc_plots/svg", + "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", + "multiqc/multiqc_plots/svg/gene_statistics.svg", + "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", + "multiqc/multiqc_plots/svg/ratio_zeros.svg", + "multiqc/multiqc_plots/svg/skewness.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", + "normalised", + "normalised/microarray.normalised", + "normalised/microarray.normalised/quantile_normalised", + "normalised/microarray.normalised/quantile_normalised/microarray.normalised.renamed.quant_norm.parquet", + "normalised/rnaseq.raw", + "normalised/rnaseq.raw/quantile_normalised", + "normalised/rnaseq.raw/quantile_normalised/rnaseq.raw.renamed.tpm.quant_norm.parquet", + "normalised/rnaseq.raw/tpm", + "normalised/rnaseq.raw/tpm/rnaseq.raw.renamed.tpm.csv", + "normfinder", + "normfinder/stability_values.normfinder.csv", "pipeline_info", "pipeline_info/software_mqc_versions.yml", + "statistics", + "statistics/id_mapping_stats.csv", + "statistics/ratio_zeros.csv", + "statistics/skewness.csv", "warnings" ], [ - "global_gene_id_mapping.csv:md5,f5b78790b2968b7eace7cde8892514a0", - "global_gene_metadata.csv:md5,34366d3e2ac26d7d2240617b381dd222", - "whole_gene_id_mapping.csv:md5,aaedcff272e322e01272d3ac5ed2e1e4", - "whole_gene_metadata.csv:md5,a19dab830757047f6283da475a22b511" + "all_genes_summary.csv:md5,03b370a040cac715b58c31865e7c8557", + "top_stable_genes_summary.csv:md5,03b370a040cac715b58c31865e7c8557", + "top_stable_genes_transposed_counts_filtered.csv:md5,9ee131e180ccaa879342af5873cdcf19", + "ratio_zeros.transposed.csv:md5,08ac547e8c31c2eefeead06c58f1fa3d", + "skewness.transposed.csv:md5,19618bb423fd67de0505f41710bff55f", + "stats_all_genes.csv:md5,38187d6b5b5069799021ae30356a7344", + "microarray.stats_all_genes.csv:md5,edd118adc5300d8bf0c6568742b2fa42", + "rnaseq.stats_all_genes.csv:md5,f3405a820aae655b84e1dda4aadc9074", + "ratio_zeros.txt:md5,ccb973ac5669e90633ff8285ef519341", + "skewness.txt:md5,0dd5498dd111ef24fcd4af8c07d1f3bf", + "gene_transcript_lengths.csv:md5,217aa7c1e227ce2f78a905138d8e5b39", + "stats_with_scores.csv:md5,cfd82cf2f9a27c4c97f8165c987a0da6", + "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", + "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", + "all_genes_summary.csv:md5,03b370a040cac715b58c31865e7c8557", + "whole_design.csv:md5,70d6c2673e619ca52d2774fb3e368382", + "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", + "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", + "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", + "genes.py:md5,680cb5f4e107a3b091821917d72a555c", + "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", + "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", + "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", + "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", + "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", + "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", + "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", + "tables.py:md5,92e8cc03fd8c4192af9201a7ff3cd979", + "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", + "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", + "config.py:md5,b629adc64e497622f000945ee6e6aed2", + "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", + "style.py:md5,b9f1207f06464e43c05e6e3912f12731", + "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", + "Solanum_tuberosum.SolTub_3.0.62.gff3.gz:md5,cca99141f43d57d697f6df75de790e05", + "global_gene_id_mapping.csv:md5,60b6fbc3f18201059a3dadc8417a8940", + "global_gene_metadata.csv:md5,01a7ee010cb92a630dcd079530e7bdff", + "microarray.normalised.renamed.csv:md5,6adb74d67379a5a3d3309b10a0c4bec5", + "rnaseq.raw.renamed.csv:md5,aa22384ba73d180629add4e174c7f37d", + "whole_gene_id_mapping.csv:md5,187a86074197044846bb8565e122eb8e", + "whole_gene_metadata.csv:md5,5ae2d701ca0cb6384d9e1e08a345e452", + "whole_design.csv:md5,70d6c2673e619ca52d2774fb3e368382", + "rnaseq.raw.renamed.tpm.csv:md5,dde1d8ea5d271c4e0486c7ba0936a972", + "stability_values.normfinder.csv:md5,8cc6f5d4bd6432d6756f93a2a3257470", + "id_mapping_stats.csv:md5,e2d63d850b210a088111cd040d34f6e7", + "ratio_zeros.csv:md5,d206e45c16e6bd13de75ea6d20bbd30d", + "skewness.csv:md5,14e2a88e24c48522b03d6cbe8f276023" ] ], "meta": { "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nextflow": "25.10.2" }, - "timestamp": "2025-11-20T21:05:57.16509583" + "timestamp": "2025-12-04T11:44:39.599729388" } } \ No newline at end of file diff --git a/tests/modules/local/aggregate_results/main.nf.test.snap b/tests/modules/local/aggregate_results/main.nf.test.snap index 20057f31..5b5a1939 100644 --- a/tests/modules/local/aggregate_results/main.nf.test.snap +++ b/tests/modules/local/aggregate_results/main.nf.test.snap @@ -3,42 +3,34 @@ "content": [ { "0": [ - "all_genes_summary.csv:md5,f2248dde8d461101270c5fceaf2d8c40" + ], "1": [ - "top_stable_genes_summary.csv:md5,f2248dde8d461101270c5fceaf2d8c40" + ], "2": [ - "all_counts_filtered.parquet:md5,286e7595b3a7b961e39976585d3c106e" + ], "3": [ - "top_stable_genes_transposed_counts_filtered.csv:md5,dcb4f5e4bf8fc0538bf99554fbc47f5f" + ], "4": [ - [ - "AGGREGATE_RESULTS", - "python", - "3.12.8" - ] + ], "5": [ - [ - "AGGREGATE_RESULTS", - "polars", - "1.17.1" - ] + ], "all_counts_filtered": [ - "all_counts_filtered.parquet:md5,286e7595b3a7b961e39976585d3c106e" + ], "all_genes_summary": [ - "all_genes_summary.csv:md5,f2248dde8d461101270c5fceaf2d8c40" + ], "top_stable_genes_summary": [ - "top_stable_genes_summary.csv:md5,f2248dde8d461101270c5fceaf2d8c40" + ], "top_stable_genes_transposed_counts_filtered": [ - "top_stable_genes_transposed_counts_filtered.csv:md5,dcb4f5e4bf8fc0538bf99554fbc47f5f" + ] } ], @@ -46,48 +38,40 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-19T15:20:34.967697416" + "timestamp": "2025-12-03T18:51:31.019751859" }, "With microarray": { "content": [ { "0": [ - "all_genes_summary.csv:md5,1757570c492a6aa0b150613f92304d24" + ], "1": [ - "top_stable_genes_summary.csv:md5,4ab34096569e9bf21e2344daa23c2f41" + ], "2": [ - "all_counts_filtered.parquet:md5,286e7595b3a7b961e39976585d3c106e" + ], "3": [ - "top_stable_genes_transposed_counts_filtered.csv:md5,dcb4f5e4bf8fc0538bf99554fbc47f5f" + ], "4": [ - [ - "AGGREGATE_RESULTS", - "python", - "3.12.8" - ] + ], "5": [ - [ - "AGGREGATE_RESULTS", - "polars", - "1.17.1" - ] + ], "all_counts_filtered": [ - "all_counts_filtered.parquet:md5,286e7595b3a7b961e39976585d3c106e" + ], "all_genes_summary": [ - "all_genes_summary.csv:md5,1757570c492a6aa0b150613f92304d24" + ], "top_stable_genes_summary": [ - "top_stable_genes_summary.csv:md5,4ab34096569e9bf21e2344daa23c2f41" + ], "top_stable_genes_transposed_counts_filtered": [ - "top_stable_genes_transposed_counts_filtered.csv:md5,dcb4f5e4bf8fc0538bf99554fbc47f5f" + ] } ], @@ -95,6 +79,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-19T15:20:39.606518336" + "timestamp": "2025-12-03T18:51:42.230203404" } } \ No newline at end of file diff --git a/tests/modules/local/clean_count_data/main.nf.test b/tests/modules/local/clean_count_data/main.nf.test deleted file mode 100644 index dc131785..00000000 --- a/tests/modules/local/clean_count_data/main.nf.test +++ /dev/null @@ -1,57 +0,0 @@ -nextflow_process { - - name "Test Process CLEAN_COUNT_DATA" - script "modules/local/clean_count_data/main.nf" - process "CLEAN_COUNT_DATA" - tag "clean_count" - - test("Runs ok") { - - when { - process { - """ - input[0] = [ - [dataset: 'test'], - file( '$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet', checkIfExists: true), - file( '$projectDir/tests/test_data/dataset_statistics/output/test.dataset_stats.csv', checkIfExists: true) - ] - input[1] = 0 - """ - } - } - - then { - assertAll( - { assert process.success }, - { assert snapshot(process.out).match() } - ) - } - - } - - test("No sample left") { - - when { - process { - """ - input[0] = [ - [dataset: 'test'], - file( '$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet', checkIfExists: true), - file( '$projectDir/tests/test_data/dataset_statistics/output/test.dataset_stats.csv', checkIfExists: true) - - ] - input[1] = 0.1 - """ - } - } - - then { - assertAll( - { assert process.success }, - { assert snapshot(process.out).match() } - ) - } - - } - -} diff --git a/tests/modules/local/clean_count_data/main.nf.test.snap b/tests/modules/local/clean_count_data/main.nf.test.snap deleted file mode 100644 index 0267a184..00000000 --- a/tests/modules/local/clean_count_data/main.nf.test.snap +++ /dev/null @@ -1,83 +0,0 @@ -{ - "No sample left": { - "content": [ - { - "0": [ - - ], - "1": [ - [ - "test", - "failure_reason.txt:md5,8b7b701a2f7e1540901e9aab371a1421" - ] - ], - "2": [ - [ - "CLEAN_COUNT_DATA", - "python", - "3.12.8" - ] - ], - "3": [ - [ - "CLEAN_COUNT_DATA", - "polars", - "1.17.1" - ] - ], - "counts": [ - - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-08T14:34:33.283176679" - }, - "Runs ok": { - "content": [ - { - "0": [ - [ - { - "dataset": "test" - }, - "cleaned_counts_filtered.parquet:md5,93c0abe8562314fe416769aa160d094a" - ] - ], - "1": [ - - ], - "2": [ - [ - "CLEAN_COUNT_DATA", - "python", - "3.12.8" - ] - ], - "3": [ - [ - "CLEAN_COUNT_DATA", - "polars", - "1.17.1" - ] - ], - "counts": [ - [ - { - "dataset": "test" - }, - "cleaned_counts_filtered.parquet:md5,93c0abe8562314fe416769aa160d094a" - ] - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-08T14:33:31.956184265" - } -} \ No newline at end of file diff --git a/tests/modules/local/compute_base_statistics/main.nf.test.snap b/tests/modules/local/compute_base_statistics/main.nf.test.snap index 87ef14f7..d57408ca 100644 --- a/tests/modules/local/compute_base_statistics/main.nf.test.snap +++ b/tests/modules/local/compute_base_statistics/main.nf.test.snap @@ -3,24 +3,16 @@ "content": [ { "0": [ - "stats_all_genes.csv:md5,bdcac2a45728098fe2d79775bcfdb743" + ], "1": [ - [ - "COMPUTE_BASE_STATISTICS", - "python", - "3.12.8" - ] + ], "2": [ - [ - "COMPUTE_BASE_STATISTICS", - "polars", - "1.17.1" - ] + ], "stats": [ - "stats_all_genes.csv:md5,bdcac2a45728098fe2d79775bcfdb743" + ] } ], @@ -28,30 +20,22 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-19T15:20:52.920513223" + "timestamp": "2025-12-03T18:51:55.581816062" }, "RNAseq platform": { "content": [ { "0": [ - "rnaseq.stats_all_genes.csv:md5,4f87865422331270c8aba3077faab8fa" + ], "1": [ - [ - "COMPUTE_BASE_STATISTICS", - "python", - "3.12.8" - ] + ], "2": [ - [ - "COMPUTE_BASE_STATISTICS", - "polars", - "1.17.1" - ] + ], "stats": [ - "rnaseq.stats_all_genes.csv:md5,4f87865422331270c8aba3077faab8fa" + ] } ], @@ -59,6 +43,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-19T15:20:57.621995644" + "timestamp": "2025-12-03T18:52:06.262096663" } } \ No newline at end of file diff --git a/tests/modules/local/compute_dataset_statistics/main.nf.test b/tests/modules/local/compute_dataset_statistics/main.nf.test new file mode 100644 index 00000000..2504fba8 --- /dev/null +++ b/tests/modules/local/compute_dataset_statistics/main.nf.test @@ -0,0 +1,30 @@ +nextflow_process { + + name "Test Process COMPUTE_DATASET_STATISTICS" + script "modules/local/compute_dataset_statistics/main.nf" + process "COMPUTE_DATASET_STATISTICS" + tag "COMPUTE_DATASET_STATISTICS" + + test("Should not fail") { + + when { + process { + """ + input[0] = [ + [dataset: 'test'], + file( '$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet', checkIfExists: true) + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + +} diff --git a/tests/modules/local/compute_dataset_statistics/main.nf.test.snap b/tests/modules/local/compute_dataset_statistics/main.nf.test.snap new file mode 100644 index 00000000..3ac70367 --- /dev/null +++ b/tests/modules/local/compute_dataset_statistics/main.nf.test.snap @@ -0,0 +1,25 @@ +{ + "Should not fail": { + "content": [ + { + "0": [ + + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.10.2" + }, + "timestamp": "2025-12-04T11:55:04.60917467" + } +} \ No newline at end of file diff --git a/tests/modules/local/compute_stability_scores/main.nf.test.snap b/tests/modules/local/compute_stability_scores/main.nf.test.snap index beae489b..53455505 100644 --- a/tests/modules/local/compute_stability_scores/main.nf.test.snap +++ b/tests/modules/local/compute_stability_scores/main.nf.test.snap @@ -3,24 +3,24 @@ "content": [ { "0": [ - "stats_with_scores.csv:md5,7a33855baf1381d7f2140bc146d196ee" + "stats_with_scores.csv:md5,d1e74b628b6dd02635e07dec414fded0" ], "1": [ [ "COMPUTE_STABILITY_SCORES", "python", - "3.13.7" + "3.12.8" ] ], "2": [ [ "COMPUTE_STABILITY_SCORES", "polars", - "1.33.1" + "1.17.1" ] ], "stats_with_stability_scores": [ - "stats_with_scores.csv:md5,7a33855baf1381d7f2140bc146d196ee" + "stats_with_scores.csv:md5,d1e74b628b6dd02635e07dec414fded0" ] } ], @@ -28,30 +28,30 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-19T15:21:08.808960169" + "timestamp": "2025-12-03T18:52:21.178734391" }, "Without Genorm": { "content": [ { "0": [ - "stats_with_scores.csv:md5,f650d762d9456129d3d262e10ec0f17c" + "stats_with_scores.csv:md5,bf305bce944291ce723ef41b68bac0fc" ], "1": [ [ "COMPUTE_STABILITY_SCORES", "python", - "3.13.7" + "3.12.8" ] ], "2": [ [ "COMPUTE_STABILITY_SCORES", "polars", - "1.33.1" + "1.17.1" ] ], "stats_with_stability_scores": [ - "stats_with_scores.csv:md5,f650d762d9456129d3d262e10ec0f17c" + "stats_with_scores.csv:md5,bf305bce944291ce723ef41b68bac0fc" ] } ], @@ -59,6 +59,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-19T15:21:03.302792724" + "timestamp": "2025-12-03T18:52:14.104078273" } } \ No newline at end of file diff --git a/tests/modules/local/dataset_statistics/main.nf.test b/tests/modules/local/dataset_statistics/main.nf.test deleted file mode 100644 index b31fe99f..00000000 --- a/tests/modules/local/dataset_statistics/main.nf.test +++ /dev/null @@ -1,54 +0,0 @@ -nextflow_process { - - name "Test Process DATASET_STATISTICS" - script "modules/local/dataset_statistics/main.nf" - process "DATASET_STATISTICS" - tag "dataset_statistics" - - test("Uniform distribution") { - - when { - process { - """ - input[0] = [ - [dataset: 'test'], - file( '$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet', checkIfExists: true) - ] - input[1] = "uniform" - """ - } - } - - then { - assertAll( - { assert process.success }, - { assert snapshot(process.out).match() } - ) - } - - } - - test("Normal distribution") { - - when { - process { - """ - input[0] = [ - [dataset: 'test'], - file( '$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet', checkIfExists: true) - ] - input[1] = "normal" - """ - } - } - - then { - assertAll( - { assert process.success }, - { assert snapshot(process.out).match() } - ) - } - - } - -} diff --git a/tests/modules/local/dataset_statistics/main.nf.test.snap b/tests/modules/local/dataset_statistics/main.nf.test.snap deleted file mode 100644 index 39a18f95..00000000 --- a/tests/modules/local/dataset_statistics/main.nf.test.snap +++ /dev/null @@ -1,112 +0,0 @@ -{ - "Uniform distribution": { - "content": [ - { - "0": [ - [ - { - "dataset": "test" - }, - "test.dataset_stats.csv:md5,58ce7d80967422011970661caa885506" - ] - ], - "1": [ - [ - "DATASET_STATISTICS", - "python", - "3.12.8" - ] - ], - "2": [ - [ - "DATASET_STATISTICS", - "pandas", - "2.2.3" - ] - ], - "3": [ - [ - "DATASET_STATISTICS", - "scipy", - "1.15.0" - ] - ], - "4": [ - [ - "DATASET_STATISTICS", - "pyarrow", - "19.0.0" - ] - ], - "stats": [ - [ - { - "dataset": "test" - }, - "test.dataset_stats.csv:md5,58ce7d80967422011970661caa885506" - ] - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.6" - }, - "timestamp": "2025-10-13T17:27:00.822472835" - }, - "Normal distribution": { - "content": [ - { - "0": [ - [ - { - "dataset": "test" - }, - "test.dataset_stats.csv:md5,1c7e068c9d74b0a6ac666fae4dbf3984" - ] - ], - "1": [ - [ - "DATASET_STATISTICS", - "python", - "3.12.8" - ] - ], - "2": [ - [ - "DATASET_STATISTICS", - "pandas", - "2.2.3" - ] - ], - "3": [ - [ - "DATASET_STATISTICS", - "scipy", - "1.15.0" - ] - ], - "4": [ - [ - "DATASET_STATISTICS", - "pyarrow", - "19.0.0" - ] - ], - "stats": [ - [ - { - "dataset": "test" - }, - "test.dataset_stats.csv:md5,1c7e068c9d74b0a6ac666fae4dbf3984" - ] - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.6" - }, - "timestamp": "2025-10-13T17:27:12.096317252" - } -} \ No newline at end of file diff --git a/tests/modules/local/genorm/expression_ratio/main.nf.test.snap b/tests/modules/local/genorm/expression_ratio/main.nf.test.snap index 8b538b8e..1cf86e59 100644 --- a/tests/modules/local/genorm/expression_ratio/main.nf.test.snap +++ b/tests/modules/local/genorm/expression_ratio/main.nf.test.snap @@ -3,31 +3,23 @@ "content": [ { "0": [ - "ratios.0.1.parquet:md5,b0999933fae92eab5ba6e01a17a352df" + ], "1": [ - [ - "EXPRESSION_RATIO", - "python", - "3.12.8" - ] + ], "2": [ - [ - "EXPRESSION_RATIO", - "polars", - "1.17.1" - ] + ], "data": [ - "ratios.0.1.parquet:md5,b0999933fae92eab5ba6e01a17a352df" + ] } ], "meta": { "nf-test": "0.9.2", - "nextflow": "24.10.3" + "nextflow": "25.04.8" }, - "timestamp": "2025-01-29T15:30:55.228354841" + "timestamp": "2025-12-03T18:55:17.991614786" } } \ No newline at end of file diff --git a/tests/modules/local/genorm/make_chunks/main.nf.test.snap b/tests/modules/local/genorm/make_chunks/main.nf.test.snap index d089bf1e..4b06bf77 100644 --- a/tests/modules/local/genorm/make_chunks/main.nf.test.snap +++ b/tests/modules/local/genorm/make_chunks/main.nf.test.snap @@ -3,41 +3,23 @@ "content": [ { "0": [ - [ - "count_chunk.0.parquet:md5,77cbba7f85bb646d4843d756c7ba701d", - "count_chunk.1.parquet:md5,33d252ec9988e951a3b9dbe30d6c2e3d", - "count_chunk.2.parquet:md5,1a80a3d72cd2b090d1b67aa561409f7c", - "count_chunk.3.parquet:md5,34890e85d64bbc623477299cff4b3c24" - ] + ], "1": [ - [ - "MAKE_CHUNKS", - "python", - "3.12.8" - ] + ], "2": [ - [ - "MAKE_CHUNKS", - "polars", - "1.17.1" - ] + ], "chunks": [ - [ - "count_chunk.0.parquet:md5,77cbba7f85bb646d4843d756c7ba701d", - "count_chunk.1.parquet:md5,33d252ec9988e951a3b9dbe30d6c2e3d", - "count_chunk.2.parquet:md5,1a80a3d72cd2b090d1b67aa561409f7c", - "count_chunk.3.parquet:md5,34890e85d64bbc623477299cff4b3c24" - ] + ] } ], "meta": { "nf-test": "0.9.2", - "nextflow": "24.10.3" + "nextflow": "25.04.8" }, - "timestamp": "2025-01-29T19:24:23.507551112" + "timestamp": "2025-12-03T18:55:28.842433752" } } \ No newline at end of file diff --git a/tests/modules/local/genorm/ratio_standard_variation/main.nf.test.snap b/tests/modules/local/genorm/ratio_standard_variation/main.nf.test.snap index 3ebf8613..087dae8c 100644 --- a/tests/modules/local/genorm/ratio_standard_variation/main.nf.test.snap +++ b/tests/modules/local/genorm/ratio_standard_variation/main.nf.test.snap @@ -3,31 +3,23 @@ "content": [ { "0": [ - "std.0.1.parquet:md5,7a1529e9be1f9ca8aecb9146c39ab4f5" + ], "1": [ - [ - "RATIO_STANDARD_VARIATION", - "python", - "3.12.8" - ] + ], "2": [ - [ - "RATIO_STANDARD_VARIATION", - "polars", - "1.17.1" - ] + ], "data": [ - "std.0.1.parquet:md5,7a1529e9be1f9ca8aecb9146c39ab4f5" + ] } ], "meta": { "nf-test": "0.9.2", - "nextflow": "24.10.3" + "nextflow": "25.04.8" }, - "timestamp": "2025-01-29T16:12:29.850020618" + "timestamp": "2025-12-03T18:55:39.031631857" } } \ No newline at end of file diff --git a/tests/modules/local/geo/getaccessions/main.nf.test b/tests/modules/local/geo/getaccessions/main.nf.test index 93f87ba8..85e50ff2 100644 --- a/tests/modules/local/geo/getaccessions/main.nf.test +++ b/tests/modules/local/geo/getaccessions/main.nf.test @@ -13,8 +13,7 @@ nextflow_process { input[0] = "beta_vulgaris" input[1] = "" input[2] = "none" - input[3] = file( '$projectDir/tests/test_data/geo/get_accessions/exclude_one_accession.txt', checkIfExists: true ) - input[4] = "none" + input[3] = file( '$projectDir/tests/test_data/public_accessions/exclude_one_geo_accession.txt', checkIfExists: true ) """ } } @@ -34,27 +33,6 @@ nextflow_process { input[1] = "leaf" input[2] = "microarray" input[3] = [] - input[4] = "none" - """ - } - } - - then { - assert process.success - } - - } - - test("Arabidopsis thaliana - NCBI Bad Gateway") { - - when { - process { - """ - input[0] = "arabidopsis_thaliana" - input[1] = "" - input[2] = "none" - input[3] = [] - input[4] = "GSE18808" """ } } diff --git a/tests/modules/local/geo/getdata/main.nf.test.snap b/tests/modules/local/geo/getdata/main.nf.test.snap index e2b5f595..ee7715de 100644 --- a/tests/modules/local/geo/getdata/main.nf.test.snap +++ b/tests/modules/local/geo/getdata/main.nf.test.snap @@ -105,6 +105,59 @@ }, "timestamp": "2025-11-17T11:27:41.033275401" }, + "Drosophila simulans - Mismatch in suppl data colnames / design": { + "content": [ + { + "0": [ + + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + + ], + "4": [ + + ], + "5": [ + [ + "GEO_GETDATA", + "R", + "4.4.3 (2025-02-28)" + ] + ], + "6": [ + [ + "GEO_GETDATA", + "GEOquery", + "2.74.0" + ] + ], + "7": [ + [ + "GEO_GETDATA", + "dplyr", + "1.1.4" + ] + ], + "counts": [ + + ], + "design": [ + + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-12-03T18:57:21.960035842" + }, "Accession does not exist": { "content": [ { @@ -264,7 +317,7 @@ }, "timestamp": "2025-11-17T11:27:58.80892872" }, - "Drosophila simulans - Only one sample among several": { + "Drosophila simulans - Only series suppl data but multiple species": { "content": [ { "0": [ @@ -315,7 +368,7 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-17T11:27:32.555586232" + "timestamp": "2025-12-03T18:57:10.8714341" }, "Beta vulgaris - Small RNA of sugar beet in response to drought stress": { "content": [ diff --git a/tests/modules/local/get_candidate_genes/main.nf.test.snap b/tests/modules/local/get_candidate_genes/main.nf.test.snap index d99ca2bb..91c17521 100644 --- a/tests/modules/local/get_candidate_genes/main.nf.test.snap +++ b/tests/modules/local/get_candidate_genes/main.nf.test.snap @@ -3,24 +3,16 @@ "content": [ { "0": [ - "candidate_counts.parquet:md5,a671f20b4818b914ca454dce84308287" + ], "1": [ - [ - "GET_CANDIDATE_GENES", - "python", - "3.12.8" - ] + ], "2": [ - [ - "GET_CANDIDATE_GENES", - "polars", - "1.17.1" - ] + ], "counts": [ - "candidate_counts.parquet:md5,a671f20b4818b914ca454dce84308287" + ] } ], @@ -28,30 +20,22 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T14:46:13.79462279" + "timestamp": "2025-12-03T18:57:33.822736894" }, "With RCVm and no filter on expression level": { "content": [ { "0": [ - "candidate_counts.parquet:md5,93c0abe8562314fe416769aa160d094a" + ], "1": [ - [ - "GET_CANDIDATE_GENES", - "python", - "3.12.8" - ] + ], "2": [ - [ - "GET_CANDIDATE_GENES", - "polars", - "1.17.1" - ] + ], "counts": [ - "candidate_counts.parquet:md5,93c0abe8562314fe416769aa160d094a" + ] } ], @@ -59,6 +43,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T14:46:41.537978144" + "timestamp": "2025-12-03T18:57:44.20986199" } } \ No newline at end of file diff --git a/tests/modules/local/gprofiler/idmapping/main.nf.test b/tests/modules/local/gprofiler/idmapping/main.nf.test index 63925534..2e4742ba 100644 --- a/tests/modules/local/gprofiler/idmapping/main.nf.test +++ b/tests/modules/local/gprofiler/idmapping/main.nf.test @@ -48,9 +48,7 @@ nextflow_process { then { assertAll( - { assert process.success }, - { assert process.out.mapping.size() == 0 }, - { assert snapshot(process.out).match() } + { assert !process.success } ) } } diff --git a/tests/modules/local/gprofiler/idmapping/main.nf.test.snap b/tests/modules/local/gprofiler/idmapping/main.nf.test.snap index 4416cbc8..c557b593 100644 --- a/tests/modules/local/gprofiler/idmapping/main.nf.test.snap +++ b/tests/modules/local/gprofiler/idmapping/main.nf.test.snap @@ -80,5 +80,34 @@ "nextflow": "25.04.8" }, "timestamp": "2025-11-20T18:15:36.900376779" + }, + "Entrez - No mapping found": { + "content": [ + { + "0": [ + + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + + ], + "4": [ + + ], + "metadata": [ + + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-12-03T18:57:58.500424421" } } \ No newline at end of file diff --git a/tests/modules/local/merge_counts/main.nf.test.snap b/tests/modules/local/merge_counts/main.nf.test.snap index 167e0bb8..6edd3d29 100644 --- a/tests/modules/local/merge_counts/main.nf.test.snap +++ b/tests/modules/local/merge_counts/main.nf.test.snap @@ -3,7 +3,7 @@ "content": [ { "0": [ - "all_counts.parquet:md5,d23e002ab08e544a259e5bab8fa70d71" + "all_counts.parquet:md5,ea386983967ba07c233245b530c3edd0" ], "1": [ [ @@ -27,7 +27,7 @@ ] ], "counts": [ - "all_counts.parquet:md5,d23e002ab08e544a259e5bab8fa70d71" + "all_counts.parquet:md5,ea386983967ba07c233245b530c3edd0" ] } ], @@ -35,13 +35,13 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T15:04:53.721960825" + "timestamp": "2025-12-03T18:58:05.174305853" }, "2 identical files": { "content": [ { "0": [ - "all_counts.parquet:md5,9ab0a1f56fbdefb41bba263d08833a19" + "all_counts.parquet:md5,a83c94a90be32af4fc3bcc4909f6f1d2" ], "1": [ [ @@ -65,7 +65,7 @@ ] ], "counts": [ - "all_counts.parquet:md5,9ab0a1f56fbdefb41bba263d08833a19" + "all_counts.parquet:md5,a83c94a90be32af4fc3bcc4909f6f1d2" ] } ], @@ -73,13 +73,13 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T15:04:58.602015048" + "timestamp": "2025-12-03T18:58:11.980577512" }, "1 file": { "content": [ { "0": [ - "all_counts.parquet:md5,a40507b974dbda63838b52a2458c5220" + "all_counts.parquet:md5,57629ccf12df0e16a39281dfe02df4bc" ], "1": [ [ @@ -103,7 +103,7 @@ ] ], "counts": [ - "all_counts.parquet:md5,a40507b974dbda63838b52a2458c5220" + "all_counts.parquet:md5,57629ccf12df0e16a39281dfe02df4bc" ] } ], @@ -111,6 +111,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-08T15:05:44.418436182" + "timestamp": "2025-12-03T18:58:18.896026558" } } \ No newline at end of file diff --git a/tests/modules/local/normalisation/edger/main.nf.test b/tests/modules/local/normalisation/compute_cpm/main.nf.test similarity index 57% rename from tests/modules/local/normalisation/edger/main.nf.test rename to tests/modules/local/normalisation/compute_cpm/main.nf.test index e552e8d5..c32312b8 100644 --- a/tests/modules/local/normalisation/edger/main.nf.test +++ b/tests/modules/local/normalisation/compute_cpm/main.nf.test @@ -1,9 +1,10 @@ nextflow_process { - name "Test Process NORMALISATION_EDGER" - script "modules/local/normalisation/edger/main.nf" - process "NORMALISATION_EDGER" - tag "edger" + name "Test Process NORMALISATION_COMPUTE_CPM" + script "modules/local/normalisation/compute_cpm/main.nf" + process "NORMALISATION_COMPUTE_CPM" + tag "cpm_norm" + test("Very small dataset") { @@ -12,9 +13,8 @@ nextflow_process { process { """ input[0] = [ - [ accession: "test" ], - file('$projectDir/tests/test_data/normalisation/base/counts.csv'), - file('$projectDir/tests/test_data/normalisation/base/design.csv') + [ dataset: "test" ], + file('$projectDir/tests/test_data/normalisation/base/counts.csv') ] """ } @@ -23,7 +23,7 @@ nextflow_process { then { assertAll( { assert process.success }, - { assert snapshot(process.out.cpm).match() } + { assert snapshot(process.out).match() } ) } @@ -31,16 +31,13 @@ nextflow_process { test("Rows with many zeros") { - tag "rows_many_zeros" - when { process { """ input[0] = [ - [ accession: "test"], - file('$projectDir/tests/test_data/normalisation/many_zeros/counts.csv'), - file('$projectDir/tests/test_data/normalisation/many_zeros/design.csv') + [ dataset: "test"], + file('$projectDir/tests/test_data/normalisation/many_zeros/counts.csv') ] """ } @@ -49,7 +46,7 @@ nextflow_process { then { assertAll( { assert process.success }, - { assert snapshot(process.out.cpm).match() } + { assert snapshot(process.out).match() } ) } @@ -62,9 +59,8 @@ nextflow_process { process { """ input[0] = [ - [ accession: "accession" ], - file('$projectDir/tests/test_data/normalisation/one_group/counts.csv'), - file('$projectDir/tests/test_data/normalisation/one_group/design.csv') + [ dataset: "accession" ], + file('$projectDir/tests/test_data/normalisation/one_group/counts.csv') ] """ } @@ -73,7 +69,7 @@ nextflow_process { then { assertAll( { assert process.success }, - { assert snapshot(process.out.cpm).match() } + { assert snapshot(process.out).match() } ) } @@ -86,9 +82,8 @@ nextflow_process { process { """ input[0] = [ - [ accession: "accession" ], - file('$projectDir/tests/test_data/normalisation/base/counts.tsv'), - file('$projectDir/tests/test_data/normalisation/base/design.tsv') + [ dataset: "accession" ], + file('$projectDir/tests/test_data/normalisation/base/counts.tsv') ] """ } @@ -97,7 +92,7 @@ nextflow_process { then { assertAll( { assert process.success }, - { assert snapshot(process.out.cpm).match() } + { assert snapshot(process.out).match() } ) } diff --git a/tests/modules/local/normalisation/compute_cpm/main.nf.test.snap b/tests/modules/local/normalisation/compute_cpm/main.nf.test.snap new file mode 100644 index 00000000..5ea72bbb --- /dev/null +++ b/tests/modules/local/normalisation/compute_cpm/main.nf.test.snap @@ -0,0 +1,190 @@ +{ + "Very small dataset": { + "content": [ + { + "0": [ + [ + { + "dataset": "test" + }, + "counts.cpm.csv:md5,9f7c5941fb9bb3293ac26f7130275975" + ] + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + [ + "NORMALISATION_COMPUTE_CPM", + "python", + "3.13.7" + ] + ], + "4": [ + [ + "NORMALISATION_COMPUTE_CPM", + "pandas", + "2.3.3" + ] + ], + "counts": [ + [ + { + "dataset": "test" + }, + "counts.cpm.csv:md5,9f7c5941fb9bb3293ac26f7130275975" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.10.2" + }, + "timestamp": "2025-12-04T11:28:41.011650184" + }, + "One group": { + "content": [ + { + "0": [ + [ + { + "dataset": "accession" + }, + "counts.cpm.csv:md5,288f9099c4dd37ddeaec3f90230431fa" + ] + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + [ + "NORMALISATION_COMPUTE_CPM", + "python", + "3.13.7" + ] + ], + "4": [ + [ + "NORMALISATION_COMPUTE_CPM", + "pandas", + "2.3.3" + ] + ], + "counts": [ + [ + { + "dataset": "accession" + }, + "counts.cpm.csv:md5,288f9099c4dd37ddeaec3f90230431fa" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.10.2" + }, + "timestamp": "2025-12-04T11:30:00.012222498" + }, + "TSV files": { + "content": [ + { + "0": [ + [ + { + "dataset": "accession" + }, + "counts.cpm.csv:md5,9f7c5941fb9bb3293ac26f7130275975" + ] + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + [ + "NORMALISATION_COMPUTE_CPM", + "python", + "3.13.7" + ] + ], + "4": [ + [ + "NORMALISATION_COMPUTE_CPM", + "pandas", + "2.3.3" + ] + ], + "counts": [ + [ + { + "dataset": "accession" + }, + "counts.cpm.csv:md5,9f7c5941fb9bb3293ac26f7130275975" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.10.2" + }, + "timestamp": "2025-12-04T11:30:07.970928668" + }, + "Rows with many zeros": { + "content": [ + { + "0": [ + [ + { + "dataset": "test" + }, + "counts.cpm.csv:md5,b9feb7bfc08ed07c717fecee08c2c8b4" + ] + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + [ + "NORMALISATION_COMPUTE_CPM", + "python", + "3.13.7" + ] + ], + "4": [ + [ + "NORMALISATION_COMPUTE_CPM", + "pandas", + "2.3.3" + ] + ], + "counts": [ + [ + { + "dataset": "test" + }, + "counts.cpm.csv:md5,b9feb7bfc08ed07c717fecee08c2c8b4" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.10.2" + }, + "timestamp": "2025-12-04T11:29:48.474873097" + } +} \ No newline at end of file diff --git a/tests/modules/local/normalisation/deseq2/main.nf.test b/tests/modules/local/normalisation/compute_tpm/main.nf.test similarity index 57% rename from tests/modules/local/normalisation/deseq2/main.nf.test rename to tests/modules/local/normalisation/compute_tpm/main.nf.test index 778c69eb..23463223 100644 --- a/tests/modules/local/normalisation/deseq2/main.nf.test +++ b/tests/modules/local/normalisation/compute_tpm/main.nf.test @@ -1,9 +1,10 @@ nextflow_process { - name "Test Process NORMALISATION_DESEQ2" - script "modules/local/normalisation/deseq2/main.nf" - process "NORMALISATION_DESEQ2" - tag "deseq2" + name "Test Process NORMALISATION_COMPUTE_TPM" + script "modules/local/normalisation/compute_tpm/main.nf" + process "NORMALISATION_COMPUTE_TPM" + tag "tpm_norm" + test("Very small dataset") { @@ -12,10 +13,10 @@ nextflow_process { process { """ input[0] = [ - [ accession: "test" ], - file('$projectDir/tests/test_data/normalisation/base/counts.csv'), - file('$projectDir/tests/test_data/normalisation/base/design.csv') + [ dataset: "test" ], + file('$projectDir/tests/test_data/normalisation/base/counts.csv') ] + input[1] = file('$projectDir/tests/test_data/normalisation/base/gene_lengths.csv') """ } } @@ -23,7 +24,7 @@ nextflow_process { then { assertAll( { assert process.success }, - { assert snapshot(process.out.cpm).match() } + { assert snapshot(process.out).match() } ) } @@ -31,17 +32,15 @@ nextflow_process { test("Rows with many zeros") { - tag "deseq2_many_zeros" - when { process { """ input[0] = [ - [ accession: "test"], - file('$projectDir/tests/test_data/normalisation/many_zeros/counts.csv'), - file('$projectDir/tests/test_data/normalisation/many_zeros/design.csv') + [ dataset: "test"], + file('$projectDir/tests/test_data/normalisation/many_zeros/counts.csv') ] + input[1] = file('$projectDir/tests/test_data/normalisation/many_zeros/gene_lengths.csv') """ } } @@ -49,7 +48,7 @@ nextflow_process { then { assertAll( { assert process.success }, - { assert snapshot(process.out.cpm).match() } + { assert snapshot(process.out).match() } ) } @@ -62,10 +61,10 @@ nextflow_process { process { """ input[0] = [ - [ accession: "accession" ], - file('$projectDir/tests/test_data/normalisation/one_group/counts.csv'), - file('$projectDir/tests/test_data/normalisation/one_group/design.csv') + [ dataset: "test" ], + file('$projectDir/tests/test_data/normalisation/one_group/counts.csv') ] + input[1] = file('$projectDir/tests/test_data/normalisation/one_group/gene_lengths.csv') """ } } @@ -73,7 +72,7 @@ nextflow_process { then { assertAll( { assert process.success }, - { assert snapshot(process.out.cpm).match() } + { assert snapshot(process.out).match() } ) } @@ -86,10 +85,10 @@ nextflow_process { process { """ input[0] = [ - [ accession: "accession" ], - file('$projectDir/tests/test_data/normalisation/base/counts.tsv'), - file('$projectDir/tests/test_data/normalisation/base/design.tsv') + [ dataset: "test" ], + file('$projectDir/tests/test_data/normalisation/base/counts.tsv') ] + input[1] = file('$projectDir/tests/test_data/normalisation/base/gene_lengths.csv') """ } } @@ -97,10 +96,11 @@ nextflow_process { then { assertAll( { assert process.success }, - { assert snapshot(process.out.cpm).match() } + { assert snapshot(process.out).match() } ) } } + } diff --git a/tests/modules/local/normalisation/compute_tpm/main.nf.test.snap b/tests/modules/local/normalisation/compute_tpm/main.nf.test.snap new file mode 100644 index 00000000..87cc9528 --- /dev/null +++ b/tests/modules/local/normalisation/compute_tpm/main.nf.test.snap @@ -0,0 +1,190 @@ +{ + "Very small dataset": { + "content": [ + { + "0": [ + [ + { + "dataset": "test" + }, + "counts.tpm.csv:md5,2c9efc0a2ca95cfc4b626e5cbf3c6dbe" + ] + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + [ + "NORMALISATION_COMPUTE_TPM", + "python", + "3.13.7" + ] + ], + "4": [ + [ + "NORMALISATION_COMPUTE_TPM", + "pandas", + "2.3.3" + ] + ], + "counts": [ + [ + { + "dataset": "test" + }, + "counts.tpm.csv:md5,2c9efc0a2ca95cfc4b626e5cbf3c6dbe" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.10.2" + }, + "timestamp": "2025-12-04T11:31:38.651488715" + }, + "One group": { + "content": [ + { + "0": [ + [ + { + "dataset": "test" + }, + "counts.tpm.csv:md5,4a0b42acaeb45f435f97b7bc9c99f36e" + ] + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + [ + "NORMALISATION_COMPUTE_TPM", + "python", + "3.13.7" + ] + ], + "4": [ + [ + "NORMALISATION_COMPUTE_TPM", + "pandas", + "2.3.3" + ] + ], + "counts": [ + [ + { + "dataset": "test" + }, + "counts.tpm.csv:md5,4a0b42acaeb45f435f97b7bc9c99f36e" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.10.2" + }, + "timestamp": "2025-12-04T11:32:01.357798876" + }, + "TSV files": { + "content": [ + { + "0": [ + [ + { + "dataset": "test" + }, + "counts.tpm.csv:md5,2c9efc0a2ca95cfc4b626e5cbf3c6dbe" + ] + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + [ + "NORMALISATION_COMPUTE_TPM", + "python", + "3.13.7" + ] + ], + "4": [ + [ + "NORMALISATION_COMPUTE_TPM", + "pandas", + "2.3.3" + ] + ], + "counts": [ + [ + { + "dataset": "test" + }, + "counts.tpm.csv:md5,2c9efc0a2ca95cfc4b626e5cbf3c6dbe" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.10.2" + }, + "timestamp": "2025-12-04T11:32:12.08145428" + }, + "Rows with many zeros": { + "content": [ + { + "0": [ + [ + { + "dataset": "test" + }, + "counts.tpm.csv:md5,7cdca05fb62e5216a79e46286e0466b7" + ] + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + [ + "NORMALISATION_COMPUTE_TPM", + "python", + "3.13.7" + ] + ], + "4": [ + [ + "NORMALISATION_COMPUTE_TPM", + "pandas", + "2.3.3" + ] + ], + "counts": [ + [ + { + "dataset": "test" + }, + "counts.tpm.csv:md5,7cdca05fb62e5216a79e46286e0466b7" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.10.2" + }, + "timestamp": "2025-12-04T11:31:50.194232905" + } +} \ No newline at end of file diff --git a/tests/modules/local/normalisation/deseq2/main.nf.test.snap b/tests/modules/local/normalisation/deseq2/main.nf.test.snap deleted file mode 100644 index bd1a6b49..00000000 --- a/tests/modules/local/normalisation/deseq2/main.nf.test.snap +++ /dev/null @@ -1,70 +0,0 @@ -{ - "Very small dataset": { - "content": [ - [ - [ - { - "accession": "test" - }, - "counts.cpm.csv:md5,df001c189c61c11dfab04d1bb47f7511" - ] - ] - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-19T15:27:44.374650599" - }, - "One group": { - "content": [ - [ - [ - { - "accession": "accession" - }, - "counts.cpm.csv:md5,b77d41a310a21def7b955335880f6892" - ] - ] - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-17T14:42:06.14494924" - }, - "TSV files": { - "content": [ - [ - [ - { - "accession": "accession" - }, - "counts.cpm.csv:md5,df001c189c61c11dfab04d1bb47f7511" - ] - ] - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-17T14:42:14.745341728" - }, - "Rows with many zeros": { - "content": [ - [ - [ - { - "accession": "test" - }, - "counts.cpm.csv:md5,921d29a427ba35ee3e798b4ac2123fd4" - ] - ] - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-19T15:27:53.007766023" - } -} \ No newline at end of file diff --git a/tests/modules/local/normalisation/edger/main.nf.test.snap b/tests/modules/local/normalisation/edger/main.nf.test.snap deleted file mode 100644 index 807fa0fb..00000000 --- a/tests/modules/local/normalisation/edger/main.nf.test.snap +++ /dev/null @@ -1,70 +0,0 @@ -{ - "Very small dataset": { - "content": [ - [ - [ - { - "accession": "test" - }, - "counts.cpm.csv:md5,537bffb095e79d9667c955accd81e3a2" - ] - ] - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-19T15:28:15.443780813" - }, - "One group": { - "content": [ - [ - [ - { - "accession": "accession" - }, - "counts.cpm.csv:md5,680e59fd500ddd83ead508a63f3f9b48" - ] - ] - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-17T14:43:10.243694942" - }, - "TSV files": { - "content": [ - [ - [ - { - "accession": "accession" - }, - "counts.cpm.csv:md5,537bffb095e79d9667c955accd81e3a2" - ] - ] - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-17T14:43:14.980374559" - }, - "Rows with many zeros": { - "content": [ - [ - [ - { - "accession": "test" - }, - "counts.cpm.csv:md5,f4624bdcb195b95f6e8bd9ee08470d3d" - ] - ] - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-19T15:28:20.601710839" - } -} \ No newline at end of file diff --git a/tests/modules/local/normfinder/main.nf.test.snap b/tests/modules/local/normfinder/main.nf.test.snap index 77bd83d3..e54a6eb4 100644 --- a/tests/modules/local/normfinder/main.nf.test.snap +++ b/tests/modules/local/normfinder/main.nf.test.snap @@ -3,24 +3,16 @@ "content": [ { "0": [ - "stability_values.normfinder.csv:md5,febede14f422e963abd897bc9efa897e" + ], "1": [ - [ - "NORMFINDER", - "python", - "3.13.7" - ] + ], "2": [ - [ - "NORMFINDER", - "polars", - "1.33.1" - ] + ], "stability_values": [ - "stability_values.normfinder.csv:md5,febede14f422e963abd897bc9efa897e" + ] } ], @@ -28,30 +20,22 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-19T15:28:45.03556591" + "timestamp": "2025-12-03T18:59:42.999801136" }, "Very small dataset - Cq values": { "content": [ { "0": [ - "stability_values.normfinder.csv:md5,bdc363268df9d133b8ae7df5a2204103" + ], "1": [ - [ - "NORMFINDER", - "python", - "3.13.7" - ] + ], "2": [ - [ - "NORMFINDER", - "polars", - "1.33.1" - ] + ], "stability_values": [ - "stability_values.normfinder.csv:md5,bdc363268df9d133b8ae7df5a2204103" + ] } ], @@ -59,6 +43,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-19T15:28:38.129095619" + "timestamp": "2025-12-03T18:59:29.818592345" } } \ No newline at end of file diff --git a/tests/modules/local/quantile_normalisation/main.nf.test.snap b/tests/modules/local/quantile_normalisation/main.nf.test.snap index 6f7600da..0816204c 100644 --- a/tests/modules/local/quantile_normalisation/main.nf.test.snap +++ b/tests/modules/local/quantile_normalisation/main.nf.test.snap @@ -7,7 +7,7 @@ { "dataset": "test" }, - "count.raw.cpm.quant_norm.parquet:md5,6c49b5fe4e23e64dbc6cad6355432b49" + "count.raw.cpm.quant_norm.parquet:md5,25e7be19da270c4211ebbbb7dcfb59bd" ] ], "1": [ @@ -43,7 +43,7 @@ { "dataset": "test" }, - "count.raw.cpm.quant_norm.parquet:md5,6c49b5fe4e23e64dbc6cad6355432b49" + "count.raw.cpm.quant_norm.parquet:md5,25e7be19da270c4211ebbbb7dcfb59bd" ] ] } @@ -52,7 +52,7 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-10-16T05:39:20.700607876" + "timestamp": "2025-12-03T18:59:52.183063354" }, "Normal target distribution": { "content": [ @@ -62,7 +62,7 @@ { "dataset": "test" }, - "count.raw.cpm.quant_norm.parquet:md5,21970d44bc6c08742ea41cee57065d69" + "count.raw.cpm.quant_norm.parquet:md5,a2fc7e21b3e26558ff4f76933ed6d512" ] ], "1": [ @@ -98,7 +98,7 @@ { "dataset": "test" }, - "count.raw.cpm.quant_norm.parquet:md5,21970d44bc6c08742ea41cee57065d69" + "count.raw.cpm.quant_norm.parquet:md5,a2fc7e21b3e26558ff4f76933ed6d512" ] ] } @@ -107,6 +107,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-10-16T05:39:32.935142616" + "timestamp": "2025-12-03T19:00:01.455299438" } } \ No newline at end of file diff --git a/tests/modules/local/rename_gene_ids/main.nf.test b/tests/modules/local/rename_gene_ids/main.nf.test index 438a7425..51eff439 100644 --- a/tests/modules/local/rename_gene_ids/main.nf.test +++ b/tests/modules/local/rename_gene_ids/main.nf.test @@ -17,7 +17,6 @@ nextflow_process { ] ) input[1] = file("$projectDir/tests/test_data/idmapping/mapped/mapped_gene_ids.csv", checkIfExists: true) - input[2] = Channel.value([]) """ } } @@ -44,8 +43,7 @@ nextflow_process { file("$projectDir/tests/test_data/idmapping/tsv/counts.ensembl_ids.tsv", checkIfExists: true) ] ) - input[1] = file("$projectDir/tests/test_data/idmapping/mapped/mapped_gene_ids.csv", checkIfExists: true) - input[2] = file( "$projectDir/tests/test_data/idmapping/tsv/mapping.tsv", checkIfExists: true) + input[1] = file( "$projectDir/tests/test_data/idmapping/tsv/mapping.tsv", checkIfExists: true) """ } } diff --git a/tests/modules/local/rename_gene_ids/main.nf.test.snap b/tests/modules/local/rename_gene_ids/main.nf.test.snap index 31c09ceb..70d08eaa 100644 --- a/tests/modules/local/rename_gene_ids/main.nf.test.snap +++ b/tests/modules/local/rename_gene_ids/main.nf.test.snap @@ -7,7 +7,7 @@ { "dataset": "test" }, - "counts.ensembl_ids.renamed.csv:md5,a72744efb93ed948149c48626a338f04" + "counts.ensembl_ids.renamed.csv:md5,c0fa7b914239a91f84e6685b465983cf" ] ], "1": [ @@ -17,24 +17,31 @@ ], "3": [ + [ + "test", + "3", + "0" + ] + ], + "4": [ [ "RENAME_GENE_IDS", "python", - "3.13.5" + "3.14.0" ] ], - "4": [ + "5": [ [ "RENAME_GENE_IDS", "pandas", - "2.3.1" + "2.3.3" ] ], - "5": [ + "6": [ [ "RENAME_GENE_IDS", - "requests", - "2.32.4" + "polars", + "1.35.2" ] ], "counts": [ @@ -42,7 +49,7 @@ { "dataset": "test" }, - "counts.ensembl_ids.renamed.csv:md5,a72744efb93ed948149c48626a338f04" + "counts.ensembl_ids.renamed.csv:md5,c0fa7b914239a91f84e6685b465983cf" ] ] } @@ -51,7 +58,7 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-20T18:21:55.432510225" + "timestamp": "2025-12-04T10:35:54.485094966" }, "Map Ensembl IDs": { "content": [ @@ -61,7 +68,7 @@ { "dataset": "test" }, - "counts.ensembl_ids.renamed.csv:md5,a1633a75d1eb91103208014705409320" + "counts.ensembl_ids.renamed.csv:md5,ed287900c6de53bfff9d73c42a57f3dc" ] ], "1": [ @@ -71,24 +78,31 @@ ], "3": [ + [ + "test", + "3", + "0" + ] + ], + "4": [ [ "RENAME_GENE_IDS", "python", - "3.13.5" + "3.14.0" ] ], - "4": [ + "5": [ [ "RENAME_GENE_IDS", "pandas", - "2.3.1" + "2.3.3" ] ], - "5": [ + "6": [ [ "RENAME_GENE_IDS", - "requests", - "2.32.4" + "polars", + "1.35.2" ] ], "counts": [ @@ -96,7 +110,7 @@ { "dataset": "test" }, - "counts.ensembl_ids.renamed.csv:md5,a1633a75d1eb91103208014705409320" + "counts.ensembl_ids.renamed.csv:md5,ed287900c6de53bfff9d73c42a57f3dc" ] ] } @@ -105,6 +119,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-20T18:20:21.92552791" + "timestamp": "2025-12-04T10:35:48.129574517" } } \ No newline at end of file diff --git a/tests/subworkflows/local/download_public_datasets/main.nf.test b/tests/subworkflows/local/download_public_datasets/main.nf.test new file mode 100644 index 00000000..e7759b47 --- /dev/null +++ b/tests/subworkflows/local/download_public_datasets/main.nf.test @@ -0,0 +1,70 @@ +nextflow_workflow { + + name "Test Workflow DOWNLOAD_PUBLIC_DATASETS" + script "subworkflows/local/download_public_datasets/main.nf" + workflow "DOWNLOAD_PUBLIC_DATASETS" + + test("Beta vulgaris - Eatlas + GEO - all accessions") { + + when { + params { + species = 'beta vulgaris' + } + workflow { + """ + input[0] = channel.value( params.species.split(' ').join('_') ) + input[1] = channel.fromList(['E-MTAB-8187', 'GSE107627', 'GSE114968', 'GSE135555', 'GSE205413', 'GSE269454', 'GSE281272', 'GSE55951', 'GSE79526', 'GSE92859']) + """ + } + } + + then { + assert workflow.success + assert snapshot(workflow.out).match() + } + + } + + test("Beta vulgaris - Eatlas only") { + + when { + params { + species = 'beta vulgaris' + } + workflow { + """ + input[0] = channel.value( params.species.split(' ').join('_') ) + input[1] = channel.fromList(['E-MTAB-8187']) + """ + } + } + + then { + assert workflow.success + assert snapshot(workflow.out).match() + } + + } + + test("No accessions") { + + when { + params { + species = 'beta vulgaris' + } + workflow { + """ + input[0] = channel.value( params.species.split(' ').join('_') ) + input[1] = channel.empty() + """ + } + } + + then { + assert workflow.success + assert snapshot(workflow.out).match() + } + + } + +} diff --git a/tests/subworkflows/local/download_public_datasets/main.nf.test.snap b/tests/subworkflows/local/download_public_datasets/main.nf.test.snap new file mode 100644 index 00000000..fc907c93 --- /dev/null +++ b/tests/subworkflows/local/download_public_datasets/main.nf.test.snap @@ -0,0 +1,91 @@ +{ + "Beta vulgaris - Eatlas only": { + "content": [ + { + "0": [ + [ + { + "dataset": "E_MTAB_8187_rnaseq", + "design": "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf" + }, + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1" + ] + ], + "datasets": [ + [ + { + "dataset": "E_MTAB_8187_rnaseq", + "design": "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf" + }, + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-12-04T10:36:40.174934839" + }, + "No accessions": { + "content": [ + { + "0": [ + + ], + "datasets": [ + + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-12-04T10:36:44.555546839" + }, + "Beta vulgaris - Eatlas + GEO - all accessions": { + "content": [ + { + "0": [ + [ + { + "dataset": "E_MTAB_8187_rnaseq", + "design": "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf" + }, + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1" + ], + [ + { + "dataset": "GSE55951_GPL18429", + "design": "GSE55951_GPL18429.microarray.normalised.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d" + }, + "GSE55951_GPL18429.microarray.normalised.counts.csv:md5,18fd2d728ad2ec5cb78f994f73375144" + ] + ], + "datasets": [ + [ + { + "dataset": "E_MTAB_8187_rnaseq", + "design": "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf" + }, + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1" + ], + [ + { + "dataset": "GSE55951_GPL18429", + "design": "GSE55951_GPL18429.microarray.normalised.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d" + }, + "GSE55951_GPL18429.microarray.normalised.counts.csv:md5,18fd2d728ad2ec5cb78f994f73375144" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.10.2" + }, + "timestamp": "2025-12-04T13:05:11.570641957" + } +} \ No newline at end of file diff --git a/tests/subworkflows/local/expression_normalisation/main.nf.test b/tests/subworkflows/local/expression_normalisation/main.nf.test index a88f0faf..33fbe3f5 100644 --- a/tests/subworkflows/local/expression_normalisation/main.nf.test +++ b/tests/subworkflows/local/expression_normalisation/main.nf.test @@ -6,7 +6,7 @@ nextflow_workflow { tag "subworkflow_expression_normalisation" tag "subworkflow" - test("DESeq2 Normalisation") { + test("TPM Normalisation") { when { workflow { @@ -15,14 +15,20 @@ nextflow_workflow { rnaseq_raw_design_file = file( '$projectDir/tests/test_data/input_datasets/rnaseq.raw.design.csv', checkIfExists: true ) microarray_normalised_file = file( '$projectDir/tests/test_data/input_datasets/microarray.normalised.csv', checkIfExists: true ) microarray_normalised_design_file = file( '$projectDir/tests/test_data/input_datasets/microarray.normalised.design.csv', checkIfExists: true ) - ch_datasets = Channel.of( - [ [normalised: false, design: rnaseq_raw_design_file, dataset: "rnaseq_raw", platform: "rnaseq"], rnaseq_raw_file], - [ [normalised: true, design: microarray_normalised_design_file, dataset: "microarray_normalised", platform: "microarray"], microarray_normalised_file ] + ch_datasets = channel.of( + [ + [normalised: false, design: rnaseq_raw_design_file, dataset: "rnaseq_raw", platform: "rnaseq"], + rnaseq_raw_file + ], + [ + [normalised: true, design: microarray_normalised_design_file, dataset: "microarray_normalised", platform: "microarray"], + microarray_normalised_file + ] ) - normalisation_method = "deseq2" - input[0] = ch_datasets - input[1] = normalisation_method - input[2] = "uniform" + input[0] = "solanum_tuberosum" + input[1] = ch_datasets + input[2] = "tpm" + input[3] = "uniform" """ } } @@ -36,7 +42,7 @@ nextflow_workflow { } - test("EdgeR Normalisation") { + test("CPM Normalisation") { when { workflow { @@ -45,14 +51,20 @@ nextflow_workflow { rnaseq_raw_design_file = file( '$projectDir/tests/test_data/input_datasets/rnaseq.raw.design.csv', checkIfExists: true ) microarray_normalised_file = file( '$projectDir/tests/test_data/input_datasets/microarray.normalised.csv', checkIfExists: true ) microarray_normalised_design_file = file( '$projectDir/tests/test_data/input_datasets/microarray.normalised.design.csv', checkIfExists: true ) - ch_datasets = Channel.of( - [ [normalised: false, design: rnaseq_raw_design_file, dataset: "rnaseq_raw", platform: "rnaseq"], rnaseq_raw_file], - [ [normalised: true, design: microarray_normalised_design_file, dataset: "microarray_normalised", platform: "microarray"], microarray_normalised_file ] + ch_datasets = channel.of( + [ + [normalised: false, design: rnaseq_raw_design_file, dataset: "rnaseq_raw", platform: "rnaseq"], + rnaseq_raw_file + ], + [ + [normalised: true, design: microarray_normalised_design_file, dataset: "microarray_normalised", platform: "microarray"], + microarray_normalised_file + ] ) - normalisation_method = "edger" - input[0] = ch_datasets - input[1] = normalisation_method - input[2] = "uniform" + input[0] = "solanum_tuberosum" + input[1] = ch_datasets + input[2] = "cpm" + input[3] = "uniform" """ } } @@ -73,13 +85,16 @@ nextflow_workflow { """ microarray_normalised_file = file( '$projectDir/tests/test_data/input_datasets/microarray.normalised.csv', checkIfExists: true ) microarray_normalised_design_file = file( '$projectDir/tests/test_data/input_datasets/microarray.normalised.design.csv', checkIfExists: true ) - ch_datasets = Channel.of( - [ [normalised: true, design: microarray_normalised_design_file, dataset: "microarray_normalised", platform: "microarray"], microarray_normalised_file ] + ch_datasets = channel.of( + [ + [normalised: true, design: microarray_normalised_design_file, dataset: "microarray_normalised", platform: "microarray"], + microarray_normalised_file + ] ) - normalisation_method = "deseq2" - input[0] = ch_datasets - input[1] = normalisation_method - input[2] = "uniform" + input[0] = "solanum_tuberosum" + input[1] = ch_datasets + input[2] = "tpm " + input[3] = "uniform" """ } } diff --git a/tests/subworkflows/local/expression_normalisation/main.nf.test.snap b/tests/subworkflows/local/expression_normalisation/main.nf.test.snap index e2ced8f8..0ebef24f 100644 --- a/tests/subworkflows/local/expression_normalisation/main.nf.test.snap +++ b/tests/subworkflows/local/expression_normalisation/main.nf.test.snap @@ -1,8 +1,17 @@ { - "No rnaseq normalisation": { + "CPM Normalisation": { "content": [ { "0": [ + [ + { + "normalised": false, + "design": "rnaseq.raw.design.csv:md5,39470b02a211aff791f9e4851b017488", + "dataset": "rnaseq_raw", + "platform": "rnaseq" + }, + "rnaseq.raw.cpm.quant_norm.parquet:md5,a9143f3a40dfb90d839c44af73d16f44" + ], [ { "normalised": true, @@ -10,10 +19,19 @@ "dataset": "microarray_normalised", "platform": "microarray" }, - "microarray.normalised.quant_norm.parquet:md5,eabdc05374b0c21ebcfd83b01efcedd4" + "microarray.normalised.quant_norm.parquet:md5,b32fba01e39c2e54458f3b6d1b5fafc1" ] ], - "normalised_counts": [ + "counts": [ + [ + { + "normalised": false, + "design": "rnaseq.raw.design.csv:md5,39470b02a211aff791f9e4851b017488", + "dataset": "rnaseq_raw", + "platform": "rnaseq" + }, + "rnaseq.raw.cpm.quant_norm.parquet:md5,a9143f3a40dfb90d839c44af73d16f44" + ], [ { "normalised": true, @@ -21,7 +39,7 @@ "dataset": "microarray_normalised", "platform": "microarray" }, - "microarray.normalised.quant_norm.parquet:md5,eabdc05374b0c21ebcfd83b01efcedd4" + "microarray.normalised.quant_norm.parquet:md5,b32fba01e39c2e54458f3b6d1b5fafc1" ] ] } @@ -30,21 +48,12 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-10-18T11:21:43.607895608" + "timestamp": "2025-12-03T18:31:21.419202123" }, - "EdgeR Normalisation": { + "No rnaseq normalisation": { "content": [ { "0": [ - [ - { - "normalised": false, - "design": "rnaseq.raw.design.csv:md5,39470b02a211aff791f9e4851b017488", - "dataset": "rnaseq_raw", - "platform": "rnaseq" - }, - "rnaseq.raw.cpm.quant_norm.parquet:md5,5c9765f2ffbc78fb90b5591a28e68fe4" - ], [ { "normalised": true, @@ -52,19 +61,10 @@ "dataset": "microarray_normalised", "platform": "microarray" }, - "microarray.normalised.quant_norm.parquet:md5,eabdc05374b0c21ebcfd83b01efcedd4" + "microarray.normalised.quant_norm.parquet:md5,b32fba01e39c2e54458f3b6d1b5fafc1" ] ], - "normalised_counts": [ - [ - { - "normalised": false, - "design": "rnaseq.raw.design.csv:md5,39470b02a211aff791f9e4851b017488", - "dataset": "rnaseq_raw", - "platform": "rnaseq" - }, - "rnaseq.raw.cpm.quant_norm.parquet:md5,5c9765f2ffbc78fb90b5591a28e68fe4" - ], + "counts": [ [ { "normalised": true, @@ -72,7 +72,7 @@ "dataset": "microarray_normalised", "platform": "microarray" }, - "microarray.normalised.quant_norm.parquet:md5,eabdc05374b0c21ebcfd83b01efcedd4" + "microarray.normalised.quant_norm.parquet:md5,b32fba01e39c2e54458f3b6d1b5fafc1" ] ] } @@ -81,9 +81,9 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-10-18T11:21:31.647845526" + "timestamp": "2025-12-03T18:34:48.109732612" }, - "DESeq2 Normalisation": { + "TPM Normalisation": { "content": [ { "0": [ @@ -94,7 +94,7 @@ "dataset": "rnaseq_raw", "platform": "rnaseq" }, - "rnaseq.raw.cpm.quant_norm.parquet:md5,5c9765f2ffbc78fb90b5591a28e68fe4" + "rnaseq.raw.tpm.quant_norm.parquet:md5,0a12a61db30db8c70e609270d32bc33d" ], [ { @@ -103,10 +103,10 @@ "dataset": "microarray_normalised", "platform": "microarray" }, - "microarray.normalised.quant_norm.parquet:md5,eabdc05374b0c21ebcfd83b01efcedd4" + "microarray.normalised.quant_norm.parquet:md5,b32fba01e39c2e54458f3b6d1b5fafc1" ] ], - "normalised_counts": [ + "counts": [ [ { "normalised": false, @@ -114,7 +114,7 @@ "dataset": "rnaseq_raw", "platform": "rnaseq" }, - "rnaseq.raw.cpm.quant_norm.parquet:md5,5c9765f2ffbc78fb90b5591a28e68fe4" + "rnaseq.raw.tpm.quant_norm.parquet:md5,0a12a61db30db8c70e609270d32bc33d" ], [ { @@ -123,7 +123,7 @@ "dataset": "microarray_normalised", "platform": "microarray" }, - "microarray.normalised.quant_norm.parquet:md5,eabdc05374b0c21ebcfd83b01efcedd4" + "microarray.normalised.quant_norm.parquet:md5,b32fba01e39c2e54458f3b6d1b5fafc1" ] ] } @@ -132,6 +132,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-10-18T11:21:17.047635315" + "timestamp": "2025-12-03T18:31:12.441977406" } } \ No newline at end of file diff --git a/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test b/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test deleted file mode 100644 index 2ace20e9..00000000 --- a/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test +++ /dev/null @@ -1,127 +0,0 @@ -nextflow_workflow { - - name "Test Workflow EXPRESSIONATLAS_FETCHDATA" - script "subworkflows/local/expressionatlas_fetchdata/main.nf" - workflow "EXPRESSIONATLAS_FETCHDATA" - tag "expressionatlas_fetchdata" - tag "subworkflow" - - test("Accessions provided + keywords") { - - when { - - params { - eatlas_accessions = "E-MTAB-552,E-GEOD-61690 ,E-PROT-138" - eatlas_accessions_file = null - excluded_eatlas_accessions_file = null - keywords = "potato,stress" - skip_fetch_eatlas_accessions = false - accessions_only = false - } - - workflow { - """ - species = 'solanum tuberosum' - input[0] = Channel.value( species.split(' ').join('_') ) - """ - } - } - - then { - assertAll( - { assert workflow.success }, - { assert snapshot(workflow.out).match() } - ) - } - - } - - test("Accessions only") { - - when { - - params { - eatlas_accessions = "E-MTAB-552,E-GEOD-61690 ,E-PROT-138" - eatlas_accessions_file = null - excluded_eatlas_accessions_file = null - keywords = "potato,stress" - skip_fetch_eatlas_accessions = false - accessions_only = true - } - - workflow { - """ - species = 'solanum tuberosum' - input[0] = Channel.value( species.split(' ').join('_') ) - """ - } - } - - then { - assertAll( - { assert workflow.success }, - { assert snapshot(workflow.out).match() } - ) - } - - } - - test("No accesssion + no keywords + multiple dataset species") { - - when { - params { - eatlas_accessions = "" - eatlas_accessions_file = null - excluded_eatlas_accessions_file = null - keywords = "" - skip_fetch_eatlas_accessions = false - accessions_only = false - } - - workflow { - """ - species = 'beta vulgaris' - input[0] = Channel.value( species.split(' ').join('_') ) - """ - } - } - - then { - assertAll( - { assert workflow.success }, - { assert snapshot(workflow.out).match() } - ) - } - - } - - test("Accessions file + Excluded accessions file") { - - when { - params { - eatlas_accessions = "E-MTAB-552,E-GEOD-61690 ,E-PROT-138" - eatlas_accessions_file = null - excluded_eatlas_accessions_file = null - keywords = "" - skip_fetch_eatlas_accessions = false - accessions_only = false - } - - workflow { - """ - species = 'beta vulgaris' - input[0] = Channel.value( species.split(' ').join('_') ) - """ - } - } - - then { - assertAll( - { assert workflow.success }, - { assert snapshot(workflow.out).match() } - ) - } - - } - -} diff --git a/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test.snap b/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test.snap deleted file mode 100644 index 581fe10e..00000000 --- a/tests/subworkflows/local/expressionatlas_fetchdata/main.nf.test.snap +++ /dev/null @@ -1,268 +0,0 @@ -{ - "Accessions file + Excluded accessions file": { - "content": [ - { - "0": [ - [ - { - "dataset": "E_GEOD_61690_rnaseq", - "design": "E_GEOD_61690_rnaseq.design.csv:md5,ba807caf74e0b55f5c3fe23810a89560" - }, - "E_GEOD_61690_rnaseq.rnaseq.raw.counts.csv:md5,2e3f1a125b3d41d622e2d24447620eb3" - ], - [ - { - "dataset": "E_MTAB_552_rnaseq", - "design": "E_MTAB_552_rnaseq.design.csv:md5,b81490696f638e90c1cf14236bb0c08c" - }, - "E_MTAB_552_rnaseq.rnaseq.raw.counts.csv:md5,830f50b60b17b62f9ca2f6a163a2879f" - ], - [ - { - "dataset": "E_MTAB_8187_rnaseq", - "design": "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf" - }, - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1" - ] - ], - "1": [ - "E-GEOD-61690", - "E-MTAB-552", - "E-MTAB-8187" - ], - "accessions": [ - "E-GEOD-61690", - "E-MTAB-552", - "E-MTAB-8187" - ], - "downloaded_datasets": [ - [ - { - "dataset": "E_GEOD_61690_rnaseq", - "design": "E_GEOD_61690_rnaseq.design.csv:md5,ba807caf74e0b55f5c3fe23810a89560" - }, - "E_GEOD_61690_rnaseq.rnaseq.raw.counts.csv:md5,2e3f1a125b3d41d622e2d24447620eb3" - ], - [ - { - "dataset": "E_MTAB_552_rnaseq", - "design": "E_MTAB_552_rnaseq.design.csv:md5,b81490696f638e90c1cf14236bb0c08c" - }, - "E_MTAB_552_rnaseq.rnaseq.raw.counts.csv:md5,830f50b60b17b62f9ca2f6a163a2879f" - ], - [ - { - "dataset": "E_MTAB_8187_rnaseq", - "design": "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf" - }, - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1" - ] - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-12T11:20:51.904507731" - }, - "Accessions only": { - "content": [ - { - "0": [ - - ], - "1": [ - "E-GEOD-61690", - "E-GEOD-61690", - "E-GEOD-77826", - "E-MTAB-4251", - "E-MTAB-4301", - "E-MTAB-5038", - "E-MTAB-5215", - "E-MTAB-552", - "E-MTAB-552", - "E-MTAB-7711" - ], - "accessions": [ - "E-GEOD-61690", - "E-GEOD-61690", - "E-GEOD-77826", - "E-MTAB-4251", - "E-MTAB-4301", - "E-MTAB-5038", - "E-MTAB-5215", - "E-MTAB-552", - "E-MTAB-552", - "E-MTAB-7711" - ], - "downloaded_datasets": [ - - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-10-18T11:11:19.899629439" - }, - "Accessions provided + keywords": { - "content": [ - { - "0": [ - [ - { - "dataset": "E_GEOD_77826_rnaseq", - "design": "E_GEOD_77826_rnaseq.design.csv:md5,5aa61df754aa9c6c107b247c642d2e53" - }, - "E_GEOD_77826_rnaseq.rnaseq.raw.counts.csv:md5,85cea79c602a9924d5a4d6b597ef5530" - ], - [ - { - "dataset": "E_MTAB_4251_rnaseq", - "design": "E_MTAB_4251_rnaseq.design.csv:md5,4f3ef7b76ca6ed1ec3157295bc4a8d84" - }, - "E_MTAB_4251_rnaseq.rnaseq.raw.counts.csv:md5,5cf27be0e00b93d5d431754ba8058687" - ], - [ - { - "dataset": "E_MTAB_4301_rnaseq", - "design": "E_MTAB_4301_rnaseq.design.csv:md5,165eeef7d612c01fd62baae1a3f296ae" - }, - "E_MTAB_4301_rnaseq.rnaseq.raw.counts.csv:md5,1ab49feea238e7b1419937b5037952b5" - ], - [ - { - "dataset": "E_MTAB_5038_rnaseq", - "design": "E_MTAB_5038_rnaseq.design.csv:md5,352ed3163d7deef2be35d899418d5ad4" - }, - "E_MTAB_5038_rnaseq.rnaseq.raw.counts.csv:md5,b4acb3d7c39cdb2bd6cef6c9314c5b2a" - ], - [ - { - "dataset": "E_MTAB_5215_rnaseq", - "design": "E_MTAB_5215_rnaseq.design.csv:md5,2741dcd5b45bacce865db632f626a273" - }, - "E_MTAB_5215_rnaseq.rnaseq.raw.counts.csv:md5,273704bdf762c342271b33958a84d1e7" - ], - [ - { - "dataset": "E_MTAB_7711_rnaseq", - "design": "E_MTAB_7711_rnaseq.design.csv:md5,3e7748b54a0c25c008d9bd2ddbf1bf00" - }, - "E_MTAB_7711_rnaseq.rnaseq.raw.counts.csv:md5,3c02cf432c29d3751c978439539df388" - ] - ], - "1": [ - "E-GEOD-61690", - "E-GEOD-61690", - "E-GEOD-77826", - "E-MTAB-4251", - "E-MTAB-4301", - "E-MTAB-5038", - "E-MTAB-5215", - "E-MTAB-552", - "E-MTAB-552", - "E-MTAB-7711" - ], - "accessions": [ - "E-GEOD-61690", - "E-GEOD-61690", - "E-GEOD-77826", - "E-MTAB-4251", - "E-MTAB-4301", - "E-MTAB-5038", - "E-MTAB-5215", - "E-MTAB-552", - "E-MTAB-552", - "E-MTAB-7711" - ], - "downloaded_datasets": [ - [ - { - "dataset": "E_GEOD_77826_rnaseq", - "design": "E_GEOD_77826_rnaseq.design.csv:md5,5aa61df754aa9c6c107b247c642d2e53" - }, - "E_GEOD_77826_rnaseq.rnaseq.raw.counts.csv:md5,85cea79c602a9924d5a4d6b597ef5530" - ], - [ - { - "dataset": "E_MTAB_4251_rnaseq", - "design": "E_MTAB_4251_rnaseq.design.csv:md5,4f3ef7b76ca6ed1ec3157295bc4a8d84" - }, - "E_MTAB_4251_rnaseq.rnaseq.raw.counts.csv:md5,5cf27be0e00b93d5d431754ba8058687" - ], - [ - { - "dataset": "E_MTAB_4301_rnaseq", - "design": "E_MTAB_4301_rnaseq.design.csv:md5,165eeef7d612c01fd62baae1a3f296ae" - }, - "E_MTAB_4301_rnaseq.rnaseq.raw.counts.csv:md5,1ab49feea238e7b1419937b5037952b5" - ], - [ - { - "dataset": "E_MTAB_5038_rnaseq", - "design": "E_MTAB_5038_rnaseq.design.csv:md5,352ed3163d7deef2be35d899418d5ad4" - }, - "E_MTAB_5038_rnaseq.rnaseq.raw.counts.csv:md5,b4acb3d7c39cdb2bd6cef6c9314c5b2a" - ], - [ - { - "dataset": "E_MTAB_5215_rnaseq", - "design": "E_MTAB_5215_rnaseq.design.csv:md5,2741dcd5b45bacce865db632f626a273" - }, - "E_MTAB_5215_rnaseq.rnaseq.raw.counts.csv:md5,273704bdf762c342271b33958a84d1e7" - ], - [ - { - "dataset": "E_MTAB_7711_rnaseq", - "design": "E_MTAB_7711_rnaseq.design.csv:md5,3e7748b54a0c25c008d9bd2ddbf1bf00" - }, - "E_MTAB_7711_rnaseq.rnaseq.raw.counts.csv:md5,3c02cf432c29d3751c978439539df388" - ] - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-12T11:20:03.290745058" - }, - "No accesssion + no keywords + multiple dataset species": { - "content": [ - { - "0": [ - [ - { - "dataset": "E_MTAB_8187_rnaseq", - "design": "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf" - }, - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1" - ] - ], - "1": [ - "E-MTAB-8187" - ], - "accessions": [ - "E-MTAB-8187" - ], - "downloaded_datasets": [ - [ - { - "dataset": "E_MTAB_8187_rnaseq", - "design": "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf" - }, - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1" - ] - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-12T11:20:34.963087914" - } -} \ No newline at end of file diff --git a/tests/subworkflows/local/genorm/main.nf.test.snap b/tests/subworkflows/local/genorm/main.nf.test.snap index a5208b28..0f1f8714 100644 --- a/tests/subworkflows/local/genorm/main.nf.test.snap +++ b/tests/subworkflows/local/genorm/main.nf.test.snap @@ -3,10 +3,10 @@ "content": [ { "0": [ - "m_measures.csv:md5,2704c9915abdddc31c3de104b1cb1b64" + ], "m_measures": [ - "m_measures.csv:md5,2704c9915abdddc31c3de104b1cb1b64" + ] } ], @@ -14,6 +14,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.8" }, - "timestamp": "2025-11-19T15:30:38.03592559" + "timestamp": "2025-12-03T19:01:05.843822263" } } \ No newline at end of file diff --git a/tests/subworkflows/local/geo_fetchdata/main.nf.test b/tests/subworkflows/local/geo_fetchdata/main.nf.test deleted file mode 100644 index 4d025ded..00000000 --- a/tests/subworkflows/local/geo_fetchdata/main.nf.test +++ /dev/null @@ -1,140 +0,0 @@ -nextflow_workflow { - - name "Test Workflow GEO_FETCHDATA" - script "subworkflows/local/geo_fetchdata/main.nf" - workflow "GEO_FETCHDATA" - tag "geo_fetchdata" - tag "subworkflow" - - test("Simple run") { - - when { - - workflow { - """ - input[0] = "beta_vulgaris" // species - input[1] = false // skip_fetch_geo_accessions - input[2] = false // accessions_only - input[3] = null // platform - input[4] = "" // keywords - input[5] = "" // geo_accessions - input[6] = null // geo_accessions_file - input[7] = "" // excluded_geo_accessions - input[8] = null // excluded_geo_accessions_file - input[9] = Channel.empty() // ch_eatlas_excluded_accessions - input[10] = Channel.value(1) // ch_nb_downloaded_eatlas_datasets - input[11] = 1500 // min_nb_eatlas_datasets_auto_skip_geo - input[12] = "test_output" // outdir - """ - } - } - - then { - assertAll( - { assert workflow.success }, - { assert snapshot(workflow.out).match() } - ) - } - - } - - test("Accesions only") { - - when { - - workflow { - """ - input[0] = "beta_vulgaris" // species - input[1] = false // skip_fetch_geo_accessions - input[2] = true // accessions_only - input[3] = null // platform - input[4] = "" // keywords - input[5] = "" // geo_accessions - input[6] = null // geo_accessions_file - input[7] = "" // excluded_geo_accessions - input[8] = null // excluded_geo_accessions_file - input[9] = Channel.empty() // ch_eatlas_excluded_accessions - input[10] = Channel.value(1) // ch_nb_downloaded_eatlas_datasets - input[11] = 1500 // min_nb_eatlas_datasets_auto_skip_geo - input[12] = "test_output" // outdir - """ - } - } - - then { - assertAll( - { assert workflow.success }, - { assert workflow.out.downloaded_datasets.size() == 0 }, - { assert snapshot(workflow.out).match() } - ) - } - - } - - test("Exclude / force include accessions + platform + keyword") { - - when { - - workflow { - """ - input[0] = "beta_vulgaris" // species - input[1] = false // skip_fetch_geo_accessions - input[2] = false // accessions_only - input[3] = "rnaseq" // platform - input[4] = "leaf" // keywords - input[5] = "GSE55951" // geo_accessions - input[6] = null // geo_accessions_file - input[7] = "GSE79526" // excluded_geo_accessions - input[8] = file( '$projectDir/tests/test_data/geo/get_accessions/exclude_two_accessions.txt', checkIfExists: true ) // excluded_geo_accessions_file - input[9] = Channel.empty() // ch_eatlas_excluded_accessions - input[10] = Channel.value(1) // ch_nb_downloaded_eatlas_datasets - input[11] = 1500 // min_nb_eatlas_datasets_auto_skip_geo - input[12] = "test_output" // outdir - """ - } - } - - then { - assertAll( - { assert workflow.success }, - { assert workflow.out.downloaded_datasets.size() == 1 }, - { assert snapshot(workflow.out).match() } - ) - } - - } - - test("Number of Eatlas datasets exceeded threshold + force download one accession") { - tag "geo_fetchdata_over_threshold" - when { - - workflow { - """ - input[0] = "beta_vulgaris" // species - input[1] = false // skip_fetch_geo_accessions - input[2] = false // accessions_only - input[3] = null // platform - input[4] = "" // keywords - input[5] = "GSE55951" // geo_accessions - input[6] = null // geo_accessions_file - input[7] = "" // excluded_geo_accessions - input[8] = null // excluded_geo_accessions_file - input[9] = Channel.empty() // ch_eatlas_excluded_accessions - input[10] = Channel.value(10) // ch_nb_downloaded_eatlas_datasets - input[11] = 10 // min_nb_eatlas_datasets_auto_skip_geo - input[12] = "test_output" // outdir - """ - } - } - - then { - assertAll( - { assert workflow.success }, - { assert workflow.out.downloaded_datasets.size() == 1 }, - { assert snapshot(workflow.out).match() } - ) - } - - } - -} diff --git a/tests/subworkflows/local/geo_fetchdata/main.nf.test.snap b/tests/subworkflows/local/geo_fetchdata/main.nf.test.snap deleted file mode 100644 index 9fea4ed0..00000000 --- a/tests/subworkflows/local/geo_fetchdata/main.nf.test.snap +++ /dev/null @@ -1,148 +0,0 @@ -{ - "Exclude / force include accessions + platform + keyword": { - "content": [ - { - "0": [ - [ - { - "dataset": "GSE55951_GPL18429", - "design": "GSE55951_GPL18429.microarray.normalised.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d" - }, - "GSE55951_GPL18429.microarray.normalised.counts.csv:md5,18fd2d728ad2ec5cb78f994f73375144" - ] - ], - "downloaded_datasets": [ - [ - { - "dataset": "GSE55951_GPL18429", - "design": "GSE55951_GPL18429.microarray.normalised.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d" - }, - "GSE55951_GPL18429.microarray.normalised.counts.csv:md5,18fd2d728ad2ec5cb78f994f73375144" - ] - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-19T19:57:27.31062531" - }, - "Number of Eatlas datasets exceeded threshold + one accession included": { - "content": [ - { - "0": [ - - ], - "downloaded_datasets": [ - - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-19T19:45:10.916816453" - }, - "Number of Eatlas datasets exceeded threshold": { - "content": [ - { - "0": [ - [ - { - "dataset": "GSE55951_GPL18429", - "design": "GSE55951_GPL18429.microarray.normalised.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d", - "normalised": true, - "platform": "microarray" - }, - "GSE55951_GPL18429.microarray.normalised.counts.csv:md5,18fd2d728ad2ec5cb78f994f73375144" - ] - ], - "downloaded_datasets": [ - [ - { - "dataset": "GSE55951_GPL18429", - "design": "GSE55951_GPL18429.microarray.normalised.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d", - "normalised": true, - "platform": "microarray" - }, - "GSE55951_GPL18429.microarray.normalised.counts.csv:md5,18fd2d728ad2ec5cb78f994f73375144" - ] - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-19T20:24:45.768801518" - }, - "Simple run": { - "content": [ - { - "0": [ - [ - { - "dataset": "GSE55951_GPL18429", - "design": "GSE55951_GPL18429.microarray.normalised.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d", - "normalised": true, - "platform": "microarray" - }, - "GSE55951_GPL18429.microarray.normalised.counts.csv:md5,18fd2d728ad2ec5cb78f994f73375144" - ] - ], - "downloaded_datasets": [ - [ - { - "dataset": "GSE55951_GPL18429", - "design": "GSE55951_GPL18429.microarray.normalised.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d", - "normalised": true, - "platform": "microarray" - }, - "GSE55951_GPL18429.microarray.normalised.counts.csv:md5,18fd2d728ad2ec5cb78f994f73375144" - ] - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-19T19:47:33.406202342" - }, - "Accesions only": { - "content": [ - { - "0": [ - - ], - "downloaded_datasets": [ - - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-19T19:23:32.005374433" - }, - "Exclude / force include accessions": { - "content": [ - { - "0": [ - - ], - "downloaded_datasets": [ - - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-19T19:23:59.957750144" - } -} \ No newline at end of file diff --git a/tests/subworkflows/local/get_public_accessions/main.nf.test b/tests/subworkflows/local/get_public_accessions/main.nf.test new file mode 100644 index 00000000..08220066 --- /dev/null +++ b/tests/subworkflows/local/get_public_accessions/main.nf.test @@ -0,0 +1,200 @@ +nextflow_workflow { + + name "Test Workflow GET_PUBLIC_ACCESSIONS" + script "subworkflows/local/get_public_accessions/main.nf" + workflow "GET_PUBLIC_ACCESSIONS" + + + test("Fetch public accessions without keywords") { + + when { + + params { + species = 'beta vulgaris' + skip_fetch_public_accessions = false + skip_fetch_eatlas_accessions = false + skip_fetch_geo_accessions = false + platform = null + keywords = "" + accessions = "" + accessions_file = null + excluded_accessions = "" + excluded_accessions_file = null + random_sampling_size = null + random_sampling_seed = 42 + outdir = "$outputDir" + } + + workflow { + """ + input[0] = channel.value( params.species.split(' ').join('_') ) + input[1] = params.skip_fetch_public_accessions + input[2] = params.skip_fetch_eatlas_accessions + input[3] = params.skip_fetch_geo_accessions + input[4] = params.platform + input[5] = params.keywords + input[6] = channel.fromList( params.accessions.tokenize(',') ) + input[7] = params.accessions_file ? channel.fromPath(params.accessions_file, checkIfExists: true) : channel.empty() + input[8] = channel.fromList( params.excluded_accessions.tokenize(',') ) + input[9] = params.excluded_accessions_file ? channel.fromPath(params.excluded_accessions_file, checkIfExists: true) : channel.empty() + input[10] = params.random_sampling_size + input[11] = params.random_sampling_seed + input[12] = params.outdir + """ + } + } + + then { + assertAll( + { assert workflow.success }, + { assert snapshot(workflow.out).match() } + ) + } + + } + + test("Fetch public accessions with keywords") { + + when { + + params { + species = 'beta vulgaris' + skip_fetch_public_accessions = false + skip_fetch_eatlas_accessions = false + skip_fetch_geo_accessions = false + platform = null + keywords = "leaf" + accessions = "" + accessions_file = null + excluded_accessions = "" + excluded_accessions_file = null + random_sampling_size = null + random_sampling_seed = 42 + outdir = "$outputDir" + } + + workflow { + """ + input[0] = channel.value( params.species.split(' ').join('_') ) + input[1] = params.skip_fetch_public_accessions + input[2] = params.skip_fetch_eatlas_accessions + input[3] = params.skip_fetch_geo_accessions + input[4] = params.platform + input[5] = params.keywords + input[6] = channel.fromList( params.accessions.tokenize(',') ) + input[7] = params.accessions_file ? channel.fromPath(params.accessions_file, checkIfExists: true) : channel.empty() + input[8] = channel.fromList( params.excluded_accessions.tokenize(',') ) + input[9] = params.excluded_accessions_file ? channel.fromPath(params.excluded_accessions_file, checkIfExists: true) : channel.empty() + input[10] = params.random_sampling_size + input[11] = params.random_sampling_seed + input[12] = params.outdir + """ + } + } + + then { + assertAll( + { assert workflow.success }, + { assert snapshot(workflow.out).match() } + ) + } + + } + + test("No GEO + accessions provided") { + + when { + + params { + species = 'beta vulgaris' + skip_fetch_public_accessions = false + skip_fetch_eatlas_accessions = false + skip_fetch_geo_accessions = true + platform = null + keywords = "" + accessions = "E-MTAB-552,E-GEOD-61690 ,E-PROT-138" + accessions_file = null + excluded_accessions = "" + excluded_accessions_file = null + random_sampling_size = null + random_sampling_seed = 42 + outdir = "$outputDir" + } + + workflow { + """ + input[0] = channel.value( params.species.split(' ').join('_') ) + input[1] = params.skip_fetch_public_accessions + input[2] = params.skip_fetch_eatlas_accessions + input[3] = params.skip_fetch_geo_accessions + input[4] = params.platform + input[5] = params.keywords + input[6] = channel.fromList( params.accessions.tokenize(',') ) + input[7] = params.accessions_file ? channel.fromPath(params.accessions_file, checkIfExists: true) : channel.empty() + input[8] = channel.fromList( params.excluded_accessions.tokenize(',') ) + input[9] = params.excluded_accessions_file ? channel.fromPath(params.excluded_accessions_file, checkIfExists: true) : channel.empty() + input[10] = params.random_sampling_size + input[11] = params.random_sampling_seed + input[12] = params.outdir + """ + } + } + + then { + assertAll( + { assert workflow.success }, + { assert snapshot(workflow.out).match() } + ) + } + + } + + test("Accessions file + Excluded accessions file") { + + when { + params { + species = 'beta vulgaris' + skip_fetch_public_accessions = false + skip_fetch_eatlas_accessions = false + skip_fetch_geo_accessions = true + platform = null + keywords = "" + accessions = "E-MTAB-552,E-GEOD-61690 ,E-PROT-138" + accessions_file = null + excluded_accessions = "" + excluded_accessions_file = file( '$projectDir/tests/test_data/public_accessions/exclude_one_two_accessions.txt', checkIfExists: true ) + random_sampling_size = null + random_sampling_seed = 42 + outdir = "$outputDir" + } + + workflow { + """ + input[0] = channel.value( params.species.split(' ').join('_') ) + input[1] = params.skip_fetch_public_accessions + input[2] = params.skip_fetch_eatlas_accessions + input[3] = params.skip_fetch_geo_accessions + input[4] = params.platform + input[5] = params.keywords + input[6] = channel.fromList( params.accessions.tokenize(',') ) + input[7] = params.accessions_file ? channel.fromPath(params.accessions_file, checkIfExists: true) : channel.empty() + input[8] = channel.fromList( params.excluded_accessions.tokenize(',') ) + input[9] = params.excluded_accessions_file ? channel.fromPath(params.excluded_accessions_file, checkIfExists: true) : channel.empty() + input[10] = params.random_sampling_size + input[11] = params.random_sampling_seed + input[12] = params.outdir + """ + } + } + + then { + assertAll( + { assert workflow.success }, + { assert snapshot(workflow.out).match() } + ) + } + + } + + +} diff --git a/tests/subworkflows/local/get_public_accessions/main.nf.test.snap b/tests/subworkflows/local/get_public_accessions/main.nf.test.snap new file mode 100644 index 00000000..e6970aff --- /dev/null +++ b/tests/subworkflows/local/get_public_accessions/main.nf.test.snap @@ -0,0 +1,110 @@ +{ + "Accessions file + Excluded accessions file": { + "content": [ + { + "0": [ + "E-GEOD-61690", + "E-MTAB-552", + "E-MTAB-8187", + "E-PROT-138" + ], + "accessions": [ + "E-GEOD-61690", + "E-MTAB-552", + "E-MTAB-8187", + "E-PROT-138" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-12-04T10:38:22.46774778" + }, + "No GEO + accessions provided": { + "content": [ + { + "0": [ + "E-GEOD-61690", + "E-MTAB-552", + "E-MTAB-8187", + "E-PROT-138" + ], + "accessions": [ + "E-GEOD-61690", + "E-MTAB-552", + "E-MTAB-8187", + "E-PROT-138" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-12-04T10:38:15.084508905" + }, + "Fetch public accessions without keywords": { + "content": [ + { + "0": [ + "E-MTAB-8187", + "GSE107627", + "GSE114968", + "GSE135555", + "GSE205413", + "GSE269454", + "GSE281272", + "GSE55951", + "GSE79526", + "GSE92859" + ], + "accessions": [ + "E-MTAB-8187", + "GSE107627", + "GSE114968", + "GSE135555", + "GSE205413", + "GSE269454", + "GSE281272", + "GSE55951", + "GSE79526", + "GSE92859" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-12-04T10:37:50.665649081" + }, + "Fetch public accessions with keywords": { + "content": [ + { + "0": [ + "E-MTAB-8187", + "GSE107627", + "GSE114968", + "GSE269454", + "GSE281272", + "GSE79526" + ], + "accessions": [ + "E-MTAB-8187", + "GSE107627", + "GSE114968", + "GSE269454", + "GSE281272", + "GSE79526" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.8" + }, + "timestamp": "2025-12-04T10:38:07.290638456" + } +} \ No newline at end of file diff --git a/tests/test_data/normalisation/base/gene_lengths.csv b/tests/test_data/normalisation/base/gene_lengths.csv new file mode 100644 index 00000000..67b05cee --- /dev/null +++ b/tests/test_data/normalisation/base/gene_lengths.csv @@ -0,0 +1,13 @@ +gene_id,length +ENSRNA549434199,100 +ENSRNA549434200,200 +ENSRNA549434201,300 +ENSRNA549434202,400 +ENSRNA549434203,500 +ENSRNA549434204,600 +ENSRNA549434205,700 +ENSRNA549434206,800 +ENSRNA549434207,900 +ENSRNA549434208,1000 +ENSRNA549434209,1100 +ENSRNA549434210,1200 diff --git a/tests/test_data/normalisation/many_zeros/gene_lengths.csv b/tests/test_data/normalisation/many_zeros/gene_lengths.csv new file mode 100644 index 00000000..923e2d65 --- /dev/null +++ b/tests/test_data/normalisation/many_zeros/gene_lengths.csv @@ -0,0 +1,6 @@ +gene_id,length +AT1G80990,100 +AT2G01008,200 +AT2G01010,300 +AT2G01020,400 +AT2G01021,500 diff --git a/tests/test_data/normalisation/one_group/gene_lengths.csv b/tests/test_data/normalisation/one_group/gene_lengths.csv new file mode 100644 index 00000000..73eb9655 --- /dev/null +++ b/tests/test_data/normalisation/one_group/gene_lengths.csv @@ -0,0 +1,6 @@ +gene_id,length +ENSG00000000003,100 +ENSG00000000005,200 +ENSG00000000419,300 +ENSG00000000457,400 +ENSG00000000460,500 diff --git a/tests/test_data/geo/get_accessions/exclude_one_accession.txt b/tests/test_data/public_accessions/exclude_one_geo_accession.txt similarity index 100% rename from tests/test_data/geo/get_accessions/exclude_one_accession.txt rename to tests/test_data/public_accessions/exclude_one_geo_accession.txt diff --git a/tests/test_data/geo/get_accessions/exclude_two_accessions.txt b/tests/test_data/public_accessions/exclude_two_geo_accessions.txt similarity index 100% rename from tests/test_data/geo/get_accessions/exclude_two_accessions.txt rename to tests/test_data/public_accessions/exclude_two_geo_accessions.txt From 8f4fe1b261573bac6c41fbadecc04ebf6914c90c Mon Sep 17 00:00:00 2001 From: Olivier Date: Thu, 4 Dec 2025 13:08:47 +0100 Subject: [PATCH 215/258] replace "Channel" by "channel" to adapt to new synthax --- .../local/download_public_datasets/main.nf | 4 +- .../local/get_public_accessions/main.nf | 4 +- subworkflows/local/idmapping/main.nf | 8 ++-- subworkflows/local/multiqc/main.nf | 46 +++++++++---------- subworkflows/local/stability_scoring/main.nf | 2 +- .../main.nf | 4 +- .../genorm/compute_m_measure/main.nf.test | 4 +- .../local/genorm/make_chunks/main.nf.test | 2 +- .../local/rename_gene_ids/main.nf.test | 4 +- tests/subworkflows/local/genorm/main.nf.test | 4 +- tests/workflows/stableexpression.nf.test | 14 +++--- workflows/stableexpression.nf | 22 ++++----- 12 files changed, 59 insertions(+), 59 deletions(-) diff --git a/subworkflows/local/download_public_datasets/main.nf b/subworkflows/local/download_public_datasets/main.nf index 9008a0ef..d0b1cfe5 100644 --- a/subworkflows/local/download_public_datasets/main.nf +++ b/subworkflows/local/download_public_datasets/main.nf @@ -20,8 +20,8 @@ workflow DOWNLOAD_PUBLIC_DATASETS { main: - ch_datasets = Channel.empty() - ch_fetched_accessions = Channel.empty() + ch_datasets = channel.empty() + ch_fetched_accessions = channel.empty() ch_accessions = ch_accessions .branch { acc -> diff --git a/subworkflows/local/get_public_accessions/main.nf b/subworkflows/local/get_public_accessions/main.nf index e00a6d19..95ff61e5 100644 --- a/subworkflows/local/get_public_accessions/main.nf +++ b/subworkflows/local/get_public_accessions/main.nf @@ -26,8 +26,8 @@ workflow GET_PUBLIC_ACCESSIONS { main: - ch_fetched_eatlas_accessions = Channel.empty() - ch_fetched_geo_accessions = Channel.empty() + ch_fetched_eatlas_accessions = channel.empty() + ch_fetched_geo_accessions = channel.empty() // ----------------------------------------------------------------- // GET EATLAS ACCESSIONS diff --git a/subworkflows/local/idmapping/main.nf b/subworkflows/local/idmapping/main.nf index 8a99ecdc..94ad9bd0 100644 --- a/subworkflows/local/idmapping/main.nf +++ b/subworkflows/local/idmapping/main.nf @@ -22,8 +22,8 @@ workflow ID_MAPPING { main: - ch_gene_id_mapping = Channel.empty() - ch_gene_metadata = Channel.empty() + ch_gene_id_mapping = channel.empty() + ch_gene_metadata = channel.empty() if ( !skip_id_mapping ) { @@ -75,7 +75,7 @@ workflow ID_MAPPING { // ----------------------------------------------------------------- ch_gene_id_mapping - .mix( custom_gene_id_mapping ? Channel.fromPath( custom_gene_id_mapping, checkIfExists: true ) : Channel.empty() ) + .mix( custom_gene_id_mapping ? channel.fromPath( custom_gene_id_mapping, checkIfExists: true ) : channel.empty() ) .splitCsv( header: true ) .unique() .collectFile( @@ -89,7 +89,7 @@ workflow ID_MAPPING { .set { ch_global_gene_id_mapping } ch_gene_metadata - .mix( custom_gene_metadata ? Channel.fromPath( custom_gene_metadata, checkIfExists: true ) : Channel.empty() ) + .mix( custom_gene_metadata ? channel.fromPath( custom_gene_metadata, checkIfExists: true ) : channel.empty() ) .splitCsv( header: true ) .unique() .collectFile( diff --git a/subworkflows/local/multiqc/main.nf b/subworkflows/local/multiqc/main.nf index ebe5ae36..9ac13088 100644 --- a/subworkflows/local/multiqc/main.nf +++ b/subworkflows/local/multiqc/main.nf @@ -25,7 +25,7 @@ workflow MULTIQC_WORKFLOW { // STATS // ------------------------------------------------------------------------------------ - Channel.topic('id_mapping_stats') + channel.topic('id_mapping_stats') .collectFile( name: 'id_mapping_stats.csv', seed: "Dataset,Nb mapped,Nb unmapped", @@ -36,7 +36,7 @@ workflow MULTIQC_WORKFLOW { } .set { ch_id_mapping_stats } - Channel.topic('skewness') + channel.topic('skewness') .map { dataset, file -> "${dataset},${file.readLines()[0]}" } // concatenate dataset name with skewness values .collectFile( name: 'skewness.csv', @@ -45,7 +45,7 @@ workflow MULTIQC_WORKFLOW { ) .set { ch_skewness } - Channel.topic('ratio_zeros') + channel.topic('ratio_zeros') .map { dataset, file -> "${dataset},${file.readLines()[0]}" } // concatenate dataset name with skewness values .collectFile( name: 'ratio_zeros.csv', @@ -61,7 +61,7 @@ workflow MULTIQC_WORKFLOW { // FAILURE / WARNING REPORTS // ------------------------------------------------------------------------------------ - Channel.topic('eatlas_failure_reason') + channel.topic('eatlas_failure_reason') .map { accession, file -> [ accession, file.readLines()[0] ] } .collectFile( name: 'eatlas_failure_reasons.csv', @@ -73,7 +73,7 @@ workflow MULTIQC_WORKFLOW { } .set { ch_eatlas_failure_reasons } - Channel.topic('eatlas_warning_reason') + channel.topic('eatlas_warning_reason') .map { accession, file -> [ accession, file.readLines()[0] ] } .collectFile( name: 'eatlas_warning_reasons.csv', @@ -85,7 +85,7 @@ workflow MULTIQC_WORKFLOW { } .set { ch_eatlas_warning_reasons } - Channel.topic('geo_failure_reason') + channel.topic('geo_failure_reason') .map { accession, file -> [ accession, file.readLines()[0] ] } .collectFile( name: 'geo_failure_reasons.csv', @@ -97,7 +97,7 @@ workflow MULTIQC_WORKFLOW { } .set { ch_geo_failure_reasons } - Channel.topic('geo_warning_reason') + channel.topic('geo_warning_reason') .map { accession, file -> [ accession, file.readLines()[0] ] } .collectFile( name: 'geo_warning_reasons.csv', @@ -109,7 +109,7 @@ workflow MULTIQC_WORKFLOW { } .set { ch_geo_warning_reasons } - Channel.topic('id_cleaning_failure_reason') + channel.topic('id_cleaning_failure_reason') .map { dataset, file -> [ dataset, file.readLines()[0] ] } .collectFile( name: 'id_cleaning_failure_reasons.tsv', @@ -121,7 +121,7 @@ workflow MULTIQC_WORKFLOW { } .set { ch_id_cleaning_failure_reasons } - Channel.topic('renaming_warning_reason') + channel.topic('renaming_warning_reason') .map { dataset, file -> [ dataset, file.readLines()[0] ] } .collectFile( name: 'renaming_warning_reasons.tsv', @@ -133,7 +133,7 @@ workflow MULTIQC_WORKFLOW { } .set { ch_id_mapping_warning_reasons } - Channel.topic('renaming_failure_reason') + channel.topic('renaming_failure_reason') .map { dataset, file -> [ dataset, file.readLines()[0] ] } .collectFile( name: 'renaming_failure_reasons.tsv', @@ -145,7 +145,7 @@ workflow MULTIQC_WORKFLOW { } .set { ch_id_mapping_failure_reasons } - Channel.topic('normalisation_warning_reason') + channel.topic('normalisation_warning_reason') .map { dataset, file -> [ dataset, file.readLines()[0] ] } .collectFile( name: 'normalisation_warning_reasons.tsv', @@ -157,7 +157,7 @@ workflow MULTIQC_WORKFLOW { } .set { ch_normalisation_warning_reasons } - Channel.topic('normalisation_failure_reason') + channel.topic('normalisation_failure_reason') .map { dataset, file -> [ dataset, file.readLines()[0] ] } .collectFile( name: 'normalisation_failure_reasons.tsv', @@ -175,11 +175,11 @@ workflow MULTIQC_WORKFLOW { // ------------------------------------------------------------------------------------ ch_multiqc_files - .mix( Channel.topic('eatlas_all_datasets').collect() ) - .mix( Channel.topic('eatlas_selected_datasets').collect() ) - .mix( Channel.topic('geo_all_datasets').collect() ) - .mix( Channel.topic('geo_selected_datasets').collect() ) - .mix( Channel.topic('geo_rejected_datasets').collect() ) + .mix( channel.topic('eatlas_all_datasets').collect() ) + .mix( channel.topic('eatlas_selected_datasets').collect() ) + .mix( channel.topic('geo_all_datasets').collect() ) + .mix( channel.topic('geo_selected_datasets').collect() ) + .mix( channel.topic('geo_rejected_datasets').collect() ) .mix( COLLECT_STATISTICS.out.csv ) .mix( ch_id_mapping_stats ) .mix( ch_eatlas_failure_reasons ) @@ -201,7 +201,7 @@ workflow MULTIQC_WORKFLOW { // TODO: use the nf-core functions when they are adapted to channel topics // Collate and save software versions - formatVersionsToYAML ( Channel.topic('versions') ) + formatVersionsToYAML ( channel.topic('versions') ) .mix ( softwareVersionsToYAML( ch_versions ) ) // mix with versions obtained from emit outputs .collectFile(storeDir: "${params.outdir}/pipeline_info", name: 'software_mqc_versions.yml', sort: true, newLine: true) .set { ch_collated_versions } @@ -212,13 +212,13 @@ workflow MULTIQC_WORKFLOW { // ------------------------------------------------------------------------------------ summary_params = paramsSummaryMap( workflow, parameters_schema: "nextflow_schema.json") - ch_workflow_summary = Channel.value(paramsSummaryMultiqc(summary_params)) + ch_workflow_summary = channel.value(paramsSummaryMultiqc(summary_params)) 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) - Channel.value( methodsDescriptionText( ch_multiqc_custom_methods_description ) ) + channel.value( methodsDescriptionText( ch_multiqc_custom_methods_description ) ) .collectFile( name: 'methods_description_mqc.yaml', sort: true @@ -231,9 +231,9 @@ workflow MULTIQC_WORKFLOW { .mix( ch_methods_description_file ) .set { ch_multiqc_files } - 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() + 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() MULTIQC ( ch_multiqc_files.collect(), diff --git a/subworkflows/local/stability_scoring/main.nf b/subworkflows/local/stability_scoring/main.nf index 08d9c439..211f54a4 100644 --- a/subworkflows/local/stability_scoring/main.nf +++ b/subworkflows/local/stability_scoring/main.nf @@ -54,7 +54,7 @@ workflow STABILITY_SCORING { GENORM ( ch_candidate_gene_counts ) GENORM.out.m_measures.set { ch_genorm_stability } } else { - ch_genorm_stability = Channel.value([]) + ch_genorm_stability = channel.value([]) } // ----------------------------------------------------------------- diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index 212e7e57..1feb3c7c 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -103,7 +103,7 @@ workflow PIPELINE_INITIALISATION { ch_input_datasets = parseInputDatasets( params.datasets ) validateInputSamplesheet( ch_input_datasets ) } else { - ch_input_datasets = Channel.empty() + ch_input_datasets = channel.empty() } emit: @@ -217,7 +217,7 @@ def validateInputParameters(params) { // Parses files from input dataset and creates two subchannels raw and normalized // with elements like [meta, count_file, normalised] def parseInputDatasets(samplesheet) { - return Channel.fromList( samplesheetToList(samplesheet, "assets/schema_datasets.json") ) + return channel.fromList( samplesheetToList(samplesheet, "assets/schema_datasets.json") ) .map { item -> def (meta, count_file) = item diff --git a/tests/modules/local/genorm/compute_m_measure/main.nf.test b/tests/modules/local/genorm/compute_m_measure/main.nf.test index 0e77138b..bbff428e 100644 --- a/tests/modules/local/genorm/compute_m_measure/main.nf.test +++ b/tests/modules/local/genorm/compute_m_measure/main.nf.test @@ -10,8 +10,8 @@ nextflow_process { when { process { """ - ch_count_file = Channel.fromPath( '$projectDir/tests/test_data/genorm/make_chunks/input/counts.parquet', checkIfExists: true) - ch_ratio_std_files = Channel.fromPath( '$projectDir/tests/test_data/genorm/compute_m_measure/input/std.*.parquet', checkIfExists: true).collect() + ch_count_file = channel.fromPath( '$projectDir/tests/test_data/genorm/make_chunks/input/counts.parquet', checkIfExists: true) + ch_ratio_std_files = channel.fromPath( '$projectDir/tests/test_data/genorm/compute_m_measure/input/std.*.parquet', checkIfExists: true).collect() input[0] = ch_count_file input[1] = ch_ratio_std_files """ diff --git a/tests/modules/local/genorm/make_chunks/main.nf.test b/tests/modules/local/genorm/make_chunks/main.nf.test index d43bcea9..bb6c00ba 100644 --- a/tests/modules/local/genorm/make_chunks/main.nf.test +++ b/tests/modules/local/genorm/make_chunks/main.nf.test @@ -10,7 +10,7 @@ nextflow_process { when { process { """ - ch_counts = Channel.fromPath( '$projectDir/tests/test_data/genorm/make_chunks/input/counts.parquet', checkIfExists: true) + ch_counts = channel.fromPath( '$projectDir/tests/test_data/genorm/make_chunks/input/counts.parquet', checkIfExists: true) input[0] = ch_counts """ } diff --git a/tests/modules/local/rename_gene_ids/main.nf.test b/tests/modules/local/rename_gene_ids/main.nf.test index 51eff439..5aa88f67 100644 --- a/tests/modules/local/rename_gene_ids/main.nf.test +++ b/tests/modules/local/rename_gene_ids/main.nf.test @@ -10,7 +10,7 @@ nextflow_process { when { process { """ - input[0] = Channel.of( + input[0] = channel.of( [ [ dataset: "test" ], file("$projectDir/tests/test_data/idmapping/base/counts.ensembl_ids.csv", checkIfExists: true) @@ -37,7 +37,7 @@ nextflow_process { when { process { """ - input[0] = Channel.of( + input[0] = channel.of( [ [ dataset: "test" ], file("$projectDir/tests/test_data/idmapping/tsv/counts.ensembl_ids.tsv", checkIfExists: true) diff --git a/tests/subworkflows/local/genorm/main.nf.test b/tests/subworkflows/local/genorm/main.nf.test index 9ceee1c4..1b44b4a7 100644 --- a/tests/subworkflows/local/genorm/main.nf.test +++ b/tests/subworkflows/local/genorm/main.nf.test @@ -13,7 +13,7 @@ nextflow_workflow { when { workflow { """ - ch_counts = Channel.fromPath( '$projectDir/tests/test_data/genorm/make_chunks/input/counts.head.parquet', checkIfExists: true) + ch_counts = channel.fromPath( '$projectDir/tests/test_data/genorm/make_chunks/input/counts.head.parquet', checkIfExists: true) input[0] = ch_counts """ } @@ -33,7 +33,7 @@ nextflow_workflow { when { workflow { """ - ch_counts = Channel.fromPath( '$projectDir/tests/test_data/genorm/make_chunks/input/counts.parquet', checkIfExists: true) + ch_counts = channel.fromPath( '$projectDir/tests/test_data/genorm/make_chunks/input/counts.parquet', checkIfExists: true) input[0] = ch_counts """ } diff --git a/tests/workflows/stableexpression.nf.test b/tests/workflows/stableexpression.nf.test index 46d4df2a..a7073c16 100644 --- a/tests/workflows/stableexpression.nf.test +++ b/tests/workflows/stableexpression.nf.test @@ -17,7 +17,7 @@ nextflow_workflow { } workflow { """ - input[0] = Channel.empty() + input[0] = channel.empty() """ } } @@ -44,7 +44,7 @@ nextflow_workflow { } workflow { """ - input[0] = Channel.empty() + input[0] = channel.empty() """ } } @@ -71,7 +71,7 @@ nextflow_workflow { } workflow { """ - input[0] = Channel.empty() + input[0] = channel.empty() """ } } @@ -99,7 +99,7 @@ nextflow_workflow { } workflow { """ - input[0] = Channel.empty() + input[0] = channel.empty() """ } } @@ -127,7 +127,7 @@ nextflow_workflow { } workflow { """ - input[0] = Channel.empty() + input[0] = channel.empty() """ } } @@ -155,7 +155,7 @@ nextflow_workflow { } workflow { """ - input[0] = Channel.empty() + input[0] = channel.empty() """ } } @@ -179,7 +179,7 @@ nextflow_workflow { } workflow { """ - input[0] = Channel.empty() + input[0] = channel.empty() """ } } diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 75bf4667..4f199e40 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -34,15 +34,15 @@ workflow STABLEEXPRESSION { main: - ch_accessions = Channel.empty() - ch_downloaded_datasets = Channel.empty() + ch_accessions = channel.empty() + ch_downloaded_datasets = channel.empty() - ch_versions = Channel.empty() - ch_multiqc_files = Channel.empty() + ch_versions = channel.empty() + ch_multiqc_files = channel.empty() - ch_top_stable_genes_summary = Channel.empty() - ch_all_genes_statistics = Channel.empty() - ch_top_stable_genes_transposed_counts = Channel.empty() + ch_top_stable_genes_summary = channel.empty() + ch_all_genes_statistics = channel.empty() + ch_top_stable_genes_transposed_counts = channel.empty() def species = params.species.split(' ').join('_').toLowerCase() @@ -57,10 +57,10 @@ workflow STABLEEXPRESSION { params.skip_fetch_geo_accessions, params.platform, params.keywords, - Channel.fromList( params.accessions.tokenize(',') ), - params.accessions_file ? Channel.fromPath(params.accessions_file, checkIfExists: true) : Channel.empty(), - Channel.fromList( params.excluded_accessions.tokenize(',') ), - params.excluded_accessions_file ? Channel.fromPath(params.excluded_accessions_file, checkIfExists: true) : Channel.empty(), + channel.fromList( params.accessions.tokenize(',') ), + params.accessions_file ? channel.fromPath(params.accessions_file, checkIfExists: true) : channel.empty(), + channel.fromList( params.excluded_accessions.tokenize(',') ), + params.excluded_accessions_file ? channel.fromPath(params.excluded_accessions_file, checkIfExists: true) : channel.empty(), params.random_sampling_size, params.random_sampling_seed, params.outdir From 3b91c7419cbf71228100a293f1199040b9f85ed1 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 8 Dec 2025 15:37:26 +0100 Subject: [PATCH 216/258] Template update for nf-core/tools version 3.5.1 --- .../nf-core-stableexpression_logo_light.png | Bin 93434 -> 93087 bytes .../nf-core-stableexpression_logo_dark.png | Bin 26177 -> 25772 bytes .../nf-core-stableexpression_logo_light.png | Bin 21966 -> 21377 bytes ro-crate-metadata.json | 53 +++++++++++++----- 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/assets/nf-core-stableexpression_logo_light.png b/assets/nf-core-stableexpression_logo_light.png index af2eddfd4c6fc1810f1c8666446084a64d7a39a1..d1af4a9ca7044f1e36eb6255d6e925319c9d6c85 100644 GIT binary patch literal 93087 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%0AxeM4)^-8e*(RP_fefc<4 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 VTKa5eg|8rYdAjk= literal 93434 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�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%0Dkk_O@?&DNn5OqBSDXAgBQ;$`2Jf;kK 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 diff --git a/docs/images/nf-core-stableexpression_logo_dark.png b/docs/images/nf-core-stableexpression_logo_dark.png index 1d474b6abb9c453dcd738cff04e0a256e30bb6e8..24d8da8bc29065f2b138c8081f81f36dd7688727 100644 GIT binary patch delta 25295 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 ZEK;k>*=+mQm2DRAFwi%Fm+Lx4{y!^d&@})6 literal 26177 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%0Do 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 Vnm(Np3Vs>}f-1`>+-&*3z- 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Ν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\n \n \n \"nf-core/stableexpression\"\n \n\n\n[![Open in GitHub Codespaces](https://img.shields.io/badge/Open_In_GitHub_Codespaces-black?labelColor=grey&logo=github)](https://github.com/codespaces/new/nf-core/stableexpression)\n[![GitHub Actions CI Status](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml/badge.svg)](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml)\n[![GitHub Actions Linting Status](https://github.com/nf-core/stableexpression/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/stableexpression/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/stableexpression/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.04.0-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-3.5.1-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/3.5.1)\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/stableexpression)\n\n[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23stableexpression-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/stableexpression)[![Follow on Bluesky](https://img.shields.io/badge/bluesky-%40nf__core-1185fe?labelColor=000000&logo=bluesky)](https://bsky.app/profile/nf-co.re)[![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/stableexpression** is a bioinformatics pipeline that ...\n\n\n\n\n2. 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/stableexpression \\\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/stableexpression/usage) and the [parameter documentation](https://nf-co.re/stableexpression/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/stableexpression/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/stableexpression/output).\n\n## Credits\n\nnf-core/stableexpression was originally written by Olivier Coen.\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 `#stableexpression` channel](https://nfcore.slack.com/channels/stableexpression) (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", "hasPart": [ { @@ -31,6 +31,9 @@ { "@id": "assets/" }, + { + "@id": "bin/" + }, { "@id": "conf/" }, @@ -99,7 +102,7 @@ }, "mentions": [ { - "@id": "#ea87a9e0-dad4-4149-b745-000686183a2c" + "@id": "#b88c0077-fb2a-4dd6-93f0-ba79d2516560" } ], "name": "nf-core/stableexpression" @@ -121,31 +124,50 @@ }, { "@id": "main.nf", - "@type": ["File", "SoftwareSourceCode", "ComputationalWorkflow"], + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], "creator": [ { "@id": "https://orcid.org/0000-0003-3387-1040" } ], "dateCreated": "", - "dateModified": "2025-11-20T09:32:21Z", + "dateModified": "2025-12-08T15:37:14Z", "dct:conformsTo": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE/", - "keywords": ["nf-core", "nextflow", "expression", "housekeeping-genes", "qpcr-analysis"], - "license": ["MIT"], + "keywords": [ + "nf-core", + "nextflow", + "expression", + "housekeeping-genes", + "qpcr-analysis" + ], + "license": [ + "MIT" + ], "maintainer": [ { "@id": "https://orcid.org/0000-0003-3387-1040" } ], - "name": ["nf-core/stableexpression"], + "name": [ + "nf-core/stableexpression" + ], "programmingLanguage": { "@id": "https://w3id.org/workflowhub/workflow-ro-crate#nextflow" }, "sdPublisher": { "@id": "https://nf-co.re/" }, - "url": ["https://github.com/nf-core/stableexpression", "https://nf-co.re/stableexpression/dev/"], - "version": ["1.0dev"] + "url": [ + "https://github.com/nf-core/stableexpression", + "https://nf-co.re/stableexpression/dev/" + ], + "version": [ + "1.0dev" + ] }, { "@id": "https://w3id.org/workflowhub/workflow-ro-crate#nextflow", @@ -160,11 +182,11 @@ "version": "!>=25.04.0" }, { - "@id": "#ea87a9e0-dad4-4149-b745-000686183a2c", + "@id": "#b88c0077-fb2a-4dd6-93f0-ba79d2516560", "@type": "TestSuite", "instance": [ { - "@id": "#7460c1e2-3fe8-4d1a-bffb-31ea3d133ade" + "@id": "#2468a657-9383-474d-8030-d66d92db7f20" } ], "mainEntity": { @@ -173,7 +195,7 @@ "name": "Test suite for nf-core/stableexpression" }, { - "@id": "#7460c1e2-3fe8-4d1a-bffb-31ea3d133ade", + "@id": "#2468a657-9383-474d-8030-d66d92db7f20", "@type": "TestInstance", "name": "GitHub Actions workflow for testing nf-core/stableexpression", "resource": "repos/nf-core/stableexpression/actions/workflows/nf-test.yml", @@ -195,6 +217,11 @@ "@type": "Dataset", "description": "Additional files" }, + { + "@id": "bin/", + "@type": "Dataset", + "description": "Scripts that must be callable from a pipeline process" + }, { "@id": "conf/", "@type": "Dataset", @@ -308,4 +335,4 @@ "name": "Olivier Coen" } ] -} +} \ No newline at end of file From 7007b6001b5264b4e08132329f1dee2476ec5835 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 8 Dec 2025 16:25:57 +0100 Subject: [PATCH 217/258] pass linters --- .nf-core.yml | 5 + README.md | 2 - modules.json | 6 +- modules/nf-core/multiqc/main.nf | 38 +++--- modules/nf-core/multiqc/meta.yml | 38 ++++-- .../multiqc/tests/custom_prefix.config | 5 + modules/nf-core/multiqc/tests/main.nf.test | 118 ++++++++++++++++++ .../nf-core/multiqc/tests/main.nf.test.snap | 60 ++++++--- ro-crate-metadata.json | 2 +- .../main.nf | 1 - .../nf-core/utils_nfcore_pipeline/main.nf | 2 +- .../tests/nextflow.config | 2 +- 12 files changed, 215 insertions(+), 64 deletions(-) create mode 100644 modules/nf-core/multiqc/tests/custom_prefix.config diff --git a/.nf-core.yml b/.nf-core.yml index d00f62cb..630a0f17 100644 --- a/.nf-core.yml +++ b/.nf-core.yml @@ -9,7 +9,12 @@ lint: - .github/PULL_REQUEST_TEMPLATE.md nextflow_config: - params.input + template_strings: + - tests/test_data/genorm/compute_m_measure/input/std.0.0.parquet + - tests/test_data/genorm/compute_m_measure/input/std.1.2.parquet + - tests/test_data/genorm/compute_m_measure/input/std.1.2.parquet schema_lint: false + nf_core_version: 3.5.1 repository_type: pipeline template: diff --git a/README.md b/README.md index d67e5cb4..3ce6dd10 100644 --- a/README.md +++ b/README.md @@ -87,8 +87,6 @@ For further information or help, don't hesitate to get in touch on the [Slack `# - - An extensive list of references for the tools used by the pipeline can be found in the [`CITATIONS.md`](CITATIONS.md) file. You can cite the `nf-core` publication as follows: diff --git a/modules.json b/modules.json index 61b6f208..da92b2de 100644 --- a/modules.json +++ b/modules.json @@ -7,7 +7,7 @@ "nf-core": { "multiqc": { "branch": "master", - "git_sha": "af27af1be706e6a2bb8fe454175b0cdf77f47b49", + "git_sha": "0b2435805036a16dcdcf21533632d956b8273ac4", "installed_by": ["modules"] } } @@ -21,12 +21,12 @@ }, "utils_nfcore_pipeline": { "branch": "master", - "git_sha": "271e7fc14eb1320364416d996fb077421f3faed2", + "git_sha": "df4d1c8cdee98a1bbbed8fc51e82296568e0f9c1", "installed_by": ["subworkflows"] }, "utils_nfschema_plugin": { "branch": "master", - "git_sha": "4b406a74dc0449c0401ed87d5bfff4252fd277fd", + "git_sha": "e753770db613ce014b3c4bc94f6cba443427b726", "installed_by": ["subworkflows"] } } diff --git a/modules/nf-core/multiqc/main.nf b/modules/nf-core/multiqc/main.nf index c1158fb0..335afccc 100644 --- a/modules/nf-core/multiqc/main.nf +++ b/modules/nf-core/multiqc/main.nf @@ -7,7 +7,7 @@ process MULTIQC { 'community.wave.seqera.io/library/multiqc:1.32--d58f60e4deb769bf' }" input: - path multiqc_files, stageAs: "?/*" + path multiqc_files, stageAs: "?/*" path(multiqc_config) path(extra_multiqc_config) path(multiqc_logo) @@ -15,10 +15,10 @@ process MULTIQC { 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 + path "*.html" , emit: report + path "*_data" , emit: data + path "*_plots" , optional:true, emit: plots + tuple val("${task.process}"), val('multiqc'), eval('multiqc --version | sed "s/.* //g"'), topic: versions, emit: versions_multiqc when: task.ext.when == null || task.ext.when @@ -26,38 +26,30 @@ 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 ? "--config ${multiqc_config}" : '' + def extra_config = extra_multiqc_config ? "--config ${extra_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} \\ + ${extra_config} \\ + ${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_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 ce30eb73..4a908611 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 @@ -28,8 +28,8 @@ input: - edam: http://edamontology.org/format_3750 # YAML - extra_multiqc_config: type: file - description: Second optional config yml for MultiQC. Will override common sections - in multiqc_config. + description: Second optional config yml for MultiQC. Will override common + sections in multiqc_config. pattern: "*.{yml,yaml}" ontologies: - edam: http://edamontology.org/format_3750 # YAML @@ -57,10 +57,10 @@ input: - edam: http://edamontology.org/format_3475 # TSV output: report: - - "*multiqc_report.html": + - "*.html": type: file description: MultiQC report file - pattern: "multiqc_report.html" + pattern: ".html" ontologies: [] data: - "*_data": @@ -73,13 +73,27 @@ output: description: Plots created by MultiQC pattern: "*_data" ontologies: [] + versions_multiqc: + - - ${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: string + description: The command used to generate the version of the tool +topics: versions: - - versions.yml: - type: file - description: File containing software versions - pattern: "versions.yml" - ontologies: - - edam: http://edamontology.org/format_3750 # YAML + - - ${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: string + description: The command used to generate the version of the tool authors: - "@abhi18av" - "@bunop" 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 00000000..b30b1358 --- /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 e69de29b..d1ae8b06 100644 --- a/modules/nf-core/multiqc/tests/main.nf.test +++ b/modules/nf-core/multiqc/tests/main.nf.test @@ -0,0 +1,118 @@ +nextflow_process { + + name "Test Process MULTIQC" + script "../main.nf" + process "MULTIQC" + + tag "modules" + tag "modules_nfcore" + tag "multiqc" + + config "./nextflow.config" + + test("sarscov2 single-end [fastqc]") { + + 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] = [] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert process.out.report[0] ==~ ".*/multiqc_report.html" }, + { assert process.out.data[0] ==~ ".*/multiqc_data" }, + { assert snapshot(process.out.findAll { key, val -> key.startsWith("versions")}).match() } + ) + } + + } + + test("sarscov2 single-end [fastqc] - custom prefix") { + config "./custom_prefix.config" + + 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] = [] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert process.out.report[0] ==~ ".*/custom_prefix.html" }, + { assert process.out.data[0] ==~ ".*/custom_prefix_data" } + ) + } + + } + + test("sarscov2 single-end [fastqc] [config]") { + + 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] = [] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert process.out.report[0] ==~ ".*/multiqc_report.html" }, + { assert process.out.data[0] ==~ ".*/multiqc_data" }, + { assert snapshot(process.out.findAll { key, val -> key.startsWith("versions")}).match() } + ) + } + } + + test("sarscov2 single-end [fastqc] - stub") { + + options "-stub" + + 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] = [] + """ + } + } + + then { + 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.findAll { key, val -> key.startsWith("versions")} ).match() } + ) + } + + } +} diff --git a/modules/nf-core/multiqc/tests/main.nf.test.snap b/modules/nf-core/multiqc/tests/main.nf.test.snap index f5af2416..f76049d3 100644 --- a/modules/nf-core/multiqc/tests/main.nf.test.snap +++ b/modules/nf-core/multiqc/tests/main.nf.test.snap @@ -1,41 +1,61 @@ { - "multiqc_versions_single": { + "sarscov2 single-end [fastqc]": { "content": [ - [ - "versions.yml:md5,737bb2c7cad54ffc2ec020791dc48b8f" - ] + { + "versions_multiqc": [ + [ + "MULTIQC", + "multiqc", + "1.32" + ] + ] + } ], "meta": { - "nf-test": "0.9.3", - "nextflow": "24.10.4" + "nf-test": "0.9.2", + "nextflow": "25.04.6" }, - "timestamp": "2025-10-27T13:33:24.356715" + "timestamp": "2025-10-28T15:27:59.813370216" }, - "multiqc_stub": { + "sarscov2 single-end [fastqc] - stub": { "content": [ [ "multiqc_report.html", "multiqc_data", "multiqc_plots", - "versions.yml:md5,737bb2c7cad54ffc2ec020791dc48b8f" + { + "versions_multiqc": [ + [ + "MULTIQC", + "multiqc", + "1.32" + ] + ] + } ] ], "meta": { - "nf-test": "0.9.3", - "nextflow": "24.10.4" + "nf-test": "0.9.2", + "nextflow": "25.04.6" }, - "timestamp": "2025-10-27T13:34:11.103619" + "timestamp": "2025-10-28T15:30:48.963962021" }, - "multiqc_versions_config": { + "sarscov2 single-end [fastqc] [config]": { "content": [ - [ - "versions.yml:md5,737bb2c7cad54ffc2ec020791dc48b8f" - ] + { + "versions_multiqc": [ + [ + "MULTIQC", + "multiqc", + "1.32" + ] + ] + } ], "meta": { - "nf-test": "0.9.3", - "nextflow": "24.10.4" + "nf-test": "0.9.2", + "nextflow": "25.04.6" }, - "timestamp": "2025-10-27T13:34:04.615233" + "timestamp": "2025-10-28T15:29:30.664969334" } -} +} \ No newline at end of file diff --git a/ro-crate-metadata.json b/ro-crate-metadata.json index 6e7553e3..ef880802 100644 --- a/ro-crate-metadata.json +++ b/ro-crate-metadata.json @@ -23,7 +23,7 @@ "@type": "Dataset", "creativeWorkStatus": "InProgress", "datePublished": "2025-12-08T14:37:14+00:00", - "description": "

    \n \n \n \"nf-core/stableexpression\"\n \n

    \n\n[![Open in GitHub Codespaces](https://img.shields.io/badge/Open_In_GitHub_Codespaces-black?labelColor=grey&logo=github)](https://github.com/codespaces/new/nf-core/stableexpression)\n[![GitHub Actions CI Status](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml/badge.svg)](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml)\n[![GitHub Actions Linting Status](https://github.com/nf-core/stableexpression/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/stableexpression/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/stableexpression/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.04.0-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-3.5.1-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/3.5.1)\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/stableexpression)\n\n[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23stableexpression-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/stableexpression)[![Follow on Bluesky](https://img.shields.io/badge/bluesky-%40nf__core-1185fe?labelColor=000000&logo=bluesky)](https://bsky.app/profile/nf-co.re)[![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/stableexpression** is a bioinformatics pipeline that ...\n\n\n\n\n2. 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/stableexpression \\\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/stableexpression/usage) and the [parameter documentation](https://nf-co.re/stableexpression/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/stableexpression/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/stableexpression/output).\n\n## Credits\n\nnf-core/stableexpression was originally written by Olivier Coen.\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 `#stableexpression` channel](https://nfcore.slack.com/channels/stableexpression) (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", + "description": "

    \n \n \n \"nf-core/stableexpression\"\n \n

    \n\n[![Open in GitHub Codespaces](https://img.shields.io/badge/Open_In_GitHub_Codespaces-black?labelColor=grey&logo=github)](https://github.com/codespaces/new/nf-core/stableexpression)\n[![GitHub Actions CI Status](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml/badge.svg)](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml)\n[![GitHub Actions Linting Status](https://github.com/nf-core/stableexpression/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/stableexpression/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/stableexpression/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.04.0-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-3.5.1-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/3.5.1)\n[![run with apptainer](https://custom-icon-badges.demolab.com/badge/run%20with-apptainer-4545?logo=apptainer&color=teal&labelColor=000000)](https://apptainer.org/)\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/stableexpression)\n\n[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23stableexpression-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/stableexpression)[![Follow on Bluesky](https://img.shields.io/badge/bluesky-%40nf__core-1185fe?labelColor=000000&logo=bluesky)](https://bsky.app/profile/nf-co.re)[![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/stableexpression** is a bioinformatics pipeline aiming to aggregate multiple count datasets (public / provided by the user) for a specific species and find the most stable genes.\n\nIt takes as main inputs :\n * a species name (mandatory)\n * keywords for Expression Atlas / GEO search (optional)\n * a CSV input file listing your own raw / normalised count datasets (optional).\n\n**Use cases**:\n * **find the most suitable genes as RT-qPCR reference genes for a specific species (and optionally specific conditions)**\n * download all Expression Atlas and NCBI GEO datasets for a species\n\n\n\n## Basic 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\nTo search the most stable genes in a species considering all public datasets, simply run:\n\n```bash\nnextflow run nf-core/stableexpression \\\n -r dev \\\n -profile \\\n --species \\\n --outdir \n ```\n\n> [!IMPORTANT]\n > For more specific scenarios, __like fetching only specific conditions or using your own expression dataset(s)__, please refer to the [usage documentation](https://nf-co.re/stableexpression/usage).\n\n> [!NOTE]\n> See [here](https://nf-co.re/stableexpression/usage#profiles) for more information about profiles.\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/stableexpression/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/stableexpression/output).\n\n## Support us\n\nIf you like nf-core/stableexpression, please make sure you give it a star on GitHub.\n\n[![stars - stableexpression](https://img.shields.io/github/stars/nf-core/stableexpression?style=social)](https://github.com/nf-core/stableexpression)\n\n## Credits\n\nnf-core/stableexpression was originally written by Olivier Coen.\n\n\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 `#stableexpression` channel](https://nfcore.slack.com/channels/stableexpression) (you can join with [this invite](https://nf-co.re/join/slack)).\n\n## Citations\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" diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index c7dc84b8..e9b2d1dd 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -11,7 +11,6 @@ include { UTILS_NFSCHEMA_PLUGIN } from '../../nf-core/utils_nfschema_plugin' include { paramsSummaryMap } from 'plugin/nf-schema' include { samplesheetToList } from 'plugin/nf-schema' -include { paramsHelp } 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' diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/main.nf b/subworkflows/nf-core/utils_nfcore_pipeline/main.nf index 2f30e9a4..bfd25876 100644 --- a/subworkflows/nf-core/utils_nfcore_pipeline/main.nf +++ b/subworkflows/nf-core/utils_nfcore_pipeline/main.nf @@ -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())) } // diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/tests/nextflow.config b/subworkflows/nf-core/utils_nfcore_pipeline/tests/nextflow.config index 9d366ee2..d0a926bf 100644 --- a/subworkflows/nf-core/utils_nfcore_pipeline/tests/nextflow.config +++ b/subworkflows/nf-core/utils_nfcore_pipeline/tests/nextflow.config @@ -3,7 +3,7 @@ manifest { author = """nf-core""" homePage = 'https://127.0.0.1' description = """Dummy pipeline""" - nextflowVersion = '!>=25.04.00' + nextflowVersion = '!>=23.04.0' version = '9.9.9' doi = 'https://doi.org/10.5281/zenodo.5070524' } From 7b352f77bb58973d7277109c07b10260ccda28d0 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 8 Dec 2025 16:52:48 +0100 Subject: [PATCH 218/258] update citations --- CITATIONS.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/CITATIONS.md b/CITATIONS.md index 321bb9bf..a8e32197 100644 --- a/CITATIONS.md +++ b/CITATIONS.md @@ -10,25 +10,29 @@ ## Pipeline tools -- [MultiQC](https://pubmed.ncbi.nlm.nih.gov/27312411/) +- [EBI Expression Atlas](https://www.ebi.ac.uk/gxa/home) -> 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. +> Papatheodorou I, Fonseca NA, Keays M, Tang YA, Barrera E, Bazant W, Burke M, Füllgrabe A, Muñoz-Pomer Fuentes A, George N, Huerta L, Koskinen S, Mohammed S, Geniza M, Preece J, Jaiswal P, Jarnuczak AF, Huber W, Stegle O, Vizcaino JA, Brazma A, Petryszak R. Expression Atlas: gene and protein expression across multiple studies and organisms. Nucleic Acids Res. 2017 Nov 20;46(Database issue):D246–D251. doi: 10.1093/nar/gkx1158. PubMed PMID: 29165655. -- [Expression Atlas](https://www.ebi.ac.uk/gxa/home) +- [NCBI GEO](https://www.ncbi.nlm.nih.gov/geo/) -> Papatheodorou I, Fonseca NA, Keays M, Tang YA, Barrera E, Bazant W, Burke M, Füllgrabe A, Muñoz-Pomer Fuentes A, George N, Huerta L, Koskinen S, Mohammed S, Geniza M, Preece J, Jaiswal P, Jarnuczak AF, Huber W, Stegle O, Vizcaino JA, Brazma A, Petryszak R. Expression Atlas: gene and protein expression across multiple studies and organisms. Nucleic Acids Res. 2017 Nov 20;46(Database issue):D246–D251. doi: 10.1093/nar/gkx1158. PubMed PMID: 29165655. +> Ron Edgar, Michael Domrachev & Alex E Lash. Gene Expression Omnibus: NCBI gene expression and hybridization array data repository. Nucleic Acids Res. 2002 Jan 1;30(1):207-10. doi: 10.1093/nar/30.1.207. PubMed PMID: 11752295. - [g:Profiler](https://biit.cs.ut.ee/gprofiler/gost) > Reimand J, Kull M, Peterson H, Hansen J, Vilo J. g:Profiler—a web-based toolset for functional profiling of gene lists from large-scale experiments. Nucleic Acids Res. 2007 May 3;35(Web Server issue):W193–W200. doi:10.1093/nar/gkm226. PubMed PMID: 17478515. -- [DESeq2](https://bioconductor.org/packages/release/bioc/html/DESeq2.html) +- [Normfinder](https://rdrr.io/github/dhammarstrom/generefer/man/normfinder.html) -> Love MI, Huber W & Anders S. Moderated estimation of fold change and dispersion for RNA-seq data with DESeq2. Genome Biology. 2014;15(12):550. doi: 10.1186/s13059-014-0550-8. PubMed PMID: 25516281. +> Claus Lindbjerg Andersen, Jens Ledet Jensen, Torben Falck Ørntoft. Normalization of Real-Time Quantitative Reverse Transcription-PCR Data: A Model-Based Variance Estimation Approach to Identify Genes Suited for Normalization, Applied to Bladder and Colon Cancer Data Sets. Cancer Res (2004) 64 (15): 5245–5250. doi:10.1158/0008-5472.CAN-04-0496. PubMed PMID: 15289330. -## [EdgeR](https://bioconductor.org/packages/release/bioc/html/edgeR.html) +- [GeNorm](https://pypi.org/project/rna-genorm/) -> 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. Pubmed PMID: 19910308. +> Jo Vandesompele, Katleen De Preter, Filip Pattyn, Bruce Poppe, Nadine Van Roy, Anne De Paepe, Frank Speleman. Accurate normalization of real-time quantitative RT-PCR data by geometric averaging of multiple internal control genes. Genome Biol. 2002 Jun 18;3(7):RESEARCH0034. doi: 10.1186/gb-2002-3-7-research0034 Pubmed PMID: 12184808. + +- [MultiQC](https://pubmed.ncbi.nlm.nih.gov/27312411/) + +> 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. ## Software packaging/containerisation tools From e8ee9ee309f53df3dc1983ca8e48edacaef94387 Mon Sep 17 00:00:00 2001 From: Olivier Date: Tue, 9 Dec 2025 15:24:05 +0100 Subject: [PATCH 219/258] remove profile apptainer from nf-test config in order to fix CI tests --- nf-test.config | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/nf-test.config b/nf-test.config index 984c552c..a0a009fd 100644 --- a/nf-test.config +++ b/nf-test.config @@ -14,15 +14,14 @@ config { ignore 'modules/nf-core/**/tests/*', 'subworkflows/nf-core/**/tests/*' // run all test with defined profile(s) from the main nextflow.config - profile "test" + //profile "apptainer" // list of filenames or patterns that should be trigger a full test run triggers 'nextflow.config', 'nf-test.config', 'conf/test.config', 'tests/nextflow.config', 'tests/.nftignore' // load the necessary plugins - profile "apptainer" requires ( - "nf-test": "0.9.2" + "nf-test": "0.9.3" ) plugins { load "nft-utils@0.0.3" From 2a7de4dd92118d66916467ec366789f0b2a43c4f Mon Sep 17 00:00:00 2001 From: Olivier Date: Tue, 9 Dec 2025 15:34:59 +0100 Subject: [PATCH 220/258] pass prettier and ruff linters --- .github/workflows/download_pipeline.yml.rej | 30 ---------- .github/workflows/fix_linting.yml.rej | 29 ---------- .../template-version-comment.yml.rej | 16 ------ .pre-commit-config.yaml | 1 + CITATIONS.md | 6 +- README.md | 16 +++--- bin/download_latest_ensembl_annotation.py | 1 - bin/natural_language_utils.py | 1 - bin/old/get_array_express_accessions.py | 3 - docs/output.md | 45 +++++++++------ docs/troubleshooting.md | 3 + docs/usage.md | 55 +++++++++---------- 12 files changed, 67 insertions(+), 139 deletions(-) delete mode 100644 .github/workflows/download_pipeline.yml.rej delete mode 100644 .github/workflows/fix_linting.yml.rej delete mode 100644 .github/workflows/template-version-comment.yml.rej diff --git a/.github/workflows/download_pipeline.yml.rej b/.github/workflows/download_pipeline.yml.rej deleted file mode 100644 index 55833335..00000000 --- a/.github/workflows/download_pipeline.yml.rej +++ /dev/null @@ -1,30 +0,0 @@ -diff a/.github/workflows/download_pipeline.yml b/.github/workflows/download_pipeline.yml (rejected hunks) -@@ -52,9 +44,9 @@ jobs: - - name: Disk space cleanup - uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 - -- - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5 -+ - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6 - with: -- python-version: "3.12" -+ python-version: "3.14" - architecture: "x64" - - - name: Setup Apptainer -@@ -65,7 +57,7 @@ jobs: - - name: Install dependencies - run: | - python -m pip install --upgrade pip -- pip install git+https://github.com/nf-core/tools.git@dev -+ pip install git+https://github.com/nf-core/tools.git - - - name: Make a cache directory for the container images - run: | -@@ -120,6 +112,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 }} diff --git a/.github/workflows/fix_linting.yml.rej b/.github/workflows/fix_linting.yml.rej deleted file mode 100644 index f80eaf24..00000000 --- a/.github/workflows/fix_linting.yml.rej +++ /dev/null @@ -1,29 +0,0 @@ -diff a/.github/workflows/fix_linting.yml b/.github/workflows/fix_linting.yml (rejected hunks) -@@ -13,13 +13,13 @@ jobs: - runs-on: ubuntu-latest - steps: - # Use the @nf-core-bot token to check out so we can push later -- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 -+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 - 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@71345be0265236311c031f5c7866368bd1eff043 # v4 -+ uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5 - with: - comment-id: ${{ github.event.comment.id }} - reactions: eyes -@@ -32,9 +32,9 @@ jobs: - GITHUB_TOKEN: ${{ secrets.nf_core_bot_auth_token }} - - # Install and run pre-commit -- - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5 -+ - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6 - with: -- python-version: "3.12" -+ python-version: "3.14" - - - name: Install pre-commit - run: pip install pre-commit diff --git a/.github/workflows/template-version-comment.yml.rej b/.github/workflows/template-version-comment.yml.rej deleted file mode 100644 index 2e300224..00000000 --- a/.github/workflows/template-version-comment.yml.rej +++ /dev/null @@ -1,16 +0,0 @@ -diff a/.github/workflows/template-version-comment.yml b/.github/workflows/template-version-comment.yml (rejected hunks) -@@ -9,12 +9,12 @@ jobs: - runs-on: ubuntu-latest - steps: - - name: Check out pipeline code -- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 -+ uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 - with: - ref: ${{ github.event.pull_request.head.sha }} - - - name: Read template version from .nf-core.yml -- uses: nichmor/minimal-read-yaml@v0.0.2 -+ uses: nichmor/minimal-read-yaml@1f7205277e25e156e1f63815781db80a6d490b8f # v0.0.2 - id: read_yml - with: - config: ${{ github.workspace }}/.nf-core.yml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fea329a7..c7942f15 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,6 +35,7 @@ repos: - id: ruff files: \.py$ args: [--fix] + exclude: bin/old/ # Run the formatter. - id: ruff-format files: \.py$ diff --git a/CITATIONS.md b/CITATIONS.md index a8e32197..e7423ab9 100644 --- a/CITATIONS.md +++ b/CITATIONS.md @@ -16,7 +16,7 @@ - [NCBI GEO](https://www.ncbi.nlm.nih.gov/geo/) -> Ron Edgar, Michael Domrachev & Alex E Lash. Gene Expression Omnibus: NCBI gene expression and hybridization array data repository. Nucleic Acids Res. 2002 Jan 1;30(1):207-10. doi: 10.1093/nar/30.1.207. PubMed PMID: 11752295. +> Ron Edgar, Michael Domrachev & Alex E Lash. Gene Expression Omnibus: NCBI gene expression and hybridization array data repository. Nucleic Acids Res. 2002 Jan 1;30(1):207-10. doi: 10.1093/nar/30.1.207. PubMed PMID: 11752295. - [g:Profiler](https://biit.cs.ut.ee/gprofiler/gost) @@ -24,11 +24,11 @@ - [Normfinder](https://rdrr.io/github/dhammarstrom/generefer/man/normfinder.html) -> Claus Lindbjerg Andersen, Jens Ledet Jensen, Torben Falck Ørntoft. Normalization of Real-Time Quantitative Reverse Transcription-PCR Data: A Model-Based Variance Estimation Approach to Identify Genes Suited for Normalization, Applied to Bladder and Colon Cancer Data Sets. Cancer Res (2004) 64 (15): 5245–5250. doi:10.1158/0008-5472.CAN-04-0496. PubMed PMID: 15289330. +> Claus Lindbjerg Andersen, Jens Ledet Jensen, Torben Falck Ørntoft. Normalization of Real-Time Quantitative Reverse Transcription-PCR Data: A Model-Based Variance Estimation Approach to Identify Genes Suited for Normalization, Applied to Bladder and Colon Cancer Data Sets. Cancer Res (2004) 64 (15): 5245–5250. doi:10.1158/0008-5472.CAN-04-0496. PubMed PMID: 15289330. - [GeNorm](https://pypi.org/project/rna-genorm/) -> Jo Vandesompele, Katleen De Preter, Filip Pattyn, Bruce Poppe, Nadine Van Roy, Anne De Paepe, Frank Speleman. Accurate normalization of real-time quantitative RT-PCR data by geometric averaging of multiple internal control genes. Genome Biol. 2002 Jun 18;3(7):RESEARCH0034. doi: 10.1186/gb-2002-3-7-research0034 Pubmed PMID: 12184808. +> Jo Vandesompele, Katleen De Preter, Filip Pattyn, Bruce Poppe, Nadine Van Roy, Anne De Paepe, Frank Speleman. Accurate normalization of real-time quantitative RT-PCR data by geometric averaging of multiple internal control genes. Genome Biol. 2002 Jun 18;3(7):RESEARCH0034. doi: 10.1186/gb-2002-3-7-research0034 Pubmed PMID: 12184808. - [MultiQC](https://pubmed.ncbi.nlm.nih.gov/27312411/) diff --git a/README.md b/README.md index 3ce6dd10..846483b2 100644 --- a/README.md +++ b/README.md @@ -25,15 +25,15 @@ **nf-core/stableexpression** is a bioinformatics pipeline aiming to aggregate multiple count datasets (public / provided by the user) for a specific species and find the most stable genes. It takes as main inputs : - * a species name (mandatory) - * keywords for Expression Atlas / GEO search (optional) - * a CSV input file listing your own raw / normalised count datasets (optional). -**Use cases**: - * **find the most suitable genes as RT-qPCR reference genes for a specific species (and optionally specific conditions)** - * download all Expression Atlas and NCBI GEO datasets for a species +- a species name (mandatory) +- keywords for Expression Atlas / GEO search (optional) +- a CSV input file listing your own raw / normalised count datasets (optional). +**Use cases**: +- **find the most suitable genes as RT-qPCR reference genes for a specific species (and optionally specific conditions)** +- download all Expression Atlas and NCBI GEO datasets for a species ## Basic usage @@ -48,10 +48,10 @@ nextflow run nf-core/stableexpression \ -profile \ --species \ --outdir - ``` +``` > [!IMPORTANT] - > For more specific scenarios, __like fetching only specific conditions or using your own expression dataset(s)__, please refer to the [usage documentation](https://nf-co.re/stableexpression/usage). +> For more specific scenarios, **like fetching only specific conditions or using your own expression dataset(s)**, please refer to the [usage documentation](https://nf-co.re/stableexpression/usage). > [!NOTE] > See [here](https://nf-co.re/stableexpression/usage#profiles) for more information about profiles. diff --git a/bin/download_latest_ensembl_annotation.py b/bin/download_latest_ensembl_annotation.py index cce6c48f..205a2014 100755 --- a/bin/download_latest_ensembl_annotation.py +++ b/bin/download_latest_ensembl_annotation.py @@ -5,7 +5,6 @@ import argparse import logging from datetime import datetime -from pathlib import Path from urllib.request import urlretrieve import pandas as pd diff --git a/bin/natural_language_utils.py b/bin/natural_language_utils.py index 2d774cc2..79f8463c 100755 --- a/bin/natural_language_utils.py +++ b/bin/natural_language_utils.py @@ -137,4 +137,3 @@ def keywords_in_fields(fields: list[str], keywords: list[str]) -> list[str]: for field in fields if word_is_in_sentence(keyword, field) ] - diff --git a/bin/old/get_array_express_accessions.py b/bin/old/get_array_express_accessions.py index beb0ddd5..7c6371d9 100755 --- a/bin/old/get_array_express_accessions.py +++ b/bin/old/get_array_express_accessions.py @@ -9,14 +9,11 @@ from functools import partial from multiprocessing import Pool -import pandas as pd import requests -import yaml from natural_language_utils import keywords_in_fields from tenacity import ( before_sleep_log, retry, - retry_if_exception_type, stop_after_delay, wait_exponential, ) diff --git a/docs/output.md b/docs/output.md index aff083f2..4eeb6c43 100644 --- a/docs/output.md +++ b/docs/output.md @@ -17,26 +17,38 @@ The directories listed below will be created in the results directory after the The pipeline is built using [Nextflow](https://www.nextflow.io/) and processes data using the following steps: 1. Get accessions - - Get [Expression Atlas](https://www.ebi.ac.uk/gxa/home) dataset accessions corresponding to the provided species (and optionally keywords) (run by default; optional) - - Get NBCI [GEO](https://www.ncbi.nlm.nih.gov/gds) __microarray__ dataset accessions corresponding to the provided species (and optionally keywords) (run by default; optional) + +- Get [Expression Atlas](https://www.ebi.ac.uk/gxa/home) dataset accessions corresponding to the provided species (and optionally keywords) (run by default; optional) +- Get NBCI [GEO](https://www.ncbi.nlm.nih.gov/gds) **microarray** dataset accessions corresponding to the provided species (and optionally keywords) (run by default; optional) + 2. Download data - - Download [Expression Atlas](https://www.ebi.ac.uk/gxa/home) data (run by default; optional) - - Download NBCI [GEO](https://www.ncbi.nlm.nih.gov/gds) data (run by default; optional) + +- Download [Expression Atlas](https://www.ebi.ac.uk/gxa/home) data (run by default; optional) +- Download NBCI [GEO](https://www.ncbi.nlm.nih.gov/gds) data (run by default; optional) + 3. ID Mapping - - Map gene IDS to NCBI Entrez Gene IDS (or Ensembl IDs) for standardisation among datasets using [g:Profiler](https://biit.cs.ut.ee/gprofiler/gost) (run by default; optional) + +- Map gene IDS to NCBI Entrez Gene IDS (or Ensembl IDs) for standardisation among datasets using [g:Profiler](https://biit.cs.ut.ee/gprofiler/gost) (run by default; optional) + 4. Data normalisation - - Normalize RNAseq raw data using [DESeq2](https://bioconductor.org/packages/release/bioc/html/DESeq2.html) or [EdgeR](https://bioconductor.org/packages/release/bioc/html/edgeR.html) - - Perform quantile normalisation on each dataset separately using [scikit-learn](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.quantile_transform.html) + +- Normalize RNAseq raw data using [DESeq2](https://bioconductor.org/packages/release/bioc/html/DESeq2.html) or [EdgeR](https://bioconductor.org/packages/release/bioc/html/edgeR.html) +- Perform quantile normalisation on each dataset separately using [scikit-learn](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.quantile_transform.html) + 5. Data cleaning - - Get statistics for each sample in each dataset - - Remove samples that diverge too much from the expected normalised profile + +- Get statistics for each sample in each dataset +- Remove samples that diverge too much from the expected normalised profile + 6. Merge all data 7. Compute base statistics for each gene, platform-wide and for each platform (RNAseq and microarray) 8. Compute stability scoring - - Get list of candidate genes based on base statistics - - Run optimised, scalable version of [Normfinder](https://www.moma.dk/software/normfinder) - - Run optimised, scalable version of [Genorm](https://genomebiology.biomedcentral.com/articles/10.1186/gb-2002-3-7-research0034) (NOT run by default; optional) - - Compute stability scores for each candidate gene + +- Get list of candidate genes based on base statistics +- Run optimised, scalable version of [Normfinder](https://www.moma.dk/software/normfinder) +- Run optimised, scalable version of [Genorm](https://genomebiology.biomedcentral.com/articles/10.1186/gb-2002-3-7-research0034) (NOT run by default; optional) +- Compute stability scores for each candidate gene + 9. Aggregate results 10. Prepare [Dash Plotly](https://dash.plotly.com/) app for further investigation of gene / sample counts 11. Make [`MultiQC`](http://multiqc.info/) report @@ -73,16 +85,17 @@ conda activate nf-core-stableexpression-dash ``` then: + ``` cd dash_app python app.py ``` + and open your browser at `http://localhost:8080` > [!NOTE] > The app will try to use the port `8080` by default. If it is already in use, it will try `8081`, `8082` and so on. Check the logs to see which port it is using. - ### Expression Atlas
    @@ -125,7 +138,6 @@ and open your browser at `http://localhost:8080` - `normalised/edger/` for EdgeR - `quantile_normalised` : Quantile normalised datasets - ### Gene base statistics
    @@ -169,9 +181,6 @@ The gene stat summary is also bundled with the Dash Plotly app.
    - - - ### Pipeline information
    diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index e3963de8..2169eae0 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -20,10 +20,13 @@ java.lang.OutOfMemoryError: Java heap space ``` We recommend adding the following line to your environment to limit this (typically in `~/.bashrc` or `~./bash_profile`): + ```bash NXF_OPTS='-Xms1g -Xmx4g' ``` or running the pipeline with: + ```bash NXF_OPTS='-Xms1g -Xmx4g' nextflow run nf-core/stableexpression ... +``` diff --git a/docs/usage.md b/docs/usage.md index f940ebf4..6ad08f44 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -10,7 +10,6 @@ > _Documentation of pipeline parameters is generated automatically from the pipeline schema and can no longer be found in markdown files._ - ## 1. Basic run This pipeline fetches Expression Atlas and GEO accessions for the provided species and downloads the corresponding data. @@ -40,12 +39,12 @@ nextflow run nf-core/stableexpression \ ``` > [!NOTE] +> > - Multiple keywords must be separated by commas. -> - Note that the keywords are additive: you will get datasets that fit with __either of the keywords__. +> - Note that the keywords are additive: you will get datasets that fit with **either of the keywords**. > - A dataset will be downloaded if a keyword is found in its summary or in the same of a sample. > - The natural language processing [`ǹltk`](https://www.nltk.org/) python package is used to find keywords as well as derived words. For example, the `leaf` keyword should match 'leaf', 'leaves', 'leafy', etc. - ## 3. Provide your own accessions You may already have an idea of specific Expression Atlas / GEO accessions you want to use in the analysis. @@ -89,6 +88,7 @@ Fetched accessions with their respective metadata will be available in ` You can of course provide your own counts datasets / experimental designs. > [!NOTE] +> > - To ensure all RNAseq datasets are processed the same way, you should provide **raw counts**. > - In case normalised counts are provided, you should provide the same normalisation method for all of them (TPM, FPKM, etc.). @@ -97,12 +97,12 @@ You can of course provide your own counts datasets / experimental designs. First, prepare a samplesheet listing the different count datasets you want to use. Each row represents a specific dataset and must contain: -| Column | Description | -| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `counts` | Path to the count dataset (a CSV / TSV file) | -| `design` | Path to the experimental design associated to this dataset (a CSV / TSV file) | -| `platform` | Platform used to generate the counts (`rnaseq` or `microarray`) -| `normalised` | Boolean (`true` / `false`) representing whether the counts are already normalised or not. +| Column | Description | +| ------------ | ----------------------------------------------------------------------------------------- | +| `counts` | Path to the count dataset (a CSV / TSV file) | +| `design` | Path to the experimental design associated to this dataset (a CSV / TSV file) | +| `platform` | Platform used to generate the counts (`rnaseq` or `microarray`) | +| `normalised` | Boolean (`true` / `false`) representing whether the counts are already normalised or not. | It should look as follows: @@ -130,7 +130,6 @@ It can also be a YAML file: normalised: true ``` - The counts should have the following structure: ```csv title=counts.csv @@ -139,7 +138,6 @@ gene_1,1,2,3 gene_2,1,2,3 ``` - While the design should look like: ```csv title=design.csv @@ -149,15 +147,14 @@ sample_B,condition_2 sample_C,condition_1 ``` - > [!WARNING] +> > - In the count file, the first header column (corresponding to gene IDs) should not be empty. However, its name can be anything. > - The count file should not have any column other than the first one (gene IDs) and the sample columns. Extra columns will be ignored. > [!TIP] > Both counts and design files can also be supplied as TSV files. - Now run the pipeline with: ```bash @@ -172,10 +169,10 @@ nextflow run nf-core/stableexpression \ ``` > [!TIP] -> The `--skip_fetch_eatlas_accessions` and `--skip_fetch_geo_accessions` parameters are supplied here to show how to analyse __only your own dataset__. You may remove these parameters if you want to mix you dataset(s) with public ones. +> The `--skip_fetch_eatlas_accessions` and `--skip_fetch_geo_accessions` parameters are supplied here to show how to analyse **only your own dataset**. You may remove these parameters if you want to mix you dataset(s) with public ones. > [!IMPORTANT] -> By default, the pipeline tries to map gene IDs to NCBI Entrez Gene IDs. __All genes that cannot be mapped are discarded from the analysis__. This ensures that all genes are named the same between datasets and allows comparing multiple datasets with each other. If you are confident that your genes have the same name between your different datasets or if you think that your gene IDs won't be mapped properly, you can disable this mapping by adding the `--skip_id_mapping` parameter. In such case, you may supply your own gene id mapping file and gene metadata file with the `--gene_id_mapping` and `--gene_metadata` parameters respectively. See [next section](#5-custom-gene-id-mapping-and-metadata) for further details. +> By default, the pipeline tries to map gene IDs to NCBI Entrez Gene IDs. **All genes that cannot be mapped are discarded from the analysis**. This ensures that all genes are named the same between datasets and allows comparing multiple datasets with each other. If you are confident that your genes have the same name between your different datasets or if you think that your gene IDs won't be mapped properly, you can disable this mapping by adding the `--skip_id_mapping` parameter. In such case, you may supply your own gene id mapping file and gene metadata file with the `--gene_id_mapping` and `--gene_metadata` parameters respectively. See [next section](#5-custom-gene-id-mapping-and-metadata) for further details. > [!TIP] > You can check if your gene IDs can be mapped using the [g:Profiler server](https://biit.cs.ut.ee/gprofiler/convert). @@ -199,10 +196,10 @@ nextflow run nf-core/stableexpression \ Structure of the gene id mapping file: -| Column | Description | -| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `original_gene_id` | Gene ID used in the provided count dataset(s) | -| `gene_id` | Mapped gene ID | +| Column | Description | +| ------------------ | --------------------------------------------- | +| `original_gene_id` | Gene ID used in the provided count dataset(s) | +| `gene_id` | Mapped gene ID | It should look as follows: @@ -212,15 +209,13 @@ gene_A,ENSG1234567890 geneB,OTHERmappedgeneID ``` - - Structure of the gene metadata file: -| Column | Description | -| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `gene_id` | Mapped gene ID | -| `name` | Gene common name | -| `description` | Gene description | +| Column | Description | +| ------------- | ---------------- | +| `gene_id` | Mapped gene ID | +| `name` | Gene common name | +| `description` | Gene description | It should look as follows: @@ -234,7 +229,6 @@ OTHERmappedgeneID,My OTHER Gene,Another description For advanced scenarios, you can see the list of available parameters in the [parameter documentation](https://nf-co.re/stableexpression/parameters). - ## Pipeline output Note that the pipeline will create the following files in your working directory: @@ -274,7 +268,6 @@ outdir: './results/' You can also generate such `YAML`/`JSON` files via [nf-core/launch](https://nf-co.re/launch). - ### 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: @@ -314,11 +307,14 @@ Several generic profiles are bundled with the pipeline which instruct the pipeli > When running the pipeline of multi-user server or on a cluster, the best practice is to use Apptainer (formerly Singularity). You can install Apptainer by following these [instructions](https://apptainer.org/docs/admin/main/installation.html#). > In case you encounter the following error when running Apptainer: +> > ``` > ERROR : Could not write info to setgroups: Permission denied > ERROR : Error while waiting event for user namespace mappings: no event received > ``` +> > you may need to install the `apptainer-suid` package instead of `apptainer`: +> > ``` > # Debian / Ubuntu > sudo apt install apptainer-suid @@ -326,8 +322,7 @@ Several generic profiles are bundled with the pipeline which instruct the pipeli > sudo yum install apptainer-suid > # Fedora > sudo dnf install apptainer-suid ->``` - +> ``` The pipeline also dynamically loads configurations from [https://github.com/nf-core/configs](https://github.com/nf-core/configs) when it runs, making multiple config profiles for various institutional clusters available at run time. For more information and to check if your system is supported, please see the [nf-core/configs documentation](https://github.com/nf-core/configs#documentation). From a1af98c01a63a39238237d007bfe684e0838b2fb Mon Sep 17 00:00:00 2001 From: Olivier Date: Tue, 9 Dec 2025 15:48:12 +0100 Subject: [PATCH 221/258] add files to skip for files_unchanged during nf-core pipelines lint --- .nf-core.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.nf-core.yml b/.nf-core.yml index 630a0f17..24459e2a 100644 --- a/.nf-core.yml +++ b/.nf-core.yml @@ -6,6 +6,8 @@ lint: - conf/igenomes_ignored.config files_unchanged: - assets/nf-core-stableexpression_logo_light.png + - docs/images/nf-core-stableexpression_logo_light.png + - docs/images/nf-core-stableexpression_logo_dark.png - .github/PULL_REQUEST_TEMPLATE.md nextflow_config: - params.input From 56899e7cecbb2a61d31666a603308f4c9f0bebef Mon Sep 17 00:00:00 2001 From: Olivier Date: Tue, 9 Dec 2025 15:48:28 +0100 Subject: [PATCH 222/258] change base config process_high modules --- conf/base.config | 9 ++------- modules/local/aggregate_results/main.nf | 2 +- modules/local/collect_statistics/main.nf | 2 +- modules/local/compute_base_statistics/main.nf | 2 +- modules/local/compute_stability_scores/main.nf | 2 +- modules/local/dash_app/main.nf | 2 +- modules/local/expressionatlas/getaccessions/main.nf | 2 +- modules/local/geo/getaccessions/main.nf | 2 +- modules/local/get_candidate_genes/main.nf | 2 +- modules/local/merge_counts/main.nf | 2 +- modules/local/normfinder/main.nf | 2 +- 11 files changed, 12 insertions(+), 17 deletions(-) diff --git a/conf/base.config b/conf/base.config index 23879b16..9566094f 100644 --- a/conf/base.config +++ b/conf/base.config @@ -50,16 +50,11 @@ process { } withLabel:process_medium { cpus = { 4 } - memory = { 8.GB + 4.GB * task.attempt } + memory = { 6.GB + 2.GB * task.attempt } time = { 4.h * task.attempt } } - withLabel:process_high_memory { + withLabel:process_high { cpus = { 4 } - memory = { 12.GB + 4.GB * task.attempt } - time = { 8.h * task.attempt } - } - withLabel:process_high_cpus { - cpus = { 8 } memory = { 8.GB + 4.GB * task.attempt } time = { 8.h * task.attempt } } diff --git a/modules/local/aggregate_results/main.nf b/modules/local/aggregate_results/main.nf index 55d5c954..d139b508 100644 --- a/modules/local/aggregate_results/main.nf +++ b/modules/local/aggregate_results/main.nf @@ -1,6 +1,6 @@ process AGGREGATE_RESULTS { - label 'process_high_memory' + label 'process_high' conda "${moduleDir}/environment.yml" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? diff --git a/modules/local/collect_statistics/main.nf b/modules/local/collect_statistics/main.nf index 923e6973..0fd5bdd2 100644 --- a/modules/local/collect_statistics/main.nf +++ b/modules/local/collect_statistics/main.nf @@ -1,7 +1,7 @@ process COLLECT_STATISTICS { tag "${file.baseName}" - label "process_high_memory" + label "process_high" conda "${moduleDir}/environment.yml" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? diff --git a/modules/local/compute_base_statistics/main.nf b/modules/local/compute_base_statistics/main.nf index 22d7bd1a..7bf75279 100644 --- a/modules/local/compute_base_statistics/main.nf +++ b/modules/local/compute_base_statistics/main.nf @@ -1,6 +1,6 @@ process COMPUTE_BASE_STATISTICS { - label 'process_high_memory' + label 'process_high' memory { def calc = (dataset_size / 50000).toInteger() def result = Math.max(1, calc) // Ensure at least 1 MB diff --git a/modules/local/compute_stability_scores/main.nf b/modules/local/compute_stability_scores/main.nf index 19e67cfe..ed0615ca 100644 --- a/modules/local/compute_stability_scores/main.nf +++ b/modules/local/compute_stability_scores/main.nf @@ -1,6 +1,6 @@ process COMPUTE_STABILITY_SCORES { - label 'process_high_memory' + label 'process_high' conda "${moduleDir}/environment.yml" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? diff --git a/modules/local/dash_app/main.nf b/modules/local/dash_app/main.nf index 73dab84d..3a49ff8a 100644 --- a/modules/local/dash_app/main.nf +++ b/modules/local/dash_app/main.nf @@ -1,6 +1,6 @@ process DASH_APP { - label 'process_high_memory' + label 'process_high' conda "${moduleDir}/app/environment.yml" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? diff --git a/modules/local/expressionatlas/getaccessions/main.nf b/modules/local/expressionatlas/getaccessions/main.nf index 0aded0be..9c1bb003 100644 --- a/modules/local/expressionatlas/getaccessions/main.nf +++ b/modules/local/expressionatlas/getaccessions/main.nf @@ -1,6 +1,6 @@ process EXPRESSIONATLAS_GETACCESSIONS { - label 'process_high_cpus' + label 'process_high' tag "${species}" diff --git a/modules/local/geo/getaccessions/main.nf b/modules/local/geo/getaccessions/main.nf index 078a4407..ef362848 100644 --- a/modules/local/geo/getaccessions/main.nf +++ b/modules/local/geo/getaccessions/main.nf @@ -1,6 +1,6 @@ process GEO_GETACCESSIONS { - label 'process_high_cpus' + label 'process_high' tag "${species}" diff --git a/modules/local/get_candidate_genes/main.nf b/modules/local/get_candidate_genes/main.nf index c91beaff..64510f21 100644 --- a/modules/local/get_candidate_genes/main.nf +++ b/modules/local/get_candidate_genes/main.nf @@ -1,6 +1,6 @@ process GET_CANDIDATE_GENES { - label 'process_high_memory' + label 'process_high' conda "${moduleDir}/environment.yml" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? diff --git a/modules/local/merge_counts/main.nf b/modules/local/merge_counts/main.nf index cddd5443..a897a563 100644 --- a/modules/local/merge_counts/main.nf +++ b/modules/local/merge_counts/main.nf @@ -1,6 +1,6 @@ process MERGE_COUNTS { - label "process_high_memory" + label "process_high" memory { def calc = (dataset_size / 50000).toInteger() def result = Math.max(1, calc) // Ensure at least 1 MB diff --git a/modules/local/normfinder/main.nf b/modules/local/normfinder/main.nf index f6c8b155..57ce0dc8 100644 --- a/modules/local/normfinder/main.nf +++ b/modules/local/normfinder/main.nf @@ -1,6 +1,6 @@ process NORMFINDER { - label 'process_high_memory' + label 'process_high' conda "${moduleDir}/environment.yml" container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? From 82ebd985ea6368e3d3e639633b13209043c2f8b8 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 10 Dec 2025 11:19:42 +0100 Subject: [PATCH 223/258] add random sampling based on nb of samples direclty in Expression Atlas get accessions --- bin/get_eatlas_accessions.py | 132 ++++++++++++++---- .../expressionatlas/getaccessions/main.nf | 4 + .../local/get_public_accessions/main.nf | 4 +- .../getaccessions/main.nf.test | 58 ++++++-- 4 files changed, 163 insertions(+), 35 deletions(-) diff --git a/bin/get_eatlas_accessions.py b/bin/get_eatlas_accessions.py index 0ef98e3f..6156d440 100755 --- a/bin/get_eatlas_accessions.py +++ b/bin/get_eatlas_accessions.py @@ -4,6 +4,7 @@ import argparse import logging +import random from functools import partial from multiprocessing import Pool @@ -21,6 +22,8 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) +ALLOWED_PLATFORMS = ["rnaseq", "microarray"] + ALL_EXP_URL = "https://www.ebi.ac.uk/gxa/json/experiments/" ACCESSION_OUTFILE_NAME = "accessions.txt" # ALL_EXPERIMENTS_METADATA_OUTFILE_NAME = "all_experiments.metadata.tsv" @@ -57,7 +60,21 @@ def parse_args(): nargs="*", help="Keywords to search for in experiment description", ) - parser.add_argument("--platform", type=str, help="Platform type") + parser.add_argument( + "--platform", type=str, help="Platform type", choices=ALLOWED_PLATFORMS + ) + parser.add_argument( + "--random-sampling-size", + dest="random_sampling_size", + type=int, + help="Random sampling size", + ) + parser.add_argument( + "--random-sampling-seed", + dest="random_sampling_seed", + type=int, + help="Random sampling seed", + ) return parser.parse_args() @@ -197,7 +214,7 @@ def get_eatlas_experiments(): return data["experiments"] -def get_platform_specific_experiments(experiments: list[dict], platform: str): +def filter_by_platform(experiments: list[dict], platform: str | None): """ Gets all experiments for a given platform from Expression Atlas Possible platforms in Expression Atlas are 'rnaseq', 'microarray', 'proteomics' @@ -221,11 +238,22 @@ def get_platform_specific_experiments(experiments: list[dict], platform: str): if isinstance(technology_type, list) else technology_type ) + # parsed_platform is in ["rnaseq", "microarray", "proteomics", ...] parsed_platform = ( parsed_technology_type.lower().split(" ")[0].replace("-", "") ) - if platform == parsed_platform: - platform_experiments.append(exp_dict) + + if platform is not None: + if parsed_platform == platform: + platform_experiments.append(exp_dict) + else: + if parsed_platform in ALLOWED_PLATFORMS: + platform_experiments.append(exp_dict) + + else: + logger.warning( + f"Technology type not found for experiment {exp_dict['accession']}" + ) return platform_experiments @@ -306,6 +334,42 @@ def get_metadata_for_selected_experiments( ] +def sample_experiments_randomly( + experiments: list[dict], sampling_size: int, seed: int +) -> list[str]: + random.seed(seed) + sampled_experiments = [] + + total_nb_samples = 0 + experiments_left = list(experiments) + while experiments_left and total_nb_samples <= sampling_size: + # if the min number of samples is greater than the remaining space left, we get out of the loop + experiments_left_nb_samples = [exp["nb_samples"] for exp in experiments_left] + min_nb_samples = min(experiments_left_nb_samples) + if min_nb_samples > sampling_size - total_nb_samples: + break + + found_experiment = False + test_total_nb_samples = int(total_nb_samples) + not_chosen_yet = list(experiments_left) + while not_chosen_yet and not found_experiment: + experiment = random.choice(not_chosen_yet) + not_chosen_yet.remove(experiment) + test_total_nb_samples = total_nb_samples + experiment["nb_samples"] + if test_total_nb_samples <= sampling_size: + found_experiment = True + + # if the last one was not good, it means we reached the limit of samples we can take + if not found_experiment: + break + else: + total_nb_samples = test_total_nb_samples + experiments_left.remove(experiment) + sampled_experiments.append(experiment) + + return [exp["accession"] for exp in sampled_experiments] + + def format_species_name(species: str) -> str: return species.replace("_", " ").capitalize().strip() @@ -333,39 +397,55 @@ def main(): keywords = args.keywords logger.info(f"Getting experiments corresponding to species {species_name}") - all_experiments = get_eatlas_experiments() + experiments = get_eatlas_experiments() - if args.platform: - logger.info(f"Getting experiments corresponding to platform {args.platform}") - all_experiments = get_platform_specific_experiments( - all_experiments, args.platform - ) + logger.info("Filtering on species name") + experiments = get_species_experiments(experiments, species_name) + logger.info(f"Found {len(experiments)} experiments for species {species_name}") - species_experiments = get_species_experiments(all_experiments, species_name) - logger.info( - f"Found {len(species_experiments)} experiments for species {species_name}" - ) + logger.info("Filtering experiments based on platform") + experiments = filter_by_platform(experiments, args.platform) logger.info("Parsing experiments") with Pool(processes=args.nb_cpus) as pool: - results = pool.map(parse_experiment, species_experiments) + results = pool.map(parse_experiment, experiments) if keywords: logger.info(f"Filtering experiments with keywords {keywords}") func = partial(filter_experiment_with_keywords, keywords=keywords) with Pool(processes=args.nb_cpus) as pool: results = [res for res in pool.map(func, results) if res is not None] - - if results: - logger.info(f"Kept {len(results)} experiments") - # getting accessions of selected experiments - selected_accessions = [exp_dict["accession"] for exp_dict in results] - # keeping metadata only for selected experiments - selected_experiments = get_metadata_for_selected_experiments( - species_experiments, results + logger.info( + f"Found {len(results)} experiments corresponding to keywords {keywords}" ) - else: + # getting accessions of selected experiments + selected_accessions = [exp_dict["accession"] for exp_dict in results] + + selected_accession_to_nb_samples = [ + { + "accession": exp_dict["experimentAccession"], + "nb_samples": exp_dict["numberOfAssays"], + } + for exp_dict in experiments + if exp_dict["experimentAccession"] in selected_accessions + ] + + nb_samples_df = pd.DataFrame.from_dict(selected_accession_to_nb_samples) + nb_samples_df.to_csv("selected_accession_to_nb_samples.csv", index=False) + + logger.info("Sampling experiments randomly") + selected_accessions = sample_experiments_randomly( + selected_accession_to_nb_samples, + args.random_sampling_size, + args.random_sampling_seed, + ) + logger.info(f"Kept {len(selected_accessions)} experiments after random sampling") + + # keeping metadata only for selected experiments + selected_experiments = get_metadata_for_selected_experiments(experiments, results) + + if not selected_accessions: logger.warning( f"Could not find experiments for species {species_name} and keywords {keywords}" ) @@ -383,7 +463,7 @@ def main(): logger.info( f"Writing metadata of all experiments for species {species_name} to {SPECIES_EXPERIMENTS_METADATA_OUTFILE_NAME}" ) - df = pd.DataFrame.from_dict(species_experiments) + df = pd.DataFrame.from_dict(experiments) df.to_csv( SPECIES_EXPERIMENTS_METADATA_OUTFILE_NAME, sep="\t", index=False, header=True ) @@ -400,7 +480,7 @@ def main(): header=True, ) - if results is not None: + if results: # exporting list of selected experiments with their keywords logger.info( f"Writing filtered experiments with keywords to {FILTERED_EXPERIMENTS_WITH_KEYWORDS_OUTFILE_NAME}" diff --git a/modules/local/expressionatlas/getaccessions/main.nf b/modules/local/expressionatlas/getaccessions/main.nf index 9c1bb003..da476e0d 100644 --- a/modules/local/expressionatlas/getaccessions/main.nf +++ b/modules/local/expressionatlas/getaccessions/main.nf @@ -13,6 +13,8 @@ process EXPRESSIONATLAS_GETACCESSIONS { val species val keywords val platform + val random_sampling_size + val random_sampling_seed output: path "accessions.txt", optional: true, emit: accessions @@ -41,6 +43,8 @@ process EXPRESSIONATLAS_GETACCESSIONS { get_eatlas_accessions.py \\ $args \\ + --random-sampling_size $random_sampling_size \\ + --random-sampling_seed $random_sampling_seed \\ --cpus ${task.cpus} """ diff --git a/subworkflows/local/get_public_accessions/main.nf b/subworkflows/local/get_public_accessions/main.nf index 95ff61e5..0791c467 100644 --- a/subworkflows/local/get_public_accessions/main.nf +++ b/subworkflows/local/get_public_accessions/main.nf @@ -41,7 +41,9 @@ workflow GET_PUBLIC_ACCESSIONS { EXPRESSION_ATLAS( species, keywords, - platform?: 'none' + platform?: 'none', + random_sampling_size + random_sampling_seed ) // removing E-GTEX-* accessions by default because they are too big diff --git a/tests/modules/local/expressionatlas/getaccessions/main.nf.test b/tests/modules/local/expressionatlas/getaccessions/main.nf.test index d4d383c4..7d76987e 100644 --- a/tests/modules/local/expressionatlas/getaccessions/main.nf.test +++ b/tests/modules/local/expressionatlas/getaccessions/main.nf.test @@ -5,17 +5,38 @@ nextflow_process { process "EXPRESSIONATLAS_GETACCESSIONS" tag "eatlas_getaccessions" - test("Solanum tuberosum one keyword") { + test("Beta vulgaris one keyword - no platform") { - tag "potato_two_kw" + when { + + process { + """ + input[0] = "beta_vulgaris" + input[1] = "leaf" + input[2] = "none" + input[3] = 100 + input[4] = 42 + """ + } + } + + then { + assert process.success + } + + } + + test("Beta vulgaris no keyword - rnaseq platform") { when { process { """ - input[0] = "solanum_tuberosum" - input[1] = "potato" + input[0] = "beta_vulgaris" + input[1] = "" input[2] = "rnaseq" + input[3] = 100 + input[4] = 42 """ } } @@ -26,9 +47,28 @@ nextflow_process { } - test('Solanum tuberosum two keywords') { + test("Beta vulgaris - no experiments left after random sampling") { - tag "potato_two_kw" + when { + + process { + """ + input[0] = "beta_vulgaris" + input[1] = "" + input[2] = "none" + input[3] = 1 + input[4] = 42 + """ + } + } + + then { + assert process.success + } + + } + + test('Solanum tuberosum two keywords - microarray') { when { @@ -37,6 +77,8 @@ nextflow_process { input[0] = "solanum_tuberosum" input[1] = "potato,phloem" input[2] = "microarray" + input[3] = 10000 + input[4] = 42 """ } } @@ -52,8 +94,6 @@ nextflow_process { test('Solanum tuberosum no keyword') { - tag "potato_no_kw" - when { process { @@ -61,6 +101,8 @@ nextflow_process { input[0] = "solanum_tuberosum" input[1] = "" input[2] = "microarray" + input[3] = 100 + input[4] = 42 """ } } From bca5fc7e295fc9a1f7d20d66b81b4af822248437 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 10 Dec 2025 15:21:59 +0100 Subject: [PATCH 224/258] reinstalled appropriate version of multiqc --- modules.json | 2 +- modules/nf-core/multiqc/main.nf | 38 +++++++----- modules/nf-core/multiqc/meta.yml | 38 ++++-------- .../multiqc/tests/custom_prefix.config | 5 -- modules/nf-core/multiqc/tests/main.nf.test | 32 +--------- .../nf-core/multiqc/tests/main.nf.test.snap | 58 ++++++------------- 6 files changed, 58 insertions(+), 115 deletions(-) delete mode 100644 modules/nf-core/multiqc/tests/custom_prefix.config diff --git a/modules.json b/modules.json index da92b2de..249895b3 100644 --- a/modules.json +++ b/modules.json @@ -7,7 +7,7 @@ "nf-core": { "multiqc": { "branch": "master", - "git_sha": "0b2435805036a16dcdcf21533632d956b8273ac4", + "git_sha": "af27af1be706e6a2bb8fe454175b0cdf77f47b49", "installed_by": ["modules"] } } diff --git a/modules/nf-core/multiqc/main.nf b/modules/nf-core/multiqc/main.nf index 335afccc..c1158fb0 100644 --- a/modules/nf-core/multiqc/main.nf +++ b/modules/nf-core/multiqc/main.nf @@ -7,7 +7,7 @@ process MULTIQC { 'community.wave.seqera.io/library/multiqc:1.32--d58f60e4deb769bf' }" input: - path multiqc_files, stageAs: "?/*" + path multiqc_files, stageAs: "?/*" path(multiqc_config) path(extra_multiqc_config) path(multiqc_logo) @@ -15,10 +15,10 @@ process MULTIQC { path(sample_names) output: - path "*.html" , emit: report - path "*_data" , emit: data - path "*_plots" , optional:true, emit: plots - tuple val("${task.process}"), val('multiqc'), eval('multiqc --version | sed "s/.* //g"'), topic: versions, emit: versions_multiqc + path "*multiqc_report.html", emit: report + path "*_data" , emit: data + path "*_plots" , optional:true, emit: plots + path "versions.yml" , emit: versions when: task.ext.when == null || task.ext.when @@ -26,30 +26,38 @@ 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 ? "--config $multiqc_config" : '' + def extra_config = extra_multiqc_config ? "--config $extra_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 \\ + $extra_config \\ + $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_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 4a908611..ce30eb73 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 @@ -28,8 +28,8 @@ input: - edam: http://edamontology.org/format_3750 # YAML - extra_multiqc_config: type: file - description: Second optional config yml for MultiQC. Will override common - sections in multiqc_config. + description: Second optional config yml for MultiQC. Will override common sections + in multiqc_config. pattern: "*.{yml,yaml}" ontologies: - edam: http://edamontology.org/format_3750 # YAML @@ -57,10 +57,10 @@ input: - edam: http://edamontology.org/format_3475 # TSV output: report: - - "*.html": + - "*multiqc_report.html": type: file description: MultiQC report file - pattern: ".html" + pattern: "multiqc_report.html" ontologies: [] data: - "*_data": @@ -73,27 +73,13 @@ output: description: Plots created by MultiQC pattern: "*_data" ontologies: [] - versions_multiqc: - - - ${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: 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 - - multiqc: - type: string - description: The tool name - - multiqc --version | sed "s/.* //g: - type: string - description: The command used to generate the version of the tool + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + ontologies: + - edam: http://edamontology.org/format_3750 # YAML authors: - "@abhi18av" - "@bunop" diff --git a/modules/nf-core/multiqc/tests/custom_prefix.config b/modules/nf-core/multiqc/tests/custom_prefix.config deleted file mode 100644 index b30b1358..00000000 --- a/modules/nf-core/multiqc/tests/custom_prefix.config +++ /dev/null @@ -1,5 +0,0 @@ -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 d1ae8b06..33316a7d 100644 --- a/modules/nf-core/multiqc/tests/main.nf.test +++ b/modules/nf-core/multiqc/tests/main.nf.test @@ -30,33 +30,7 @@ nextflow_process { { assert process.success }, { assert process.out.report[0] ==~ ".*/multiqc_report.html" }, { assert process.out.data[0] ==~ ".*/multiqc_data" }, - { assert snapshot(process.out.findAll { key, val -> key.startsWith("versions")}).match() } - ) - } - - } - - test("sarscov2 single-end [fastqc] - custom prefix") { - config "./custom_prefix.config" - - 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] = [] - """ - } - } - - then { - assertAll( - { assert process.success }, - { assert process.out.report[0] ==~ ".*/custom_prefix.html" }, - { assert process.out.data[0] ==~ ".*/custom_prefix_data" } + { assert snapshot(process.out.versions).match("multiqc_versions_single") } ) } @@ -82,7 +56,7 @@ nextflow_process { { assert process.success }, { assert process.out.report[0] ==~ ".*/multiqc_report.html" }, { assert process.out.data[0] ==~ ".*/multiqc_data" }, - { assert snapshot(process.out.findAll { key, val -> key.startsWith("versions")}).match() } + { assert snapshot(process.out.versions).match("multiqc_versions_config") } ) } } @@ -110,7 +84,7 @@ nextflow_process { { 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.findAll { key, val -> key.startsWith("versions")} ).match() } + process.out.versions ).match("multiqc_stub") } ) } diff --git a/modules/nf-core/multiqc/tests/main.nf.test.snap b/modules/nf-core/multiqc/tests/main.nf.test.snap index f76049d3..a88bafd6 100644 --- a/modules/nf-core/multiqc/tests/main.nf.test.snap +++ b/modules/nf-core/multiqc/tests/main.nf.test.snap @@ -1,61 +1,41 @@ { - "sarscov2 single-end [fastqc]": { + "multiqc_versions_single": { "content": [ - { - "versions_multiqc": [ - [ - "MULTIQC", - "multiqc", - "1.32" - ] - ] - } + [ + "versions.yml:md5,737bb2c7cad54ffc2ec020791dc48b8f" + ] ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.6" + "nf-test": "0.9.3", + "nextflow": "24.10.4" }, - "timestamp": "2025-10-28T15:27:59.813370216" + "timestamp": "2025-10-27T13:33:24.356715" }, - "sarscov2 single-end [fastqc] - stub": { + "multiqc_stub": { "content": [ [ "multiqc_report.html", "multiqc_data", "multiqc_plots", - { - "versions_multiqc": [ - [ - "MULTIQC", - "multiqc", - "1.32" - ] - ] - } + "versions.yml:md5,737bb2c7cad54ffc2ec020791dc48b8f" ] ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.6" + "nf-test": "0.9.3", + "nextflow": "24.10.4" }, - "timestamp": "2025-10-28T15:30:48.963962021" + "timestamp": "2025-10-27T13:34:11.103619" }, - "sarscov2 single-end [fastqc] [config]": { + "multiqc_versions_config": { "content": [ - { - "versions_multiqc": [ - [ - "MULTIQC", - "multiqc", - "1.32" - ] - ] - } + [ + "versions.yml:md5,737bb2c7cad54ffc2ec020791dc48b8f" + ] ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.6" + "nf-test": "0.9.3", + "nextflow": "24.10.4" }, - "timestamp": "2025-10-28T15:29:30.664969334" + "timestamp": "2025-10-27T13:34:04.615233" } } \ No newline at end of file From 73635035c819a6aca94d39ddf446495177d05c34 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 10 Dec 2025 15:30:42 +0100 Subject: [PATCH 225/258] propadata random sampling to geo when not enough samples were collected from Expression Atlas --- bin/get_eatlas_accessions.py | 61 +++++-- bin/get_geo_dataset_accessions.py | 166 +++++++++++++++--- modules/local/aggregate_results/main.nf | 2 +- .../expressionatlas/getaccessions/main.nf | 10 +- modules/local/geo/getaccessions/main.nf | 12 +- .../local/get_public_accessions/main.nf | 31 +--- subworkflows/local/multiqc/main.nf | 43 +++-- .../main.nf | 9 +- .../getaccessions/main.nf.test | 4 +- .../local/geo/getaccessions/main.nf.test | 27 ++- workflows/stableexpression.nf | 8 +- 11 files changed, 274 insertions(+), 99 deletions(-) diff --git a/bin/get_eatlas_accessions.py b/bin/get_eatlas_accessions.py index 6156d440..3481c54a 100755 --- a/bin/get_eatlas_accessions.py +++ b/bin/get_eatlas_accessions.py @@ -23,6 +23,9 @@ logger = logging.getLogger(__name__) ALLOWED_PLATFORMS = ["rnaseq", "microarray"] +# accessions that should not be fetched automatically: +# - E-GTEX-8 contains 17350 samples (way too big) +EXCLUDED_ACCESSION_PATTERNS = ["E-GTEX-"] ALL_EXP_URL = "https://www.ebi.ac.uk/gxa/json/experiments/" ACCESSION_OUTFILE_NAME = "accessions.txt" @@ -297,6 +300,20 @@ def get_experiment_data(exp_dict: dict): return get_data(exp_url) +def filter_out_excluded_accessions(experiments: list[dict]) -> list[dict]: + valid_experiments = [] + for exp_dict in experiments: + for accession_pattern in EXCLUDED_ACCESSION_PATTERNS: + if exp_dict["experimentAccession"].startswith(accession_pattern): + logger.warning( + f"Skipping experiment {exp_dict['experimentAccession']} due to exclusion pattern" + ) + break + else: + valid_experiments.append(exp_dict) + return valid_experiments + + def parse_experiment(exp_dict: dict): # getting accession and description accession = get_experiment_accession(exp_dict) @@ -406,6 +423,9 @@ def main(): logger.info("Filtering experiments based on platform") experiments = filter_by_platform(experiments, args.platform) + logger.info("Filtering out excluded accessions") + experiments = filter_out_excluded_accessions(experiments) + logger.info("Parsing experiments") with Pool(processes=args.nb_cpus) as pool: results = pool.map(parse_experiment, experiments) @@ -422,25 +442,28 @@ def main(): # getting accessions of selected experiments selected_accessions = [exp_dict["accession"] for exp_dict in results] - selected_accession_to_nb_samples = [ - { - "accession": exp_dict["experimentAccession"], - "nb_samples": exp_dict["numberOfAssays"], - } - for exp_dict in experiments - if exp_dict["experimentAccession"] in selected_accessions - ] - - nb_samples_df = pd.DataFrame.from_dict(selected_accession_to_nb_samples) - nb_samples_df.to_csv("selected_accession_to_nb_samples.csv", index=False) - - logger.info("Sampling experiments randomly") - selected_accessions = sample_experiments_randomly( - selected_accession_to_nb_samples, - args.random_sampling_size, - args.random_sampling_seed, - ) - logger.info(f"Kept {len(selected_accessions)} experiments after random sampling") + if args.random_sampling_size and args.random_sampling_seed: + selected_accession_to_nb_samples = [ + { + "accession": exp_dict["experimentAccession"], + "nb_samples": exp_dict["numberOfAssays"], + } + for exp_dict in experiments + if exp_dict["experimentAccession"] in selected_accessions + ] + + nb_samples_df = pd.DataFrame.from_dict(selected_accession_to_nb_samples) + nb_samples_df.to_csv("selected_accession_to_nb_samples.csv", index=False) + + logger.info("Sampling experiments randomly") + selected_accessions = sample_experiments_randomly( + selected_accession_to_nb_samples, + args.random_sampling_size, + args.random_sampling_seed, + ) + logger.info( + f"Kept {len(selected_accessions)} experiments after random sampling" + ) # keeping metadata only for selected experiments selected_experiments = get_metadata_for_selected_experiments(experiments, results) diff --git a/bin/get_geo_dataset_accessions.py b/bin/get_geo_dataset_accessions.py index 31933c49..21383235 100755 --- a/bin/get_geo_dataset_accessions.py +++ b/bin/get_geo_dataset_accessions.py @@ -4,6 +4,7 @@ import argparse import logging +import random import tarfile from functools import partial from multiprocessing import Pool @@ -31,6 +32,8 @@ # mandatory for running the script in an apptainer container # Entrez.Parser.Parser.directory("/tmp/biopython") +ALLOWED_PLATFORMS = ["rnaseq", "microarray"] + ACCESSION_OUTFILE_NAME = "accessions.txt" SPECIES_DATASETS_OUTFILE_NAME = "geo_all_datasets.metadata.tsv" REJECTED_DATASETS_OUTFILE_NAME = "geo_rejected_datasets.metadata.tsv" @@ -104,13 +107,27 @@ def parse_args(): nargs="*", help="Keywords to search for in datasets description", ) - parser.add_argument("--platform", type=str, help="Platform type") + parser.add_argument( + "--platform", type=str, help="Platform type", choices=ALLOWED_PLATFORMS + ) parser.add_argument( "--exclude-accessions-in", dest="excluded_accessions_file", type=Path, help="Exclude accessions contained in this file", ) + parser.add_argument( + "--random-sampling-size", + dest="random_sampling_size", + type=int, + help="Random sampling size", + ) + parser.add_argument( + "--random-sampling-seed", + dest="random_sampling_seed", + type=int, + help="Random sampling seed", + ) parser.add_argument( "--cpus", dest="nb_cpus", @@ -467,16 +484,16 @@ def fetch_dataset_metadata(dataset_metadata: dict) -> dict | None: def exclude_unwanted_accessions( - datasets: list[dict], excluded_accessions_file: Path -) -> list[dict]: - # parsing list of unwanted accessions - with open(excluded_accessions_file) as fin: - excluded_accessions = fin.read().splitlines() + datasets: list[dict], excluded_accessions: list[str] +) -> tuple[list[dict], list[dict]]: datasets_to_keep = [] + excluded_datasets = [] for dataset in datasets: - if dataset["Accession"] not in excluded_accessions: + if dataset["accession"] in excluded_accessions: + excluded_datasets.append(dataset) + else: datasets_to_keep.append(dataset) - return datasets_to_keep + return datasets_to_keep, excluded_datasets def check_species_issues(parsed_species_list: list, species: str) -> str | None: @@ -661,6 +678,47 @@ def check_dataset_platforms( return {} +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# RANDOM SAMPLING +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +def sample_experiments_randomly( + experiments: list[dict], sampling_size: int, seed: int +) -> list[str]: + random.seed(seed) + sampled_experiments = [] + + total_nb_samples = 0 + experiments_left = list(experiments) + while experiments_left and total_nb_samples <= sampling_size: + # if the min number of samples is greater than the remaining space left, we get out of the loop + experiments_left_nb_samples = [exp["nb_samples"] for exp in experiments_left] + min_nb_samples = min(experiments_left_nb_samples) + if min_nb_samples > sampling_size - total_nb_samples: + break + + found_experiment = False + test_total_nb_samples = int(total_nb_samples) + not_chosen_yet = list(experiments_left) + while not_chosen_yet and not found_experiment: + experiment = random.choice(not_chosen_yet) + not_chosen_yet.remove(experiment) + test_total_nb_samples = total_nb_samples + experiment["nb_samples"] + if test_total_nb_samples <= sampling_size: + found_experiment = True + + # if the last one was not good, it means we reached the limit of samples we can take + if not found_experiment: + break + else: + total_nb_samples = test_total_nb_samples + experiments_left.remove(experiment) + sampled_experiments.append(experiment) + + return [exp["accession"] for exp in sampled_experiments] + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # EXPORT # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -705,6 +763,7 @@ def export_dataset_metadatas( def main(): args = parse_args() + random_sampling_size = args.random_sampling_size # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # PARSING GEO DATASETS @@ -724,17 +783,6 @@ def main(): datasets = [d for d in datasets if d["Accession"] in dev_accessions] logger.info(f"Kept {len(datasets)} datasets for dev / testing purposes") - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # EXCLUDING UNWANTED ACCESSIONS - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - if args.excluded_accessions_file: - logger.info("Excluding unwanted datasets") - datasets = exclude_unwanted_accessions(datasets, args.excluded_accessions_file) - logger.info( - f"{len(datasets)} datasets remaining after excluding unwanted accessions" - ) - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # PARSING DATASET METADATA # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -772,6 +820,41 @@ def main(): logger.info(f"Validated {len(checked_datasets)} datasets") + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # EXCLUDING UNWANTED ACCESSIONS + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + # we exclude unwanted accessions only now + # because we want to get the metadata of the excluded datasets + # in order to adjust the random sampling size + if args.excluded_accessions_file: + # parsing list of accessions which were already fetched from Expression Atlas + with open(args.excluded_accessions_file) as fin: + excluded_accessions = fin.read().splitlines() + logger.info("Excluding unwanted datasets") + checked_datasets, excluded_datasets = exclude_unwanted_accessions( + checked_datasets, excluded_accessions + ) + logger.info( + f"{len(checked_datasets)} datasets remaining after excluding unwanted accessions" + ) + + # adjusting random sampling size by substracting the number of excluded accessions + if random_sampling_size: + total_nb_excluded_samples = sum( + [len(dataset["sample_titles"]) for dataset in excluded_datasets] + ) + logger.info( + f"Subtracting {total_nb_excluded_samples} samples from random sampling size" + ) + random_sampling_size -= total_nb_excluded_samples + # keeping it positive (just in case) + if random_sampling_size < 0: + logger.warning( + f"Random sampling size is negative ({random_sampling_size}), setting it to 0" + ) + random_sampling_size = 0 + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # GETTING METADATA OF SEQUENCING PLATFORMS # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -813,16 +896,51 @@ def main(): logger.warning(f"{len(rejection_dict)} datasets rejected") logger.warning(f"Reasons for rejection: {rejection_dict}") + selected_accessions = sorted( + [dataset["accession"] for dataset in selected_datasets] + ) + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # RANDOM SAMPLING + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + if random_sampling_size is not None and args.random_sampling_seed is not None: + selected_accession_to_nb_samples = [ + { + "accession": dataset["accession"], + "nb_samples": len(dataset["sample_titles"]), + } + for dataset in selected_datasets + ] + + nb_samples_df = pd.DataFrame.from_dict(selected_accession_to_nb_samples) + nb_samples_df.to_csv("selected_accession_to_nb_samples.csv", index=False) + + logger.info("Sampling experiments randomly") + selected_accessions = sample_experiments_randomly( + selected_accession_to_nb_samples, + random_sampling_size, + args.random_sampling_seed, + ) + logger.info( + f"Kept {len(selected_accessions)} experiments after random sampling" + ) + selected_datasets = [ + dataset + for dataset in selected_datasets + if dataset["accession"] in selected_accessions + ] + else: + logger.info( + f"No random sampling requested. Kept {len(selected_datasets)} datasets" + ) + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # EXPORTING ACCESSIONS # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - logger.info(f"Kept {len(selected_datasets)} datasets") - # getting accessions of selected experiments # sorting accessions to ensure that outputs are reproducible - selected_accessions = sorted( - [dataset["accession"] for dataset in selected_datasets] - ) + selected_accessions = sorted(selected_accessions) with open(ACCESSION_OUTFILE_NAME, "w") as fout: fout.write("\n".join(selected_accessions)) diff --git a/modules/local/aggregate_results/main.nf b/modules/local/aggregate_results/main.nf index d139b508..3b806b22 100644 --- a/modules/local/aggregate_results/main.nf +++ b/modules/local/aggregate_results/main.nf @@ -41,7 +41,7 @@ process AGGREGATE_RESULTS { $mapping_files_arg \\ $metadata_files_arg \\ $rnaseq_dataset_stat_file_arg \\ - $microarray_dataset_stat_file_arg \\ + $microarray_dataset_stat_file_arg """ } diff --git a/modules/local/expressionatlas/getaccessions/main.nf b/modules/local/expressionatlas/getaccessions/main.nf index da476e0d..fca62f2e 100644 --- a/modules/local/expressionatlas/getaccessions/main.nf +++ b/modules/local/expressionatlas/getaccessions/main.nf @@ -34,17 +34,21 @@ process EXPRESSIONATLAS_GETACCESSIONS { if ( keywords_string != "" ) { args += " --keywords $keywords_string" } - if ( platform != 'none' ) { + if ( platform ) { args += " --platform $platform" } + if ( random_sampling_size ) { + args += " --random-sampling-size $random_sampling_size" + } + if ( random_sampling_seed ) { + args += " --random-sampling-seed $random_sampling_seed" + } """ # the folder where nltk will download data needs to be writable (necessary for singularity) export NLTK_DATA=\${PWD} get_eatlas_accessions.py \\ $args \\ - --random-sampling_size $random_sampling_size \\ - --random-sampling_seed $random_sampling_seed \\ --cpus ${task.cpus} """ diff --git a/modules/local/geo/getaccessions/main.nf b/modules/local/geo/getaccessions/main.nf index ef362848..bf8ac3e9 100644 --- a/modules/local/geo/getaccessions/main.nf +++ b/modules/local/geo/getaccessions/main.nf @@ -14,6 +14,8 @@ process GEO_GETACCESSIONS { val keywords val platform path excluded_accessions_file + val random_sampling_size + val random_sampling_seed output: path "accessions.txt", optional: true, emit: accessions @@ -34,12 +36,18 @@ process GEO_GETACCESSIONS { if ( keywords_string != "" ) { args += " --keywords $keywords_string" } - if ( platform != 'none' ) { + if ( platform ) { args += " --platform $platform" } - if ( excluded_accessions_file != [] ) { + if ( excluded_accessions_file ) { args += " --exclude-accessions-in $excluded_accessions_file" } + if ( random_sampling_size ) { + args += " --random-sampling-size $random_sampling_size" + } + if ( random_sampling_seed ) { + args += " --random-sampling-seed $random_sampling_seed" + } // the folder where nltk will download data needs to be writable (necessary for singularity) """ # the Entrez module from biopython automatically stores temp results in /.config diff --git a/subworkflows/local/get_public_accessions/main.nf b/subworkflows/local/get_public_accessions/main.nf index 0791c467..6f94db65 100644 --- a/subworkflows/local/get_public_accessions/main.nf +++ b/subworkflows/local/get_public_accessions/main.nf @@ -41,16 +41,13 @@ workflow GET_PUBLIC_ACCESSIONS { EXPRESSION_ATLAS( species, keywords, - platform?: 'none', - random_sampling_size - random_sampling_seed + platform?: [], + random_sampling_size?: [], + random_sampling_seed?: [] ) - // removing E-GTEX-* accessions by default because they are too big - // however, contrary to E-PROT- accessions, they can be added by the user - ch_fetched_eatlas_accessions = EXPRESSION_ATLAS.out.accessions - .splitText() - .filter { acc -> !acc.startsWith('E-GTEX-') } + ch_fetched_eatlas_accessions = EXPRESSION_ATLAS.out.accessions.splitText() + } // ------------------------------------------------------------------------------------ @@ -78,8 +75,10 @@ workflow GET_PUBLIC_ACCESSIONS { GEO( species, keywords, - platform ?: 'none', - ch_excluded_eatlas_accessions_file + platform?: [], + ch_excluded_eatlas_accessions_file, + random_sampling_size?: [], + random_sampling_seed?: [] ) ch_fetched_geo_accessions = GEO.out.accessions.splitText() @@ -107,18 +106,6 @@ workflow GET_PUBLIC_ACCESSIONS { .filter { accession, excluded_accessions -> !(accession in excluded_accessions) } .map { accession, excluded_accessions -> accession } - // ----------------------------------------------------------------- - // IF NECESSARY, SUBSAMPLE RANDOMLY THE ACCESSIONS - // ----------------------------------------------------------------- - - if ( random_sampling_size != null ) { - if ( random_sampling_seed != null ) { - ch_fetched_public_accessions = ch_fetched_public_accessions.randomSample( random_sampling_size, random_sampling_seed ) - } else { - ch_fetched_public_accessions = ch_fetched_public_accessions.randomSample( random_sampling_size ) - } - } - // ----------------------------------------------------------------- // ADDING USER PROVIDED ACCESSIONS // ----------------------------------------------------------------- diff --git a/subworkflows/local/multiqc/main.nf b/subworkflows/local/multiqc/main.nf index 34e8678a..d0fb32f4 100644 --- a/subworkflows/local/multiqc/main.nf +++ b/subworkflows/local/multiqc/main.nf @@ -228,29 +228,42 @@ workflow MULTIQC_WORKFLOW { // CONFIG // ------------------------------------------------------------------------------------ - summary_params = paramsSummaryMap( workflow, parameters_schema: "nextflow_schema.json") + 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) - channel.value( methodsDescriptionText( ch_multiqc_custom_methods_description ) ) - .collectFile( - name: 'methods_description_mqc.yaml', - sort: true - ) - .set { ch_methods_description_file } + ch_methods_description = channel.value( + methodsDescriptionText(ch_multiqc_custom_methods_description) + ) - ch_multiqc_files - .mix( ch_workflow_summary.collectFile(name: 'workflow_summary_mqc.yaml') ) + ch_multiqc_files = ch_multiqc_files .mix( ch_collated_versions ) - .mix( ch_methods_description_file ) - .set { ch_multiqc_files } - - 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() + .mix( + ch_methods_description.collectFile( + name: 'methods_description_mqc.yaml', + sort: true + ) + ) MULTIQC ( ch_multiqc_files.collect(), diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index e9b2d1dd..acd88b17 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -421,15 +421,16 @@ def storeDatasetSize( ch_counts, nb_genes_key, nb_samples_key ) { def checkCounts(ch_counts) { // display a warning if no datasets are found def msg = [ - "No dataset found. ", - "You may want to check at https://www.ncbi.nlm.nih.gov/gds if there are datasets for this species that you can prepare yourself. ", - "Once you have prepared your own data, you can relaunch the pipeline with the --datasets parameter. ", + "Could not find any readily usable public dataset.", + "You can check directly on NCBI GEO if there are datasets for this species that you can prepare yourself:", + "https://www.ncbi.nlm.nih.gov/gds", + "Once you have prepared your own data, you can relaunch the pipeline and provided your prepared count datasets using the --datasets parameter. ", "For more information, see the online documentation at https://nf-co.re/stableexpression." ].join("\n").trim() ch_counts.count().map { n -> if( n == 0 ) { - log.warn(msg) + error(msg) } } } diff --git a/tests/modules/local/expressionatlas/getaccessions/main.nf.test b/tests/modules/local/expressionatlas/getaccessions/main.nf.test index 7d76987e..a5e4f860 100644 --- a/tests/modules/local/expressionatlas/getaccessions/main.nf.test +++ b/tests/modules/local/expressionatlas/getaccessions/main.nf.test @@ -13,7 +13,7 @@ nextflow_process { """ input[0] = "beta_vulgaris" input[1] = "leaf" - input[2] = "none" + input[2] = [] input[3] = 100 input[4] = 42 """ @@ -55,7 +55,7 @@ nextflow_process { """ input[0] = "beta_vulgaris" input[1] = "" - input[2] = "none" + input[2] = [] input[3] = 1 input[4] = 42 """ diff --git a/tests/modules/local/geo/getaccessions/main.nf.test b/tests/modules/local/geo/getaccessions/main.nf.test index 85e50ff2..deddb432 100644 --- a/tests/modules/local/geo/getaccessions/main.nf.test +++ b/tests/modules/local/geo/getaccessions/main.nf.test @@ -12,8 +12,10 @@ nextflow_process { """ input[0] = "beta_vulgaris" input[1] = "" - input[2] = "none" + input[2] = [] input[3] = file( '$projectDir/tests/test_data/public_accessions/exclude_one_geo_accession.txt', checkIfExists: true ) + input[4] = 100 + input[5] = 42 """ } } @@ -33,6 +35,29 @@ nextflow_process { input[1] = "leaf" input[2] = "microarray" input[3] = [] + input[4] = 100 + input[5] = 42 + """ + } + } + + then { + assert process.success + } + + } + + test("Beta vulgaris - leaf / microarray") { + + when { + process { + """ + input[0] = "beta_vulgaris" + input[1] = "leaf" + input[2] = "microarray" + input[3] = [] + input[4] = 100 + input[5] = 42 """ } } diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 4f199e40..4db97b44 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -82,11 +82,9 @@ workflow STABLEEXPRESSION { } ch_counts = ch_input_datasets.mix( ch_downloaded_datasets ) - // store nb of genes and nb f samples at this stage in the meta maps ch_counts = storeDatasetSize( ch_counts, "nb_genes", "nb_samples" ) - - // displays a message if no dataset was found + // returns an error with a message if no dataset was found checkCounts( ch_counts ) if ( !params.accessions_only && !params.download_only ) { @@ -215,11 +213,9 @@ workflow STABLEEXPRESSION { ch_versions ) - MULTIQC_WORKFLOW.out.report.toList().set { multiqc_report } - emit: - multiqc_report + multiqc_report = MULTIQC_WORKFLOW.out.report.toList() } From b8eda8eb77fd1b2713f87675b279cd46468e6f59 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Wed, 10 Dec 2025 23:27:26 +0100 Subject: [PATCH 226/258] set sampling quota in geo --- bin/get_eatlas_accessions.py | 48 ++++++++++++------- .../expressionatlas/getaccessions/main.nf | 5 ++ nextflow.config | 2 +- .../local/get_public_accessions/main.nf | 10 +++- 4 files changed, 46 insertions(+), 19 deletions(-) diff --git a/bin/get_eatlas_accessions.py b/bin/get_eatlas_accessions.py index 3481c54a..b2aa2f53 100755 --- a/bin/get_eatlas_accessions.py +++ b/bin/get_eatlas_accessions.py @@ -34,6 +34,8 @@ SELECTED_EXPERIMENTS_METADATA_OUTFILE_NAME = "selected_experiments.metadata.tsv" FILTERED_EXPERIMENTS_WITH_KEYWORDS_OUTFILE_NAME = "filtered_experiments.keywords.yaml" +SAMPLING_QUOTA_OUTFILE = "sampling_quota.txt" + ################################################################## ################################################################## @@ -353,38 +355,44 @@ def get_metadata_for_selected_experiments( def sample_experiments_randomly( experiments: list[dict], sampling_size: int, seed: int -) -> list[str]: +) -> tuple[list[str], bool]: random.seed(seed) sampled_experiments = [] total_nb_samples = 0 + sampling_quota_reached = False experiments_left = list(experiments) - while experiments_left and total_nb_samples <= sampling_size: + while experiments_left: # if the min number of samples is greater than the remaining space left, we get out of the loop experiments_left_nb_samples = [exp["nb_samples"] for exp in experiments_left] min_nb_samples = min(experiments_left_nb_samples) if min_nb_samples > sampling_size - total_nb_samples: + sampling_quota_reached = True + logger.warning("Sampling quota reached") break - found_experiment = False + experiment = None test_total_nb_samples = int(total_nb_samples) - not_chosen_yet = list(experiments_left) - while not_chosen_yet and not found_experiment: - experiment = random.choice(not_chosen_yet) - not_chosen_yet.remove(experiment) + experiments_not_tested = list(experiments_left) + while experiments_not_tested: + experiment = random.choice(experiments_not_tested) + experiments_not_tested.remove(experiment) + # if we do not exceed the sampling size with this experiment + # we keep it test_total_nb_samples = total_nb_samples + experiment["nb_samples"] if test_total_nb_samples <= sampling_size: - found_experiment = True + break - # if the last one was not good, it means we reached the limit of samples we can take - if not found_experiment: - break - else: - total_nb_samples = test_total_nb_samples - experiments_left.remove(experiment) - sampled_experiments.append(experiment) + # this should not happen but we keep it for safety + if experiment is None: + logger.error("No experiment found") + continue + + total_nb_samples = test_total_nb_samples + experiments_left.remove(experiment) + sampled_experiments.append(experiment) - return [exp["accession"] for exp in sampled_experiments] + return [exp["accession"] for exp in sampled_experiments], sampling_quota_reached def format_species_name(species: str) -> str: @@ -456,7 +464,7 @@ def main(): nb_samples_df.to_csv("selected_accession_to_nb_samples.csv", index=False) logger.info("Sampling experiments randomly") - selected_accessions = sample_experiments_randomly( + selected_accessions, sampling_quota_reached = sample_experiments_randomly( selected_accession_to_nb_samples, args.random_sampling_size, args.random_sampling_seed, @@ -465,6 +473,12 @@ def main(): f"Kept {len(selected_accessions)} experiments after random sampling" ) + # writing status to file + # so that the wrapper module can get the status + with open(SAMPLING_QUOTA_OUTFILE, "w") as fout: + sampling_status = "full" if sampling_quota_reached else "ok" + fout.write(sampling_status) + # keeping metadata only for selected experiments selected_experiments = get_metadata_for_selected_experiments(experiments, results) diff --git a/modules/local/expressionatlas/getaccessions/main.nf b/modules/local/expressionatlas/getaccessions/main.nf index fca62f2e..0ce4133c 100644 --- a/modules/local/expressionatlas/getaccessions/main.nf +++ b/modules/local/expressionatlas/getaccessions/main.nf @@ -18,6 +18,7 @@ process EXPRESSIONATLAS_GETACCESSIONS { output: path "accessions.txt", optional: true, emit: accessions + env("SAMPLING_QUOTA"), emit: sampling_quota path "selected_experiments.metadata.tsv", optional: true, topic: eatlas_selected_datasets path "species_experiments.metadata.tsv", optional: true, topic: eatlas_all_datasets //path "filtered_experiments.metadata.tsv", optional: true, topic: filtered_eatlas_experiment_metadata @@ -50,6 +51,8 @@ process EXPRESSIONATLAS_GETACCESSIONS { get_eatlas_accessions.py \\ $args \\ --cpus ${task.cpus} + + SAMPLING_QUOTA=\$(cat sampling_quota.txt) """ stub: @@ -58,6 +61,8 @@ process EXPRESSIONATLAS_GETACCESSIONS { all_experiments.metadata.tsv \\ filtered_experiments.metadata.tsv \\ filtered_experiments.keywords.yaml + + SAMPLING_QUOTA="ok" """ } diff --git a/nextflow.config b/nextflow.config index dd277529..25e893c7 100644 --- a/nextflow.config +++ b/nextflow.config @@ -50,7 +50,7 @@ params { // random sampling random_sampling_seed = 42 - random_sampling_size = null + random_sampling_size = 10000 // MultiQC options multiqc_config = null diff --git a/subworkflows/local/get_public_accessions/main.nf b/subworkflows/local/get_public_accessions/main.nf index 6f94db65..65bb87af 100644 --- a/subworkflows/local/get_public_accessions/main.nf +++ b/subworkflows/local/get_public_accessions/main.nf @@ -28,6 +28,7 @@ workflow GET_PUBLIC_ACCESSIONS { ch_fetched_eatlas_accessions = channel.empty() ch_fetched_geo_accessions = channel.empty() + ch_sampling_quota = channel.of( "ok" ) // ----------------------------------------------------------------- // GET EATLAS ACCESSIONS @@ -47,6 +48,7 @@ workflow GET_PUBLIC_ACCESSIONS { ) ch_fetched_eatlas_accessions = EXPRESSION_ATLAS.out.accessions.splitText() + ch_sampling_quota = EXPRESSION_ATLAS.out.sampling_quota } @@ -70,10 +72,16 @@ workflow GET_PUBLIC_ACCESSIONS { ) .ifEmpty( [] ) + // trick to avoid fetching accessions from GEO when the sampling quota is already exceeded + ch_species = channel.of( species ) + .combine( ch_sampling_quota ) + .filter { species, quota -> quota == "ok" } + .map { species, quota -> species } + // getting GEO accessions given a species name and keywords // keywords can be an empty string GEO( - species, + ch_species, keywords, platform?: [], ch_excluded_eatlas_accessions_file, From d5a4a8b0c5b83aede8900ce37bb7d14e74d6e747 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Thu, 11 Dec 2025 08:27:59 +0100 Subject: [PATCH 227/258] set fetching geo accessions as optional --- conf/test_dataset_eatlas.config | 2 +- docs/usage.md | 7 +- nextflow.config | 3 +- nextflow_schema.json | 18 +-- .../local/get_public_accessions/main.nf | 7 +- .../main.nf | 2 +- tests/default.nf.test | 8 +- .../local/get_public_accessions/main.nf.test | 106 ++++++++---------- workflows/stableexpression.nf | 3 +- 9 files changed, 65 insertions(+), 91 deletions(-) diff --git a/conf/test_dataset_eatlas.config b/conf/test_dataset_eatlas.config index 6d9ae3a2..c46350ad 100644 --- a/conf/test_dataset_eatlas.config +++ b/conf/test_dataset_eatlas.config @@ -19,7 +19,7 @@ params { species = 'mus_musculus' accessions = "E-MTAB-2262" skip_fetch_eatlas_accessions = true - skip_fetch_geo_accessions = true + fetch_geo_accessions = false datasets = 'https://raw.githubusercontent.com/nf-core/test-datasets/stableexpression/test_data/input_datasets/input_big.yaml' outdir = "results/test_dataset_eatlas" } diff --git a/docs/usage.md b/docs/usage.md index 6ad08f44..36e47f82 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -56,7 +56,6 @@ nextflow run nf-core/stableexpression \ -profile \ --species \ --skip_fetch_eatlas_accessions \ - --skip_fetch_geo_accessions \ [--eatlas_accessions ] \ [--eatlas_accessions_file ] \ [--geo_accessions ] \ @@ -65,7 +64,7 @@ nextflow run nf-core/stableexpression \ ``` > [!WARNING] -> If you want to download only the datasets corresponding to the accessions supplied, you must set the `--skip_fetch_eatlas_accessions` and `--skip_fetch_geo_accessions`. +> If you want to download only the datasets corresponding to the accessions supplied, you must set the `--skip_fetch_eatlas_accessions` parameter. > [!NOTE] > If you provide accessions through `--eatlas_accessions_file` or `--geo_accessions_file`, there must be one accession per line. The extension of the file does not matter. @@ -164,12 +163,11 @@ nextflow run nf-core/stableexpression \ --species \ --datasets \ --skip_fetch_eatlas_accessions \ - --skip_fetch_geo_accessions \ --outdir ``` > [!TIP] -> The `--skip_fetch_eatlas_accessions` and `--skip_fetch_geo_accessions` parameters are supplied here to show how to analyse **only your own dataset**. You may remove these parameters if you want to mix you dataset(s) with public ones. +> The `--skip_fetch_eatlas_accessions` parameter is supplied here to show how to analyse **only your own dataset**. You may remove this parameter if you want to mix you dataset(s) with public ones. > [!IMPORTANT] > By default, the pipeline tries to map gene IDs to NCBI Entrez Gene IDs. **All genes that cannot be mapped are discarded from the analysis**. This ensures that all genes are named the same between datasets and allows comparing multiple datasets with each other. If you are confident that your genes have the same name between your different datasets or if you think that your gene IDs won't be mapped properly, you can disable this mapping by adding the `--skip_id_mapping` parameter. In such case, you may supply your own gene id mapping file and gene metadata file with the `--gene_id_mapping` and `--gene_metadata` parameters respectively. See [next section](#5-custom-gene-id-mapping-and-metadata) for further details. @@ -190,7 +188,6 @@ nextflow run nf-core/stableexpression \ --gene_id_mapping \ --gene_metadata \ --skip_fetch_eatlas_accessions \ - --skip_fetch_geo_accessions \ --outdir ``` diff --git a/nextflow.config b/nextflow.config index 25e893c7..943039e1 100644 --- a/nextflow.config +++ b/nextflow.config @@ -22,9 +22,8 @@ params { datasets = null // Expression atlas - skip_fetch_public_accessions = false skip_fetch_eatlas_accessions = false - skip_fetch_geo_accessions = false + fetch_geo_accessions = false accessions = "" excluded_accessions = "" accessions_file = null diff --git a/nextflow_schema.json b/nextflow_schema.json index 120f54d4..8ef0cbe5 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -33,7 +33,7 @@ "schema": "assets/schema_datasets.json", "pattern": "^\\S+\\.(csv|yaml|yml|dat)$", "description": "Custom datasets (counts + designs)", - "help_text": "Path to CSV / YAML file listing your own count datasets and their related experimental design. This file should be a comma-separated file with 4 columns (`counts`, `design`, `platform` and `normalised`). It must have a header row. Before running the pipeline, and for each count dataset provided by you, a design file with information about the samples in your experiment is required. Combine with both --skip_fetch_eatlas_accessions and --skip_fetch_geo_accessions if you only want to analyse your own count datasets. Otherwise, accessions from Expression Atlas and GEO will be fetched automatically. See [usage docs](https://nf-co.re/stableexpression/usage#samplesheet-input) for more information. ", + "help_text": "Path to CSV / YAML file listing your own count datasets and their related experimental design. This file should be a comma-separated file with 4 columns (`counts`, `design`, `platform` and `normalised`). It must have a header row. Before running the pipeline, and for each count dataset provided by you, a design file with information about the samples in your experiment is required. Combine with --skip_fetch_eatlas_accessions if you only want to analyse your own count datasets. Otherwise, accessions from Expression Atlas and GEO will be fetched automatically. See [usage docs](https://nf-co.re/stableexpression/usage#samplesheet-input) for more information. ", "fa_icon": "fas fa-file-csv" }, "keywords": { @@ -41,7 +41,7 @@ "description": "Keywords used for selecting specific Expression Atlas / GEO accessions", "fa_icon": "fas fa-font", "pattern": "(([a-zA-Z,]+))?", - "help_text": "Keywords (separated by commas) to use when retrieving specific experiments from Expression Atlas and / or GEO datasets. The pipeline will select all Expression Atlas experiments / GEO datasets that contain the provided keywords in their description of in one of the condition names. Example: `--keywords 'stress,flowering'`. This parameter is unused if both --skip_fetch_eatlas_accessions and --skip_fetch_geo_accessions are set." + "help_text": "Keywords (separated by commas) to use when retrieving specific experiments from Expression Atlas and / or GEO datasets. The pipeline will select all Expression Atlas experiments / GEO datasets that contain the provided keywords in their description of in one of the condition names. Example: `--keywords 'stress,flowering'`. This parameter is unused if --skip_fetch_eatlas_accessions is set and --fetch_geo_accessions is not set." }, "platform": { "type": "string", @@ -82,30 +82,24 @@ "fa_icon": "fas fa-book-atlas", "description": "Options for fetching experiment data from Expression Atlas / GEO.", "properties": { - "skip_fetch_public_accessions": { - "type": "boolean", - "fa_icon": "fas fa-cloud-arrow-down", - "description": "Skip fetching public accessions", - "help_text": "Expression Atlas / GEO accessions are automatically fetched by default. Set this parameter to skip this step. Equivalent to `--skip_fetch_eatlas_accessions --skip_fetch_geo_accessions`" - }, "skip_fetch_eatlas_accessions": { "type": "boolean", "fa_icon": "fas fa-cloud-arrow-down", "description": "Skip fetching Expression Atlas accessions", "help_text": "Expression Atlas accessions are automatically fetched by default. Set this parameter to skip this step." }, - "skip_fetch_geo_accessions": { + "fetch_geo_accessions": { "type": "boolean", "fa_icon": "fas fa-cloud-arrow-down", - "description": "Skip fetching GEO accessions", - "help_text": "GEO accessions are automatically fetched by default. Set this parameter to skip this step." + "description": "Fetch GEO accessions from NCBI", + "help_text": "Set this parameter to fetch GEO accessions from NCBI. This feature is experimental and may not work as expected. Please report any issues to https://github.com/nf-core/stableexpression." }, "accessions": { "type": "string", "pattern": "([A-Z0-9-]+,?)+", "description": "Expression Atlas / GEO accession(s) to include", "fa_icon": "fas fa-address-card", - "help_text": "Provide Expression Atlas / GEO accession(s) that you want to download. The accessions should be comma-separated. Example: `--accessions E-MTAB-552,E-GEOD-61690,GSE8165,GSE8161`. Combine with --skip_fetch_accessions if you want only these accessions to be used. User provided accessions are prioritised over excluded accessions." + "help_text": "Provide Expression Atlas / GEO accession(s) that you want to download. The accessions should be comma-separated. Example: `--accessions E-MTAB-552,E-GEOD-61690,GSE8165,GSE8161`. Combine with --skip_fetch_eatlas_accessions if you want only these accessions to be used. User provided accessions are prioritised over excluded accessions." }, "accessions_file": { "type": "string", diff --git a/subworkflows/local/get_public_accessions/main.nf b/subworkflows/local/get_public_accessions/main.nf index 65bb87af..32fd76a9 100644 --- a/subworkflows/local/get_public_accessions/main.nf +++ b/subworkflows/local/get_public_accessions/main.nf @@ -11,9 +11,8 @@ workflow GET_PUBLIC_ACCESSIONS { take: species - skip_fetch_public_accessions skip_fetch_eatlas_accessions - skip_fetch_geo_accessions + fetch_geo_accessions platform keywords ch_accessions @@ -35,7 +34,7 @@ workflow GET_PUBLIC_ACCESSIONS { // ----------------------------------------------------------------- // fetching Expression Atlas accessions if applicable - if ( !skip_fetch_public_accessions && !skip_fetch_eatlas_accessions ) { + if ( !skip_fetch_eatlas_accessions ) { // getting Expression Atlas accessions given a species name and keywords // keywords can be an empty string @@ -57,7 +56,7 @@ workflow GET_PUBLIC_ACCESSIONS { // ------------------------------------------------------------------------------------ // fetching GEO accessions if applicable - if ( !skip_fetch_public_accessions && !skip_fetch_geo_accessions ) { + if ( fetch_geo_accessions ) { // all Expression Atlas accessions starting with E-GEOD- are imported from GEO // we do not want to collect these GEO data if we already get them from Expression Atlas diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index acd88b17..f8de5372 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -205,7 +205,7 @@ def validateInputParameters(params) { check_accession_file( params.accessions_file ) check_accession_file( params.excluded_accessions_file ) - if ( params.keywords && ( params.skip_fetch_public_accessions || ( params.skip_fetch_eatlas_accessions && params.skip_fetch_geo_accessions ) ) ) { + if ( params.keywords && params.skip_fetch_eatlas_accessions && !params.fetch_geo_accessions ) { log.warn "Ignoring keywords as accessions will not be fetched from Expression Atlas or GEO" } diff --git a/tests/default.nf.test b/tests/default.nf.test index 9eb232e4..7c5438ed 100644 --- a/tests/default.nf.test +++ b/tests/default.nf.test @@ -13,6 +13,7 @@ nextflow_pipeline { species = 'beta vulgaris' keywords = "leaf" datasets = "https://raw.githubusercontent.com/nf-core/test-datasets/stableexpression/test_data/input_datasets/input_beta_vulgaris.csv" + fetch_geo_accessions = true outdir = "$outputDir" } } @@ -38,7 +39,6 @@ nextflow_pipeline { species = 'mus musculus' datasets = "https://raw.githubusercontent.com/nf-core/test-datasets/stableexpression/test_data/input_datasets/input_big.yaml" skip_fetch_eatlas_accessions = true - skip_fetch_geo_accessions = true outdir = "$outputDir" } } @@ -128,7 +128,6 @@ nextflow_pipeline { species = 'arabidopsis thaliana' eatlas_accessions = "E-GEOD-51720" skip_fetch_eatlas_accessions = true - skip_fetch_geo_accessions = true outdir = "$outputDir" } } @@ -154,7 +153,6 @@ nextflow_pipeline { params { species = 'beta vulgaris' keywords = "leaf" - skip_fetch_geo_accessions = true outdir = "$outputDir" } } @@ -182,7 +180,6 @@ nextflow_pipeline { datasets = "${projectDir}/tests/test_data/input_datasets/input.csv" skip_id_mapping = true skip_fetch_eatlas_accessions = true - skip_fetch_geo_accessions = true outdir = "$outputDir" } } @@ -210,7 +207,6 @@ nextflow_pipeline { datasets = "${projectDir}/tests/test_data/input_datasets/input.csv" skip_id_mapping = true skip_fetch_eatlas_accessions = true - skip_fetch_geo_accessions = true gene_id_mapping = "${projectDir}/tests/test_data/input_datasets/mapping.csv" gene_metadata = "${projectDir}/tests/test_data/input_datasets/metadata.csv" outdir = "$outputDir" @@ -263,8 +259,6 @@ nextflow_pipeline { test("-profile test_gprofiler_target_database_entrez") { - tag "test" - when { params { species = 'beta vulgaris' diff --git a/tests/subworkflows/local/get_public_accessions/main.nf.test b/tests/subworkflows/local/get_public_accessions/main.nf.test index 08220066..01f11274 100644 --- a/tests/subworkflows/local/get_public_accessions/main.nf.test +++ b/tests/subworkflows/local/get_public_accessions/main.nf.test @@ -5,15 +5,14 @@ nextflow_workflow { workflow "GET_PUBLIC_ACCESSIONS" - test("Fetch public accessions without keywords") { + test("Fetch eatlas accessions without keywords") { when { params { species = 'beta vulgaris' - skip_fetch_public_accessions = false skip_fetch_eatlas_accessions = false - skip_fetch_geo_accessions = false + fetch_geo_accessions = false platform = null keywords = "" accessions = "" @@ -28,18 +27,17 @@ nextflow_workflow { workflow { """ input[0] = channel.value( params.species.split(' ').join('_') ) - input[1] = params.skip_fetch_public_accessions - input[2] = params.skip_fetch_eatlas_accessions - input[3] = params.skip_fetch_geo_accessions - input[4] = params.platform - input[5] = params.keywords - input[6] = channel.fromList( params.accessions.tokenize(',') ) - input[7] = params.accessions_file ? channel.fromPath(params.accessions_file, checkIfExists: true) : channel.empty() - input[8] = channel.fromList( params.excluded_accessions.tokenize(',') ) - input[9] = params.excluded_accessions_file ? channel.fromPath(params.excluded_accessions_file, checkIfExists: true) : channel.empty() - input[10] = params.random_sampling_size - input[11] = params.random_sampling_seed - input[12] = params.outdir + input[1] = params.skip_fetch_eatlas_accessions + input[2] = params.fetch_geo_accessions + input[3] = params.platform + input[4] = params.keywords + input[5] = channel.fromList( params.accessions.tokenize(',') ) + input[6] = params.accessions_file ? channel.fromPath(params.accessions_file, checkIfExists: true) : channel.empty() + input[7] = channel.fromList( params.excluded_accessions.tokenize(',') ) + input[8] = params.excluded_accessions_file ? channel.fromPath(params.excluded_accessions_file, checkIfExists: true) : channel.empty() + input[9] = params.random_sampling_size + input[10] = params.random_sampling_seed + input[11] = params.outdir """ } } @@ -59,9 +57,8 @@ nextflow_workflow { params { species = 'beta vulgaris' - skip_fetch_public_accessions = false skip_fetch_eatlas_accessions = false - skip_fetch_geo_accessions = false + fetch_geo_accessions = true platform = null keywords = "leaf" accessions = "" @@ -76,18 +73,17 @@ nextflow_workflow { workflow { """ input[0] = channel.value( params.species.split(' ').join('_') ) - input[1] = params.skip_fetch_public_accessions - input[2] = params.skip_fetch_eatlas_accessions - input[3] = params.skip_fetch_geo_accessions - input[4] = params.platform - input[5] = params.keywords - input[6] = channel.fromList( params.accessions.tokenize(',') ) - input[7] = params.accessions_file ? channel.fromPath(params.accessions_file, checkIfExists: true) : channel.empty() - input[8] = channel.fromList( params.excluded_accessions.tokenize(',') ) - input[9] = params.excluded_accessions_file ? channel.fromPath(params.excluded_accessions_file, checkIfExists: true) : channel.empty() - input[10] = params.random_sampling_size - input[11] = params.random_sampling_seed - input[12] = params.outdir + input[1] = params.skip_fetch_eatlas_accessions + input[2] = params.fetch_geo_accessions + input[3] = params.platform + input[4] = params.keywords + input[5] = channel.fromList( params.accessions.tokenize(',') ) + input[6] = params.accessions_file ? channel.fromPath(params.accessions_file, checkIfExists: true) : channel.empty() + input[7] = channel.fromList( params.excluded_accessions.tokenize(',') ) + input[8] = params.excluded_accessions_file ? channel.fromPath(params.excluded_accessions_file, checkIfExists: true) : channel.empty() + input[9] = params.random_sampling_size + input[10] = params.random_sampling_seed + input[11] = params.outdir """ } } @@ -107,9 +103,8 @@ nextflow_workflow { params { species = 'beta vulgaris' - skip_fetch_public_accessions = false skip_fetch_eatlas_accessions = false - skip_fetch_geo_accessions = true + fetch_geo_accessions = false platform = null keywords = "" accessions = "E-MTAB-552,E-GEOD-61690 ,E-PROT-138" @@ -124,18 +119,17 @@ nextflow_workflow { workflow { """ input[0] = channel.value( params.species.split(' ').join('_') ) - input[1] = params.skip_fetch_public_accessions - input[2] = params.skip_fetch_eatlas_accessions - input[3] = params.skip_fetch_geo_accessions - input[4] = params.platform - input[5] = params.keywords - input[6] = channel.fromList( params.accessions.tokenize(',') ) - input[7] = params.accessions_file ? channel.fromPath(params.accessions_file, checkIfExists: true) : channel.empty() - input[8] = channel.fromList( params.excluded_accessions.tokenize(',') ) - input[9] = params.excluded_accessions_file ? channel.fromPath(params.excluded_accessions_file, checkIfExists: true) : channel.empty() - input[10] = params.random_sampling_size - input[11] = params.random_sampling_seed - input[12] = params.outdir + input[1] = params.skip_fetch_eatlas_accessions + input[2] = params.fetch_geo_accessions + input[3] = params.platform + input[4] = params.keywords + input[5] = channel.fromList( params.accessions.tokenize(',') ) + input[6] = params.accessions_file ? channel.fromPath(params.accessions_file, checkIfExists: true) : channel.empty() + input[7] = channel.fromList( params.excluded_accessions.tokenize(',') ) + input[8] = params.excluded_accessions_file ? channel.fromPath(params.excluded_accessions_file, checkIfExists: true) : channel.empty() + input[9] = params.random_sampling_size + input[10] = params.random_sampling_seed + input[11] = params.outdir """ } } @@ -154,9 +148,8 @@ nextflow_workflow { when { params { species = 'beta vulgaris' - skip_fetch_public_accessions = false skip_fetch_eatlas_accessions = false - skip_fetch_geo_accessions = true + fetch_geo_accessions = false platform = null keywords = "" accessions = "E-MTAB-552,E-GEOD-61690 ,E-PROT-138" @@ -171,18 +164,17 @@ nextflow_workflow { workflow { """ input[0] = channel.value( params.species.split(' ').join('_') ) - input[1] = params.skip_fetch_public_accessions - input[2] = params.skip_fetch_eatlas_accessions - input[3] = params.skip_fetch_geo_accessions - input[4] = params.platform - input[5] = params.keywords - input[6] = channel.fromList( params.accessions.tokenize(',') ) - input[7] = params.accessions_file ? channel.fromPath(params.accessions_file, checkIfExists: true) : channel.empty() - input[8] = channel.fromList( params.excluded_accessions.tokenize(',') ) - input[9] = params.excluded_accessions_file ? channel.fromPath(params.excluded_accessions_file, checkIfExists: true) : channel.empty() - input[10] = params.random_sampling_size - input[11] = params.random_sampling_seed - input[12] = params.outdir + input[1] = params.skip_fetch_eatlas_accessions + input[2] = params.fetch_geo_accessions + input[3] = params.platform + input[4] = params.keywords + input[5] = channel.fromList( params.accessions.tokenize(',') ) + input[6] = params.accessions_file ? channel.fromPath(params.accessions_file, checkIfExists: true) : channel.empty() + input[7] = channel.fromList( params.excluded_accessions.tokenize(',') ) + input[8] = params.excluded_accessions_file ? channel.fromPath(params.excluded_accessions_file, checkIfExists: true) : channel.empty() + input[9] = params.random_sampling_size + input[10] = params.random_sampling_seed + input[11] = params.outdir """ } } diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 4db97b44..bd6de101 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -52,9 +52,8 @@ workflow STABLEEXPRESSION { GET_PUBLIC_ACCESSIONS( species, - params.skip_fetch_public_accessions, params.skip_fetch_eatlas_accessions, - params.skip_fetch_geo_accessions, + params.fetch_geo_accessions, params.platform, params.keywords, channel.fromList( params.accessions.tokenize(',') ), From 413d313a108250646301e8a1ce44534338967e56 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Thu, 11 Dec 2025 08:28:19 +0100 Subject: [PATCH 228/258] update method description --- assets/methods_description_template.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/assets/methods_description_template.yml b/assets/methods_description_template.yml index e116bbee..d4670c25 100644 --- a/assets/methods_description_template.yml +++ b/assets/methods_description_template.yml @@ -3,8 +3,6 @@ description: "Suggested text and references to use when describing pipeline usag section_name: "nf-core/stableexpression Methods Description" section_href: "https://github.com/nf-core/stableexpression" 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/stableexpression 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.

    From 5757d92a6ea176ca0a111fdff2606114c3f2e330 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Thu, 11 Dec 2025 09:12:51 +0100 Subject: [PATCH 229/258] fix issue with gene id mapping stats in multiqc report --- subworkflows/local/multiqc/main.nf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subworkflows/local/multiqc/main.nf b/subworkflows/local/multiqc/main.nf index d0fb32f4..add94200 100644 --- a/subworkflows/local/multiqc/main.nf +++ b/subworkflows/local/multiqc/main.nf @@ -27,7 +27,7 @@ workflow MULTIQC_WORKFLOW { channel.topic('id_mapping_stats') .collectFile( name: 'id_mapping_stats.csv', - seed: "Dataset,Nb mapped,Nb unmapped", + seed: "Dataset,mapped,unmapped", newLine: true, storeDir: "${params.outdir}/statistics/" ) { From 34587ce1cf5ef825a76c11f8291f8589f10f1720 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Thu, 11 Dec 2025 14:34:13 +0100 Subject: [PATCH 230/258] remove set statements to prepare for strict synthax --- .../local/expression_normalisation/main.nf | 10 +- .../local/get_public_accessions/main.nf | 54 ++-- subworkflows/local/idmapping/main.nf | 72 +++-- subworkflows/local/merge_data/main.nf | 132 ++++---- subworkflows/local/multiqc/main.nf | 283 +++++++++--------- subworkflows/local/old/data_cleansing/main.nf | 41 --- subworkflows/local/stability_scoring/main.nf | 7 +- workflows/stableexpression.nf | 48 +-- 8 files changed, 292 insertions(+), 355 deletions(-) delete mode 100644 subworkflows/local/old/data_cleansing/main.nf diff --git a/subworkflows/local/expression_normalisation/main.nf b/subworkflows/local/expression_normalisation/main.nf index 70286828..c0f3721d 100644 --- a/subworkflows/local/expression_normalisation/main.nf +++ b/subworkflows/local/expression_normalisation/main.nf @@ -31,9 +31,7 @@ workflow EXPRESSION_NORMALISATION { normalised: meta.normalised == true } - ch_datasets - .raw.filter { meta, file -> meta.platform == 'rnaseq' } - .set { ch_raw_rnaseq_datasets_to_normalise } + ch_raw_rnaseq_datasets_to_normalise = ch_datasets.raw.filter { meta, file -> meta.platform == 'rnaseq' } if ( normalisation_method == 'tpm' ) { @@ -57,12 +55,8 @@ workflow EXPRESSION_NORMALISATION { // // putting all normalised count datasets together and performing quantile normalisation - ch_datasets.normalised - .mix( ch_raw_rnaseq_datasets_normalised ) - .set { quant_norm_input } - QUANTILE_NORMALISATION ( - quant_norm_input, + ch_datasets.normalised.mix( ch_raw_rnaseq_datasets_normalised ), quantile_norm_target_distrib ) diff --git a/subworkflows/local/get_public_accessions/main.nf b/subworkflows/local/get_public_accessions/main.nf index 32fd76a9..0959cf83 100644 --- a/subworkflows/local/get_public_accessions/main.nf +++ b/subworkflows/local/get_public_accessions/main.nf @@ -26,8 +26,8 @@ workflow GET_PUBLIC_ACCESSIONS { main: ch_fetched_eatlas_accessions = channel.empty() - ch_fetched_geo_accessions = channel.empty() - ch_sampling_quota = channel.of( "ok" ) + ch_fetched_geo_accessions = channel.empty() + ch_sampling_quota = channel.of( "ok" ) // ----------------------------------------------------------------- // GET EATLAS ACCESSIONS @@ -47,7 +47,7 @@ workflow GET_PUBLIC_ACCESSIONS { ) ch_fetched_eatlas_accessions = EXPRESSION_ATLAS.out.accessions.splitText() - ch_sampling_quota = EXPRESSION_ATLAS.out.sampling_quota + ch_sampling_quota = EXPRESSION_ATLAS.out.sampling_quota } @@ -73,9 +73,9 @@ workflow GET_PUBLIC_ACCESSIONS { // trick to avoid fetching accessions from GEO when the sampling quota is already exceeded ch_species = channel.of( species ) - .combine( ch_sampling_quota ) - .filter { species, quota -> quota == "ok" } - .map { species, quota -> species } + .combine( ch_sampling_quota ) + .filter { species, quota -> quota == "ok" } + .map { species, quota -> species } // getting GEO accessions given a species name and keywords // keywords can be an empty string @@ -97,41 +97,41 @@ workflow GET_PUBLIC_ACCESSIONS { // getting accessions to exclude and preparing in the right format ch_excluded_accessions = ch_excluded_accessions - .mix( ch_excluded_accessions_file.splitText() ) - .unique() - .map { acc -> acc.trim() } - .toList() - .map { lst -> [lst] } // list of lists : mandatory when combining in the next step + .mix( ch_excluded_accessions_file.splitText() ) + .unique() + .map { acc -> acc.trim() } + .toList() + .map { lst -> [lst] } // list of lists : mandatory when combining in the next step ch_fetched_public_accessions = ch_fetched_eatlas_accessions - .mix( ch_fetched_geo_accessions ) - .map { acc -> acc.trim() } - .filter { acc -> - (acc.startsWith('E-') || acc.startsWith('GSE')) && !acc.startsWith('E-PROT-') - } - .combine ( ch_excluded_accessions ) - .filter { accession, excluded_accessions -> !(accession in excluded_accessions) } - .map { accession, excluded_accessions -> accession } + .mix( ch_fetched_geo_accessions ) + .map { acc -> acc.trim() } + .filter { acc -> + (acc.startsWith('E-') || acc.startsWith('GSE')) && !acc.startsWith('E-PROT-') + } + .combine ( ch_excluded_accessions ) + .filter { accession, excluded_accessions -> !(accession in excluded_accessions) } + .map { accession, excluded_accessions -> accession } // ----------------------------------------------------------------- // ADDING USER PROVIDED ACCESSIONS // ----------------------------------------------------------------- ch_input_accessions = ch_accessions - .mix( ch_accessions_file.splitText() ) - .unique() - .map { acc -> acc.trim() } + .mix( ch_accessions_file.splitText() ) + .unique() + .map { acc -> acc.trim() } // appending to accessions provided by the user // ensures that no accessions is present twice (provided by the user and fetched from E. Atlas) // removing E-PROT- accessions because they are not supported in subsequent steps // removing excluded accessions - ch_accessions = ch_input_accessions - .mix( ch_fetched_public_accessions ) - .unique() - .map { acc -> acc.trim() } + ch_all_accessions = ch_input_accessions + .mix( ch_fetched_public_accessions ) + .unique() + .map { acc -> acc.trim() } emit: - accessions = ch_accessions + accessions = ch_all_accessions } diff --git a/subworkflows/local/idmapping/main.nf b/subworkflows/local/idmapping/main.nf index 94ad9bd0..e2da01e3 100644 --- a/subworkflows/local/idmapping/main.nf +++ b/subworkflows/local/idmapping/main.nf @@ -22,8 +22,8 @@ workflow ID_MAPPING { main: - ch_gene_id_mapping = channel.empty() - ch_gene_metadata = channel.empty() + ch_gene_id_mapping = channel.empty() + ch_gene_metadata = channel.empty() if ( !skip_id_mapping ) { @@ -41,13 +41,13 @@ workflow ID_MAPPING { // the buffer operator creates non-deterministic chunks // which prevents resuming the pipeline // so we sort the list of files before buffering them - ch_chunck_counts = ch_counts + ch_chunk_counts = ch_counts .map{ meta, file -> file } .collect( sort: true ) // get all files and sort them .flatten() // needed to convert the list back to individual channel items .buffer( size: 100, remainder: true ) - COLLECT_GENE_IDS( ch_chunck_counts ) + COLLECT_GENE_IDS( ch_chunk_counts ) ch_gene_ids = COLLECT_GENE_IDS.out.gene_ids .splitText() @@ -66,41 +66,47 @@ workflow ID_MAPPING { species, gprofiler_target_db ) - GPROFILER_IDMAPPING.out.mapping.set { ch_gene_id_mapping } - GPROFILER_IDMAPPING.out.metadata.set { ch_gene_metadata } + ch_gene_id_mapping = GPROFILER_IDMAPPING.out.mapping + ch_gene_metadata = GPROFILER_IDMAPPING.out.metadata } // ----------------------------------------------------------------- // COLLECTING GLOBAL GENE ID MAPPING AND METADATA // ----------------------------------------------------------------- - ch_gene_id_mapping - .mix( custom_gene_id_mapping ? channel.fromPath( custom_gene_id_mapping, checkIfExists: true ) : channel.empty() ) - .splitCsv( header: true ) - .unique() - .collectFile( - name: 'global_gene_id_mapping.csv', - seed: "original_gene_id,gene_id", - newLine: true, - storeDir: "${outdir}/idmapping/" - ) { - item -> "${item["original_gene_id"]},${item["gene_id"]}" - } - .set { ch_global_gene_id_mapping } - - ch_gene_metadata - .mix( custom_gene_metadata ? channel.fromPath( custom_gene_metadata, checkIfExists: true ) : channel.empty() ) - .splitCsv( header: true ) - .unique() - .collectFile( - name: 'global_gene_metadata.csv', - seed: "gene_id,name,description", - newLine: true, - storeDir: "${outdir}/idmapping/" - ) { - item -> "${item["gene_id"]},${item["name"]},${item["description"]}" - } - .set { ch_global_gene_metadata } + ch_global_gene_id_mapping = ch_gene_id_mapping + .mix( + custom_gene_id_mapping ? + channel.fromPath( custom_gene_id_mapping, checkIfExists: true ) : + channel.empty() + ) + .splitCsv( header: true ) + .unique() + .collectFile( + name: 'global_gene_id_mapping.csv', + seed: "original_gene_id,gene_id", + newLine: true, + storeDir: "${outdir}/idmapping/" + ) { + item -> "${item["original_gene_id"]},${item["gene_id"]}" + } + + ch_global_gene_metadata = ch_gene_metadata + .mix( + custom_gene_metadata ? + channel.fromPath( custom_gene_metadata, checkIfExists: true ) : + channel.empty() + ) + .splitCsv( header: true ) + .unique() + .collectFile( + name: 'global_gene_metadata.csv', + seed: "gene_id,name,description", + newLine: true, + storeDir: "${outdir}/idmapping/" + ) { + item -> "${item["gene_id"]},${item["name"]},${item["description"]}" + } // ----------------------------------------------------------------- // RENAMING GENE IDS IN ALL COUNT DATASETS (ONLY IF NECESSARY) diff --git a/subworkflows/local/merge_data/main.nf b/subworkflows/local/merge_data/main.nf index 84bb9d34..d86eb5e7 100644 --- a/subworkflows/local/merge_data/main.nf +++ b/subworkflows/local/merge_data/main.nf @@ -24,43 +24,34 @@ workflow MERGE_DATA { // ----------------------------------------------------------------- // RNASEQ - ch_normalised_counts - .filter { meta, file -> meta.platform == "rnaseq" } - .set { ch_normalised_rnaseq_counts } - - ch_whole_rnaseq_size = getWholeDatasetSize ( ch_normalised_rnaseq_counts ) + ch_normalised_rnaseq_counts = ch_normalised_counts.filter { meta, file -> meta.platform == "rnaseq" } + ch_whole_rnaseq_size = getWholeDatasetSize ( ch_normalised_rnaseq_counts ) MERGE_RNASEQ_COUNTS ( ch_normalised_rnaseq_counts.map { meta, file -> file }.collect(), ch_whole_rnaseq_size.collect() ) - MERGE_RNASEQ_COUNTS.out.counts.set { ch_merged_rnaseq_counts } // MICROARRAY - ch_normalised_counts - .filter { meta, file -> meta.platform == "microarray" } - .set { ch_normalised_microarray_counts } - - ch_whole_microarray_size = getWholeDatasetSize ( ch_normalised_microarray_counts ) + ch_normalised_microarray_counts = ch_normalised_counts.filter { meta, file -> meta.platform == "microarray" } + ch_whole_microarray_size = getWholeDatasetSize ( ch_normalised_microarray_counts ) MERGE_MICROARRAY_COUNTS ( ch_normalised_microarray_counts.map { meta, file -> file }.collect(), ch_whole_microarray_size.collect() ) - MERGE_MICROARRAY_COUNTS.out.counts.set { ch_merged_microarray_counts } // ----------------------------------------------------------------- // MERGE ALL COUNTS // ----------------------------------------------------------------- - ch_merged_rnaseq_counts - .mix ( ch_merged_microarray_counts ) - .set { ch_platform_counts } + ch_merged_rnaseq_counts = MERGE_RNASEQ_COUNTS.out.counts + ch_merged_microarray_counts = MERGE_MICROARRAY_COUNTS.out.counts + ch_platform_counts = ch_merged_rnaseq_counts.mix ( ch_merged_microarray_counts ) - ch_whole_rnaseq_size - .mix(ch_whole_microarray_size) - .reduce { rnaseq_size, microarray_size -> rnaseq_size + microarray_size } - .set { ch_whole_size } + ch_whole_size = ch_whole_rnaseq_size + .mix(ch_whole_microarray_size) + .reduce { rnaseq_size, microarray_size -> rnaseq_size + microarray_size } MERGE_ALL_COUNTS( ch_platform_counts.collect(), @@ -71,69 +62,66 @@ workflow MERGE_DATA { // MERGE ALL DESIGNS IN A SINGLE TABLE // ----------------------------------------------------------------- - ch_normalised_counts - .map { - meta, file -> // extracts design file and adds batch column whenever missing (for custom datasets) - def design_content = meta.design.splitCsv( header: true ) - // if there is no batch, it is custom data - def updated_design_content = design_content.collect { row -> - row.batch = row.batch ?: "custom_${meta.dataset}" - return row - } - [ updated_design_content ] - } - .flatten() - .unique() - .collectFile( - name: 'whole_design.csv', - seed: "batch,condition,sample", - newLine: true, - sort: true, - storeDir: "${params.outdir}/merged_datasets/" - ) { - item -> "${item.batch},${item.condition},${item.sample}" - } - .set { ch_whole_design } + ch_whole_design = ch_normalised_counts + .map { + meta, file -> // extracts design file and adds batch column whenever missing (for custom datasets) + def design_content = meta.design.splitCsv( header: true ) + // if there is no batch, it is custom data + def updated_design_content = design_content.collect { row -> + row.batch = row.batch ?: "custom_${meta.dataset}" + return row + } + [ updated_design_content ] + } + .flatten() + .unique() + .collectFile( + name: 'whole_design.csv', + seed: "batch,condition,sample", + newLine: true, + sort: true, + storeDir: "${params.outdir}/merged_datasets/" + ) { + item -> "${item.batch},${item.condition},${item.sample}" + } // ----------------------------------------------------------------- // MERGE ALL GENE ID MAPPINGS // ----------------------------------------------------------------- - ch_gene_id_mapping - .filter { it != [] } // handle case where there are no mappings - .splitCsv( header: true ) - .unique() - .collectFile( - name: 'whole_gene_id_mapping.csv', - seed: "original_gene_id,gene_id", - newLine: true, - sort: true, - storeDir: "${params.outdir}/idmapping/" - ) { - item -> "${item.original_gene_id},${item.gene_id}" - } - .ifEmpty([]) // handle case where there are no mappings - .set { ch_whole_gene_id_mapping } + ch_whole_gene_id_mapping = ch_gene_id_mapping + .filter { it != [] } // handle case where there are no mappings + .splitCsv( header: true ) + .unique() + .collectFile( + name: 'whole_gene_id_mapping.csv', + seed: "original_gene_id,gene_id", + newLine: true, + sort: true, + storeDir: "${params.outdir}/idmapping/" + ) { + item -> "${item.original_gene_id},${item.gene_id}" + } + .ifEmpty([]) // handle case where there are no mappings // ----------------------------------------------------------------- // MERGE ALL GENE METADATA // ----------------------------------------------------------------- - ch_gene_metadata - .filter { it != [] } // handle case where there are no mappings - .splitCsv( header: true ) - .unique() - .collectFile( - name: 'whole_gene_metadata.csv', - seed: "gene_id,name,description", - newLine: true, - sort: true, - storeDir: "${params.outdir}/idmapping/" - ) { - item -> "${item.gene_id},${item.name},${item.description}" - } - .ifEmpty([]) // handle case where there are no mappings - .set { ch_whole_gene_metadata } + ch_whole_gene_metadata = ch_gene_metadata + .filter { it != [] } // handle case where there are no mappings + .splitCsv( header: true ) + .unique() + .collectFile( + name: 'whole_gene_metadata.csv', + seed: "gene_id,name,description", + newLine: true, + sort: true, + storeDir: "${params.outdir}/idmapping/" + ) { + item -> "${item.gene_id},${item.name},${item.description}" + } + .ifEmpty([]) // handle case where there are no mappings emit: all_counts = MERGE_ALL_COUNTS.out.counts diff --git a/subworkflows/local/multiqc/main.nf b/subworkflows/local/multiqc/main.nf index add94200..167bee1a 100644 --- a/subworkflows/local/multiqc/main.nf +++ b/subworkflows/local/multiqc/main.nf @@ -24,149 +24,140 @@ workflow MULTIQC_WORKFLOW { // STATS // ------------------------------------------------------------------------------------ - channel.topic('id_mapping_stats') - .collectFile( - name: 'id_mapping_stats.csv', - seed: "Dataset,mapped,unmapped", - newLine: true, - storeDir: "${params.outdir}/statistics/" - ) { - item -> "${item[0]},${item[1]},${item[2]}" - } - .set { ch_id_mapping_stats } - - channel.topic('skewness') - .map { dataset, file -> "${dataset},${file.readLines()[0]}" } // concatenate dataset name with skewness values - .collectFile( - name: 'skewness.csv', - newLine: true, - storeDir: "${params.outdir}/statistics/" - ) - .set { ch_skewness } - - channel.topic('ratio_zeros') - .map { dataset, file -> "${dataset},${file.readLines()[0]}" } // concatenate dataset name with skewness values - .collectFile( - name: 'ratio_zeros.csv', - newLine: true, - storeDir: "${params.outdir}/statistics/" - ) - .set { ch_ratio_zeros } - - ch_to_collect = ch_skewness.mix( ch_ratio_zeros ) - COLLECT_STATISTICS( ch_to_collect ) + ch_id_mapping_stats = channel.topic('id_mapping_stats') + .collectFile( + name: 'id_mapping_stats.csv', + seed: "Dataset,mapped,unmapped", + newLine: true, + storeDir: "${params.outdir}/statistics/" + ) { + item -> "${item[0]},${item[1]},${item[2]}" + } + + ch_skewness = channel.topic('skewness') + .map { dataset, file -> "${dataset},${file.readLines()[0]}" } // concatenate dataset name with skewness values + .collectFile( + name: 'skewness.csv', + newLine: true, + storeDir: "${params.outdir}/statistics/" + ) + + + ch_ratio_zeros = channel.topic('ratio_zeros') + .map { dataset, file -> "${dataset},${file.readLines()[0]}" } // concatenate dataset name with skewness values + .collectFile( + name: 'ratio_zeros.csv', + newLine: true, + storeDir: "${params.outdir}/statistics/" + ) + + COLLECT_STATISTICS( + ch_skewness.mix( ch_ratio_zeros ) + ) // ------------------------------------------------------------------------------------ // FAILURE / WARNING REPORTS // ------------------------------------------------------------------------------------ - channel.topic('eatlas_failure_reason') - .map { accession, file -> [ accession, file.readLines()[0] ] } - .collectFile( - name: 'eatlas_failure_reasons.csv', - seed: "Accession,Reason", - newLine: true, - storeDir: "${params.outdir}/errors/" - ) { - item -> "${item[0]},${item[1]}" - } - .set { ch_eatlas_failure_reasons } - - channel.topic('eatlas_warning_reason') - .map { accession, file -> [ accession, file.readLines()[0] ] } - .collectFile( - name: 'eatlas_warning_reasons.csv', - seed: "Accession,Reason", - newLine: true, - storeDir: "${params.outdir}/warnings/" - ) { - item -> "${item[0]},${item[1]}" - } - .set { ch_eatlas_warning_reasons } - - channel.topic('geo_failure_reason') - .map { accession, file -> [ accession, file.readLines()[0] ] } - .collectFile( - name: 'geo_failure_reasons.csv', - seed: "Accession,Reason", - newLine: true, - storeDir: "${params.outdir}/errors/" - ) { - item -> "${item[0]},${item[1]}" - } - .set { ch_geo_failure_reasons } - - channel.topic('geo_warning_reason') - .map { accession, file -> [ accession, file.readLines()[0] ] } - .collectFile( - name: 'geo_warning_reasons.csv', - seed: "Accession,Reason", - newLine: true, - storeDir: "${params.outdir}/warnings/" - ) { - item -> "${item[0]},${item[1]}" - } - .set { ch_geo_warning_reasons } - - channel.topic('id_cleaning_failure_reason') - .map { dataset, file -> [ dataset, file.readLines()[0] ] } - .collectFile( - name: 'id_cleaning_failure_reasons.tsv', - seed: "Dataset\tReason", - newLine: true, - storeDir: "${params.outdir}/errors/" - ) { - item -> "${item[0]}\t${item[1]}" - } - .set { ch_id_cleaning_failure_reasons } - - channel.topic('renaming_warning_reason') - .map { dataset, file -> [ dataset, file.readLines()[0] ] } - .collectFile( - name: 'renaming_warning_reasons.tsv', - seed: "Dataset\tReason", - newLine: true, - storeDir: "${params.outdir}/warnings/" - ) { - item -> "${item[0]}\t${item[1]}" - } - .set { ch_id_mapping_warning_reasons } - - channel.topic('renaming_failure_reason') - .map { dataset, file -> [ dataset, file.readLines()[0] ] } - .collectFile( - name: 'renaming_failure_reasons.tsv', - seed: "Dataset\tReason", - newLine: true, - storeDir: "${params.outdir}/errors/" - ) { - item -> "${item[0]}\t${item[1]}" - } - .set { ch_id_mapping_failure_reasons } - - channel.topic('normalisation_warning_reason') - .map { dataset, file -> [ dataset, file.readLines()[0] ] } - .collectFile( - name: 'normalisation_warning_reasons.tsv', - seed: "Dataset\tReason", - newLine: true, - storeDir: "${params.outdir}/warnings/" - ) { - item -> "${item[0]}\t${item[1]}" - } - .set { ch_normalisation_warning_reasons } - - channel.topic('normalisation_failure_reason') - .map { dataset, file -> [ dataset, file.readLines()[0] ] } - .collectFile( - name: 'normalisation_failure_reasons.tsv', - seed: "Dataset\tReason", - newLine: true, - storeDir: "${params.outdir}/errors/" - ) { - item -> "${item[0]}\t${item[1]}" - } - .set { ch_normalisation_failure_reasons } + ch_eatlas_failure_reasons = channel.topic('eatlas_failure_reason') + .map { accession, file -> [ accession, file.readLines()[0] ] } + .collectFile( + name: 'eatlas_failure_reasons.csv', + seed: "Accession,Reason", + newLine: true, + storeDir: "${params.outdir}/errors/" + ) { + item -> "${item[0]},${item[1]}" + } + + ch_eatlas_warning_reasons = channel.topic('eatlas_warning_reason') + .map { accession, file -> [ accession, file.readLines()[0] ] } + .collectFile( + name: 'eatlas_warning_reasons.csv', + seed: "Accession,Reason", + newLine: true, + storeDir: "${params.outdir}/warnings/" + ) { + item -> "${item[0]},${item[1]}" + } + + ch_geo_failure_reasons = channel.topic('geo_failure_reason') + .map { accession, file -> [ accession, file.readLines()[0] ] } + .collectFile( + name: 'geo_failure_reasons.csv', + seed: "Accession,Reason", + newLine: true, + storeDir: "${params.outdir}/errors/" + ) { + item -> "${item[0]},${item[1]}" + } + + + ch_geo_warning_reasons = channel.topic('geo_warning_reason') + .map { accession, file -> [ accession, file.readLines()[0] ] } + .collectFile( + name: 'geo_warning_reasons.csv', + seed: "Accession,Reason", + newLine: true, + storeDir: "${params.outdir}/warnings/" + ) { + item -> "${item[0]},${item[1]}" + } + + ch_id_cleaning_failure_reasons = channel.topic('id_cleaning_failure_reason') + .map { dataset, file -> [ dataset, file.readLines()[0] ] } + .collectFile( + name: 'id_cleaning_failure_reasons.tsv', + seed: "Dataset\tReason", + newLine: true, + storeDir: "${params.outdir}/errors/" + ) { + item -> "${item[0]}\t${item[1]}" + } + + ch_id_mapping_warning_reasons = channel.topic('renaming_warning_reason') + .map { dataset, file -> [ dataset, file.readLines()[0] ] } + .collectFile( + name: 'renaming_warning_reasons.tsv', + seed: "Dataset\tReason", + newLine: true, + storeDir: "${params.outdir}/warnings/" + ) { + item -> "${item[0]}\t${item[1]}" + } + + ch_id_mapping_failure_reasons = channel.topic('renaming_failure_reason') + .map { dataset, file -> [ dataset, file.readLines()[0] ] } + .collectFile( + name: 'renaming_failure_reasons.tsv', + seed: "Dataset\tReason", + newLine: true, + storeDir: "${params.outdir}/errors/" + ) { + item -> "${item[0]}\t${item[1]}" + } + + ch_normalisation_warning_reasons = channel.topic('normalisation_warning_reason') + .map { dataset, file -> [ dataset, file.readLines()[0] ] } + .collectFile( + name: 'normalisation_warning_reasons.tsv', + seed: "Dataset\tReason", + newLine: true, + storeDir: "${params.outdir}/warnings/" + ) { + item -> "${item[0]}\t${item[1]}" + } + + ch_normalisation_failure_reasons = channel.topic('normalisation_failure_reason') + .map { dataset, file -> [ dataset, file.readLines()[0] ] } + .collectFile( + name: 'normalisation_failure_reasons.tsv', + seed: "Dataset\tReason", + newLine: true, + storeDir: "${params.outdir}/errors/" + ) { + item -> "${item[0]}\t${item[1]}" + } // ------------------------------------------------------------------------------------ @@ -215,14 +206,14 @@ workflow MULTIQC_WORKFLOW { "${process}:\n${tool_versions.join('\n')}" } - softwareVersionsToYAML(ch_versions.mix(topic_versions.versions_file)) - .mix(topic_versions_string) - .collectFile( - storeDir: "${params.outdir}/pipeline_info", - name: 'nf_core_' + 'stableexpression_software_' + 'mqc_' + 'versions.yml', - sort: true, - newLine: true - ).set { ch_collated_versions } + ch_collated_versions = softwareVersionsToYAML(ch_versions.mix(topic_versions.versions_file)) + .mix(topic_versions_string) + .collectFile( + storeDir: "${params.outdir}/pipeline_info", + name: 'nf_core_' + 'stableexpression_software_' + 'mqc_' + 'versions.yml', + sort: true, + newLine: true + ) // ------------------------------------------------------------------------------------ // CONFIG diff --git a/subworkflows/local/old/data_cleansing/main.nf b/subworkflows/local/old/data_cleansing/main.nf deleted file mode 100644 index d7de77d7..00000000 --- a/subworkflows/local/old/data_cleansing/main.nf +++ /dev/null @@ -1,41 +0,0 @@ -include { DATASET_STATISTICS } from '../../../modules/local/dataset_statistics' -include { CLEAN_COUNT_DATA } from '../../../modules/local/clean_count_data' - -/* -======================================================================================== - SUBWORKFLOW TO NORMALISE AND HARMONISE EXPRESSION DATASETS -======================================================================================== -*/ - -workflow DATA_CLEANSING { - - take: - ch_quantile_normalised_datasets - quantile_norm_target_distrib - ks_pvalue_threshold - - main: - - // - // Get global stats for each sample in each dataset - // - - DATASET_STATISTICS( - ch_quantile_normalised_datasets, - quantile_norm_target_distrib - ) - - // - // Filter out aberrant samples and perform some sorting / cleaning - // - - CLEAN_COUNT_DATA ( - ch_quantile_normalised_datasets.join( DATASET_STATISTICS.out.stats ), - ks_pvalue_threshold - ) - - - emit: - cleaned_counts = CLEAN_COUNT_DATA.out.counts - -} diff --git a/subworkflows/local/stability_scoring/main.nf b/subworkflows/local/stability_scoring/main.nf index 211f54a4..55785826 100644 --- a/subworkflows/local/stability_scoring/main.nf +++ b/subworkflows/local/stability_scoring/main.nf @@ -34,7 +34,7 @@ workflow STABILITY_SCORING { nb_top_gene_candidates, min_expr_threshold ) - GET_CANDIDATE_GENES.out.counts.set { ch_candidate_gene_counts } + ch_candidate_gene_counts = GET_CANDIDATE_GENES.out.counts // ----------------------------------------------------------------- // NORMFINDER @@ -44,7 +44,6 @@ workflow STABILITY_SCORING { ch_candidate_gene_counts.collect(), ch_design.collect() ) - NORMFINDER.out.stability_values.set { ch_normfinder_stability } // ----------------------------------------------------------------- // GENORM @@ -52,7 +51,7 @@ workflow STABILITY_SCORING { if ( run_genorm ) { GENORM ( ch_candidate_gene_counts ) - GENORM.out.m_measures.set { ch_genorm_stability } + ch_genorm_stability = GENORM.out.m_measures } else { ch_genorm_stability = channel.value([]) } @@ -64,7 +63,7 @@ workflow STABILITY_SCORING { COMPUTE_STABILITY_SCORES ( ch_stats.collect(), stability_score_weights, - ch_normfinder_stability, + NORMFINDER.out.stability_values, ch_genorm_stability ) diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index bd6de101..45d4e347 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -40,9 +40,9 @@ workflow STABLEEXPRESSION { ch_versions = channel.empty() ch_multiqc_files = channel.empty() - ch_top_stable_genes_summary = channel.empty() + ch_most_stable_genes_summary = channel.empty() ch_all_genes_statistics = channel.empty() - ch_top_stable_genes_transposed_counts = channel.empty() + ch_most_stable_genes_transposed_counts = channel.empty() def species = params.species.split(' ').join('_').toLowerCase() @@ -80,14 +80,14 @@ workflow STABLEEXPRESSION { } - ch_counts = ch_input_datasets.mix( ch_downloaded_datasets ) - // store nb of genes and nb f samples at this stage in the meta maps - ch_counts = storeDatasetSize( ch_counts, "nb_genes", "nb_samples" ) - // returns an error with a message if no dataset was found - checkCounts( ch_counts ) - if ( !params.accessions_only && !params.download_only ) { + ch_counts = ch_input_datasets.mix( ch_downloaded_datasets ) + // store nb of genes and nb of samples at this stage in the meta maps + ch_counts = storeDatasetSize( ch_counts, "nb_genes", "nb_samples" ) + // returns an error with a message if no dataset was found + checkCounts( ch_counts ) + // ----------------------------------------------------------------- // IDMAPPING // ----------------------------------------------------------------- @@ -102,9 +102,9 @@ workflow STABLEEXPRESSION { params.gene_metadata, params.outdir ) - ID_MAPPING.out.counts.set { ch_counts } - ID_MAPPING.out.mapping.set { ch_gene_id_mapping } - ID_MAPPING.out.metadata.set { ch_gene_metadata } + ch_counts = ID_MAPPING.out.counts + ch_gene_id_mapping = ID_MAPPING.out.mapping + ch_gene_metadata = ID_MAPPING.out.metadata ch_counts = storeDatasetSize( ch_counts, "nb_genes_after_idmapping", "nb_samples_after_idmapping" ) @@ -135,8 +135,8 @@ workflow STABLEEXPRESSION { ch_gene_metadata ) - MERGE_DATA.out.all_counts.set { ch_all_counts } - MERGE_DATA.out.whole_design.set { ch_whole_design } + ch_all_counts = MERGE_DATA.out.all_counts + ch_whole_design = MERGE_DATA.out.whole_design // ----------------------------------------------------------------- // COMPUTE BASE STATISTICS FOR ALL GENES @@ -148,7 +148,7 @@ workflow STABLEEXPRESSION { MERGE_DATA.out.microarray_counts ) - BASE_STATISTICS.out.stats.set { ch_all_datasets_stats } + ch_all_datasets_stats = BASE_STATISTICS.out.stats // ----------------------------------------------------------------- // GET CANDIDATES AS REFERENCE GENE AND COMPUTES VARIOUS STABILITY VALUES @@ -165,7 +165,7 @@ workflow STABLEEXPRESSION { params.stability_score_weights ) - STABILITY_SCORING.out.summary_statistics.set { ch_stats_all_genes_with_scores } + ch_stats_all_genes_with_scores = STABILITY_SCORING.out.summary_statistics // ----------------------------------------------------------------- // AGGREGATE ALL RESULTS FOR MULTIQC @@ -180,9 +180,9 @@ workflow STABLEEXPRESSION { MERGE_DATA.out.whole_gene_id_mapping.collect() ) - AGGREGATE_RESULTS.out.all_genes_summary.set { ch_all_genes_summary } - AGGREGATE_RESULTS.out.top_stable_genes_summary.set { ch_top_stable_genes_summary } - AGGREGATE_RESULTS.out.top_stable_genes_transposed_counts_filtered.set { ch_top_stable_genes_transposed_counts } + ch_all_genes_summary = AGGREGATE_RESULTS.out.all_genes_summary + ch_most_stable_genes_summary = AGGREGATE_RESULTS.out.most_stable_genes_summary + ch_most_stable_genes_transposed_counts = AGGREGATE_RESULTS.out.most_stable_genes_transposed_counts_filtered // ----------------------------------------------------------------- // DASH APPLICATION @@ -195,11 +195,10 @@ workflow STABLEEXPRESSION { ) ch_versions = ch_versions.mix ( DASH_APP.out.versions ) - ch_multiqc_files - .mix( ch_top_stable_genes_summary.collect() ) - .mix( ch_all_genes_summary.collect() ) - .mix( ch_top_stable_genes_transposed_counts.collect() ) - .set { ch_multiqc_files } + ch_multiqc_files = ch_multiqc_files + .mix( ch_most_stable_genes_summary.collect() ) + .mix( ch_all_genes_summary.collect() ) + .mix( ch_most_stable_genes_transposed_counts.collect() ) } @@ -214,7 +213,8 @@ workflow STABLEEXPRESSION { emit: - multiqc_report = MULTIQC_WORKFLOW.out.report.toList() + multiqc_report = MULTIQC_WORKFLOW.out.report.toList() + most_stable_genes_summary = ch_most_stable_genes_summary } From d3bd377ba03a44c090a5566483c254c9886afb55 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Thu, 11 Dec 2025 14:35:04 +0100 Subject: [PATCH 231/258] rename top stable genes by most stable genes --- assets/multiqc_config.yml | 12 +++---- bin/aggregate_results.py | 32 ++++++++++--------- bin/get_candidate_genes.py | 10 +++--- docs/troubleshooting.md | 4 ++- galaxy/build/static/boilerplate.xml | 8 ++--- .../tool/nf_core_stableexpression.xml | 8 ++--- modules/local/aggregate_results/main.nf | 4 +-- modules/local/geo/getdata/main.nf | 2 +- modules/local/get_candidate_genes/main.nf | 4 +-- 9 files changed, 45 insertions(+), 39 deletions(-) diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index 2e34b9ff..30b48441 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -32,7 +32,7 @@ table_cond_formatting_colours: - very_high: "#d9534f" custom_data: - ranked_top_stable_genes_summary: + ranked_most_stable_genes_summary: section_name: "Stable genes ranking" file_format: "csv" no_violin: true @@ -301,7 +301,7 @@ custom_data: description: | Ratio of Microarray samples in which the gene has a zero value. - expression_distributions_top_stable_genes: + expression_distributions_most_stable_genes: section_name: "Count distributions" file_format: "csv" pconfig: @@ -642,11 +642,11 @@ custom_data: log_filesize_limit: 10000000000 # 10GB sp: - ranked_top_stable_genes_summary: - fn: "*top_stable_genes_summary.csv" + ranked_most_stable_genes_summary: + fn: "*most_stable_genes_summary.csv" max_filesize: 5000000 # 5MB - expression_distributions_top_stable_genes: - fn: "*top_stable_genes_transposed_counts*.csv" + expression_distributions_most_stable_genes: + fn: "*most_stable_genes_transposed_counts*.csv" max_filesize: 50000000 # 50MB gene_statistics: fn: "*all_genes_summary.csv" diff --git a/bin/aggregate_results.py b/bin/aggregate_results.py index 9fd70c79..f3ee5e63 100755 --- a/bin/aggregate_results.py +++ b/bin/aggregate_results.py @@ -14,12 +14,14 @@ # outfile names ALL_GENE_SUMMARY_OUTFILENAME = "all_genes_summary.csv" -TOP_STABLE_GENE_SUMMARY_OUTFILENAME = "top_stable_genes_summary.csv" +MOST_STABLE_GENE_SUMMARY_OUTFILENAME = "most_stable_genes_summary.csv" ALL_COUNTS_FILTERED_PARQUET_OUTFILENAME = "all_counts_filtered.parquet" -TOP_STABLE_GENES_COUNTS_OUTFILENAME = "top_stable_genes_transposed_counts_filtered.csv" +MOST_STABLE_GENES_COUNTS_OUTFILENAME = ( + "most_stable_genes_transposed_counts_filtered.csv" +) # nb of top stable genes to select and to display at the end -NB_TOP_STABLE_GENES = 1000 +NB_MOST_STABLE_GENES = 1000 # quantile intervals NB_QUANTILES = 100 NB_TOP_GENES_TO_SHOW_IN_BOX_PLOTS = 100 @@ -184,7 +186,7 @@ def get_all_genes_summary( return stat_summary_df -def get_top_stable_genes_counts( +def get_most_stable_genes_counts( log_count_df: pl.DataFrame, stat_summary_df: pl.DataFrame ) -> pl.DataFrame: # getting list of top stable genes with their order @@ -210,9 +212,9 @@ def get_top_stable_genes_counts( def export_data( all_genes_summary_df: pl.DataFrame, - top_stable_genes_summary_df: pl.DataFrame, + most_stable_genes_summary_df: pl.DataFrame, all_counts_df: pl.DataFrame, - top_stable_genes_counts_df: pl.DataFrame, + most_stable_genes_counts_df: pl.DataFrame, ): """Export gene expression data to CSV files.""" logger.info(f"Exporting statistics of all genes to: {ALL_GENE_SUMMARY_OUTFILENAME}") @@ -221,20 +223,20 @@ def export_data( ) logger.info( - f"Exporting statistics of the top stable genes to: {TOP_STABLE_GENE_SUMMARY_OUTFILENAME}" + f"Exporting statistics of the top stable genes to: {MOST_STABLE_GENE_SUMMARY_OUTFILENAME}" ) - top_stable_genes_summary_df.write_csv( - TOP_STABLE_GENE_SUMMARY_OUTFILENAME, float_precision=config.CSV_FLOAT_PRECISION + most_stable_genes_summary_df.write_csv( + MOST_STABLE_GENE_SUMMARY_OUTFILENAME, float_precision=config.CSV_FLOAT_PRECISION ) logger.info(f"Exporting all counts to: {ALL_COUNTS_FILTERED_PARQUET_OUTFILENAME}") all_counts_df.write_parquet(ALL_COUNTS_FILTERED_PARQUET_OUTFILENAME) logger.info( - f"Exporting counts of the top stable genes to: {TOP_STABLE_GENES_COUNTS_OUTFILENAME}" + f"Exporting counts of the top stable genes to: {MOST_STABLE_GENES_COUNTS_OUTFILENAME}" ) - top_stable_genes_counts_df.write_csv( - TOP_STABLE_GENES_COUNTS_OUTFILENAME, float_precision=config.CSV_FLOAT_PRECISION + most_stable_genes_counts_df.write_csv( + MOST_STABLE_GENES_COUNTS_OUTFILENAME, float_precision=config.CSV_FLOAT_PRECISION ) logger.info("Done") @@ -281,11 +283,11 @@ def main(): all_genes_stat_summary_df, *additional_data_dfs ) - top_stable_stat_summary_df = all_genes_summary_df.head(NB_TOP_STABLE_GENES) + top_stable_stat_summary_df = all_genes_summary_df.head(NB_MOST_STABLE_GENES) # reducing dataframe size (it is only used for plotting by MultiQC) count_df = cast_count_columns_to_float(count_df) - top_stable_genes_counts_df = get_top_stable_genes_counts( + most_stable_genes_counts_df = get_most_stable_genes_counts( count_df, top_stable_stat_summary_df ) @@ -294,7 +296,7 @@ def main(): all_genes_summary_df, top_stable_stat_summary_df, count_df, - top_stable_genes_counts_df, + most_stable_genes_counts_df, ) diff --git a/bin/get_candidate_genes.py b/bin/get_candidate_genes.py index ec5e12d5..c41177cf 100755 --- a/bin/get_candidate_genes.py +++ b/bin/get_candidate_genes.py @@ -51,7 +51,7 @@ def parse_args(): parser.add_argument( "--nb-top-stable-genes", type=int, - dest="nb_top_stable_genes", + dest="nb_most_stable_genes", required=True, help="Number of top stable genes to show", ) @@ -73,7 +73,9 @@ def parse_stats(file: Path) -> pl.DataFrame: def get_best_candidates( - stat_df: pl.DataFrame, candidate_selection_descriptor: str, nb_top_stable_genes: int + stat_df: pl.DataFrame, + candidate_selection_descriptor: str, + nb_most_stable_genes: int, ) -> list[str]: logger.info("Getting best candidates") column_for_sorting = config.SCORING_BASE_TO_STABILITY_SCORE_COLUMN[ @@ -81,7 +83,7 @@ def get_best_candidates( ] return ( stat_df.sort(column_for_sorting, descending=False, nulls_last=True) - .head(nb_top_stable_genes) + .head(nb_most_stable_genes) .select(config.GENE_ID_COLNAME) .to_series() .to_list() @@ -142,7 +144,7 @@ def main(): # get base candidate genes based on the chosen statistical descriptor (cv, rcvm) best_candidates = get_best_candidates( - stat_df, args.candidate_selection_descriptor, args.nb_top_stable_genes + stat_df, args.candidate_selection_descriptor, args.nb_most_stable_genes ) # get counts for candidate genes diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 2169eae0..034ab8d6 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -9,7 +9,9 @@ WARN: No dataset found. Please note that for the moment only Microarray count da You can check at https://www.ncbi.nlm.nih.gov/gds if there are raw RNA-seq count datasets for this species. ``` -You may want to check if there are any raw RNA-seq count datasets available for this species on [NCBI GEO](https://www.ncbi.nlm.nih.gov/gds). You can then relaunch the pipeline by providing your own count datasets. +You may want to check if there are any raw RNA-seq count datasets available for this species on [NCBI GEO](https://www.ncbi.nlm.nih.gov/gds). You can then relaunch the pipeline by providing your own prepared count datasets. + +## Not enough memory ## Java heap space diff --git a/galaxy/build/static/boilerplate.xml b/galaxy/build/static/boilerplate.xml index 13fc6700..0d79424c 100644 --- a/galaxy/build/static/boilerplate.xml +++ b/galaxy/build/static/boilerplate.xml @@ -59,7 +59,7 @@ INPUTS - + @@ -76,7 +76,7 @@ INPUTS
    - + @@ -95,7 +95,7 @@ INPUTS
    - + @@ -116,7 +116,7 @@ INPUTS - + diff --git a/galaxy/tool_shed/tool/nf_core_stableexpression.xml b/galaxy/tool_shed/tool/nf_core_stableexpression.xml index 6172d00e..15b10895 100644 --- a/galaxy/tool_shed/tool/nf_core_stableexpression.xml +++ b/galaxy/tool_shed/tool/nf_core_stableexpression.xml @@ -210,7 +210,7 @@ VERSION="1.0dev"; echo "$VERSION" - + @@ -227,7 +227,7 @@ VERSION="1.0dev"; echo "$VERSION"
    - + @@ -246,7 +246,7 @@ VERSION="1.0dev"; echo "$VERSION" - + @@ -267,7 +267,7 @@ VERSION="1.0dev"; echo "$VERSION" - + diff --git a/modules/local/aggregate_results/main.nf b/modules/local/aggregate_results/main.nf index 3b806b22..1402f24b 100644 --- a/modules/local/aggregate_results/main.nf +++ b/modules/local/aggregate_results/main.nf @@ -17,9 +17,9 @@ process AGGREGATE_RESULTS { output: path 'all_genes_summary.csv', emit: all_genes_summary - path 'top_stable_genes_summary.csv', emit: top_stable_genes_summary + path 'most_stable_genes_summary.csv', emit: most_stable_genes_summary path 'all_counts_filtered.parquet', emit: all_counts_filtered - path 'top_stable_genes_transposed_counts_filtered.csv', emit: top_stable_genes_transposed_counts_filtered + path 'most_stable_genes_transposed_counts_filtered.csv', emit: most_stable_genes_transposed_counts_filtered tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions diff --git a/modules/local/geo/getdata/main.nf b/modules/local/geo/getdata/main.nf index f848dd1c..99040abd 100644 --- a/modules/local/geo/getdata/main.nf +++ b/modules/local/geo/getdata/main.nf @@ -18,7 +18,7 @@ process GEO_GETDATA { output: path("*.counts.csv"), optional: true, emit: counts path("*.design.csv"), optional: true, emit: design - path("rejected/**"), optional: true + path("rejected/**"), optional: true, emit: rejected tuple val(accession), path("failure_reason.txt"), optional: true, topic: geo_failure_reason tuple val(accession), path("warning_reason.txt"), optional: true, topic: geo_warning_reason tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //"'), topic: versions diff --git a/modules/local/get_candidate_genes/main.nf b/modules/local/get_candidate_genes/main.nf index 64510f21..194585ae 100644 --- a/modules/local/get_candidate_genes/main.nf +++ b/modules/local/get_candidate_genes/main.nf @@ -11,7 +11,7 @@ process GET_CANDIDATE_GENES { path count_file path stat_file val candidate_selection_descriptor - val nb_top_stable_genes + val nb_most_stable_genes val min_pct_quantile_expr_level output: @@ -31,7 +31,7 @@ process GET_CANDIDATE_GENES { --counts $count_file \\ --stats $stat_file \\ --candidate_selection_descriptor $candidate_selection_descriptor \\ - --nb-top-stable-genes $nb_top_stable_genes \\ + --nb-top-stable-genes $nb_most_stable_genes \\ --min-pct-quantile-expr-level $min_pct_quantile_expr_level """ From ce578b3f689103f28c72b027e4f1c65088d2aa98 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Thu, 11 Dec 2025 16:41:07 +0100 Subject: [PATCH 232/258] remove access to params in merge_data and multiqc subworkflows --- subworkflows/local/merge_data/main.nf | 7 +-- subworkflows/local/multiqc/main.nf | 68 +++++++++++++++------------ workflows/stableexpression.nf | 9 +++- 3 files changed, 48 insertions(+), 36 deletions(-) diff --git a/subworkflows/local/merge_data/main.nf b/subworkflows/local/merge_data/main.nf index d86eb5e7..c18de814 100644 --- a/subworkflows/local/merge_data/main.nf +++ b/subworkflows/local/merge_data/main.nf @@ -16,6 +16,7 @@ workflow MERGE_DATA { ch_normalised_counts ch_gene_id_mapping ch_gene_metadata + outdir main: @@ -80,7 +81,7 @@ workflow MERGE_DATA { seed: "batch,condition,sample", newLine: true, sort: true, - storeDir: "${params.outdir}/merged_datasets/" + storeDir: "${outdir}/merged_datasets/" ) { item -> "${item.batch},${item.condition},${item.sample}" } @@ -98,7 +99,7 @@ workflow MERGE_DATA { seed: "original_gene_id,gene_id", newLine: true, sort: true, - storeDir: "${params.outdir}/idmapping/" + storeDir: "${outdir}/idmapping/" ) { item -> "${item.original_gene_id},${item.gene_id}" } @@ -117,7 +118,7 @@ workflow MERGE_DATA { seed: "gene_id,name,description", newLine: true, sort: true, - storeDir: "${params.outdir}/idmapping/" + storeDir: "${outdir}/idmapping/" ) { item -> "${item.gene_id},${item.name},${item.description}" } diff --git a/subworkflows/local/multiqc/main.nf b/subworkflows/local/multiqc/main.nf index 167bee1a..5a0db6d1 100644 --- a/subworkflows/local/multiqc/main.nf +++ b/subworkflows/local/multiqc/main.nf @@ -17,6 +17,10 @@ workflow MULTIQC_WORKFLOW { take: ch_multiqc_files ch_versions + multiqc_config + multiqc_logo + multiqc_methods_description + outdir main: @@ -29,27 +33,29 @@ workflow MULTIQC_WORKFLOW { name: 'id_mapping_stats.csv', seed: "Dataset,mapped,unmapped", newLine: true, - storeDir: "${params.outdir}/statistics/" + storeDir: "${outdir}/statistics/" ) { item -> "${item[0]},${item[1]},${item[2]}" } - ch_skewness = channel.topic('skewness') - .map { dataset, file -> "${dataset},${file.readLines()[0]}" } // concatenate dataset name with skewness values - .collectFile( - name: 'skewness.csv', - newLine: true, - storeDir: "${params.outdir}/statistics/" - ) + ch_skewness = channel.topic('skewness') + .map { dataset, file -> "${dataset},${file.readLines()[0]}" } // concatenate dataset name with skewness values + .collectFile( + name: 'skewness.csv', + newLine: true, + sort: true, + storeDir: "${outdir}/statistics/" + ) - ch_ratio_zeros = channel.topic('ratio_zeros') - .map { dataset, file -> "${dataset},${file.readLines()[0]}" } // concatenate dataset name with skewness values - .collectFile( - name: 'ratio_zeros.csv', - newLine: true, - storeDir: "${params.outdir}/statistics/" - ) + ch_ratio_zeros = channel.topic('ratio_zeros') + .map { dataset, file -> "${dataset},${file.readLines()[0]}" } // concatenate dataset name with ratio values + .collectFile( + name: 'ratio_zeros.csv', + newLine: true, + sort: true, + storeDir: "${outdir}/statistics/" + ) COLLECT_STATISTICS( ch_skewness.mix( ch_ratio_zeros ) @@ -65,7 +71,7 @@ workflow MULTIQC_WORKFLOW { name: 'eatlas_failure_reasons.csv', seed: "Accession,Reason", newLine: true, - storeDir: "${params.outdir}/errors/" + storeDir: "${outdir}/errors/" ) { item -> "${item[0]},${item[1]}" } @@ -76,7 +82,7 @@ workflow MULTIQC_WORKFLOW { name: 'eatlas_warning_reasons.csv', seed: "Accession,Reason", newLine: true, - storeDir: "${params.outdir}/warnings/" + storeDir: "${outdir}/warnings/" ) { item -> "${item[0]},${item[1]}" } @@ -87,7 +93,7 @@ workflow MULTIQC_WORKFLOW { name: 'geo_failure_reasons.csv', seed: "Accession,Reason", newLine: true, - storeDir: "${params.outdir}/errors/" + storeDir: "${outdir}/errors/" ) { item -> "${item[0]},${item[1]}" } @@ -99,7 +105,7 @@ workflow MULTIQC_WORKFLOW { name: 'geo_warning_reasons.csv', seed: "Accession,Reason", newLine: true, - storeDir: "${params.outdir}/warnings/" + storeDir: "${outdir}/warnings/" ) { item -> "${item[0]},${item[1]}" } @@ -110,7 +116,7 @@ workflow MULTIQC_WORKFLOW { name: 'id_cleaning_failure_reasons.tsv', seed: "Dataset\tReason", newLine: true, - storeDir: "${params.outdir}/errors/" + storeDir: "${outdir}/errors/" ) { item -> "${item[0]}\t${item[1]}" } @@ -121,7 +127,7 @@ workflow MULTIQC_WORKFLOW { name: 'renaming_warning_reasons.tsv', seed: "Dataset\tReason", newLine: true, - storeDir: "${params.outdir}/warnings/" + storeDir: "${outdir}/warnings/" ) { item -> "${item[0]}\t${item[1]}" } @@ -132,7 +138,7 @@ workflow MULTIQC_WORKFLOW { name: 'renaming_failure_reasons.tsv', seed: "Dataset\tReason", newLine: true, - storeDir: "${params.outdir}/errors/" + storeDir: "${outdir}/errors/" ) { item -> "${item[0]}\t${item[1]}" } @@ -143,7 +149,7 @@ workflow MULTIQC_WORKFLOW { name: 'normalisation_warning_reasons.tsv', seed: "Dataset\tReason", newLine: true, - storeDir: "${params.outdir}/warnings/" + storeDir: "${outdir}/warnings/" ) { item -> "${item[0]}\t${item[1]}" } @@ -154,7 +160,7 @@ workflow MULTIQC_WORKFLOW { name: 'normalisation_failure_reasons.tsv', seed: "Dataset\tReason", newLine: true, - storeDir: "${params.outdir}/errors/" + storeDir: "${outdir}/errors/" ) { item -> "${item[0]}\t${item[1]}" } @@ -209,7 +215,7 @@ workflow MULTIQC_WORKFLOW { ch_collated_versions = softwareVersionsToYAML(ch_versions.mix(topic_versions.versions_file)) .mix(topic_versions_string) .collectFile( - storeDir: "${params.outdir}/pipeline_info", + storeDir: "${outdir}/pipeline_info", name: 'nf_core_' + 'stableexpression_software_' + 'mqc_' + 'versions.yml', sort: true, newLine: true @@ -222,12 +228,12 @@ workflow MULTIQC_WORKFLOW { 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) : + ch_multiqc_custom_config = multiqc_config ? + channel.fromPath(multiqc_config, checkIfExists: true) : channel.empty() - ch_multiqc_logo = params.multiqc_logo ? - channel.fromPath(params.multiqc_logo, checkIfExists: true) : + ch_multiqc_logo = multiqc_logo ? + channel.fromPath(multiqc_logo, checkIfExists: true) : channel.empty() summary_params = paramsSummaryMap( @@ -239,8 +245,8 @@ workflow MULTIQC_WORKFLOW { 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) : + ch_multiqc_custom_methods_description = multiqc_methods_description ? + file(multiqc_methods_description, checkIfExists: true) : file("$projectDir/assets/methods_description_template.yml", checkIfExists: true) ch_methods_description = channel.value( diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 45d4e347..3383a340 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -132,7 +132,8 @@ workflow STABLEEXPRESSION { MERGE_DATA ( EXPRESSION_NORMALISATION.out.counts, ch_gene_id_mapping, - ch_gene_metadata + ch_gene_metadata, + params.outdir ) ch_all_counts = MERGE_DATA.out.all_counts @@ -208,7 +209,11 @@ workflow STABLEEXPRESSION { MULTIQC_WORKFLOW( ch_multiqc_files, - ch_versions + ch_versions, + params.multiqc_config, + params.multiqc_logo, + params.multiqc_methods_description, + params.outdir ) From 026a84004c1e0cb5f89cebae8d03f85fb344a6dc Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Fri, 12 Dec 2025 19:28:34 +0100 Subject: [PATCH 233/258] add default hard limits for resources in base.config --- conf/base.config | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/conf/base.config b/conf/base.config index 9566094f..eeb8957a 100644 --- a/conf/base.config +++ b/conf/base.config @@ -8,9 +8,19 @@ ---------------------------------------------------------------------------------------- */ +executor { + cpus = 12 + memory = 24.GB +} + process { - // TODO nf-core: Check the defaults for all processes + resourceLimits = [ + cpus: 16, + memory: '25.GB', + time: '4.h' + ] + cpus = { 1 * task.attempt } memory = { 6.GB * task.attempt } time = { 4.h * task.attempt } From 079be55c7d6da15e8076983bfceab07fb4e61663 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Mon, 15 Dec 2025 09:02:15 +0100 Subject: [PATCH 234/258] fix some tests --- bin/collect_gene_ids.py | 3 +- bin/merge_counts.py | 5 + conf/modules.config | 16 +- conf/modules/aggregation.config | 10 + subworkflows/local/base_statistics/main.nf | 6 +- subworkflows/local/genorm/main.nf | 2 +- subworkflows/local/idmapping/main.nf | 9 +- subworkflows/local/merge_data/main.nf | 12 +- subworkflows/local/multiqc/main.nf | 21 +- subworkflows/local/stability_scoring/main.nf | 10 +- tests/default.nf.test | 24 +- tests/default.nf.test.snap | 2421 +++++++++-------- .../local/aggregate_results/main.nf.test.snap | 8 +- .../getaccessions/main.nf.test.snap | 52 +- .../local/geo/getdata/main.nf.test.snap | 72 +- .../local/gprofiler/idmapping/main.nf.test | 2 +- .../gprofiler/idmapping/main.nf.test.snap | 70 - .../download_public_datasets/main.nf.test | 24 +- .../main.nf.test.snap | 43 +- .../local/get_public_accessions/main.nf.test | 10 +- .../get_public_accessions/main.nf.test.snap | 66 +- tests/workflows/nextflow.config | 3 - tests/workflows/stableexpression.nf.test | 198 -- tests/workflows/stableexpression.nf.test.snap | 113 - 24 files changed, 1492 insertions(+), 1708 deletions(-) create mode 100644 conf/modules/aggregation.config delete mode 100644 tests/workflows/nextflow.config delete mode 100644 tests/workflows/stableexpression.nf.test delete mode 100644 tests/workflows/stableexpression.nf.test.snap diff --git a/bin/collect_gene_ids.py b/bin/collect_gene_ids.py index e6ebaf3a..c5a535f7 100755 --- a/bin/collect_gene_ids.py +++ b/bin/collect_gene_ids.py @@ -54,8 +54,9 @@ def main(): df = parse_table(count_file) all_gene_ids.update(list(df.index)) + # sorting IDs in order to have a consistent output with open(ALL_GENE_IDS_OUTFILE, "w") as f: - f.write("\n".join([str(gene_id) for gene_id in all_gene_ids])) + f.write("\n".join(sorted([str(gene_id) for gene_id in all_gene_ids]))) if __name__ == "__main__": diff --git a/bin/merge_counts.py b/bin/merge_counts.py index 67c5d2da..84f78c9b 100755 --- a/bin/merge_counts.py +++ b/bin/merge_counts.py @@ -5,6 +5,7 @@ import argparse import logging from functools import reduce +from operator import attrgetter from pathlib import Path import config @@ -134,7 +135,11 @@ def export_data(count_df: pl.DataFrame): def main(): args = parse_args() + + # parsing count files count_files = [Path(file) for file in args.count_files.split(" ")] + # sorting them by file name to ensure consistent order between runs + count_files.sort(key=attrgetter("name")) logger.info(f"Merging {len(count_files)} count files") # putting all counts into a single dataframe diff --git a/conf/modules.config b/conf/modules.config index 11705c20..5c75b088 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -10,16 +10,16 @@ ---------------------------------------------------------------------------------------- */ -process { - - publishDir = [ - path: { "${params.outdir}/${task.process.tokenize(':')[-1].toLowerCase()}" }, - mode: params.publish_dir_mode - ] - -} +/* +publishDir = [ + path: { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" }, + mode: params.publish_dir_mode, + saveAs: { filename -> filename.equals('versions.yml') ? null : filename } +] +*/ includeConfig 'modules/public_data.config' includeConfig 'modules/id_mapping.config' includeConfig 'modules/normalisation.config' includeConfig 'modules/qc.config' +includeConfig 'modules/aggregation.config' diff --git a/conf/modules/aggregation.config b/conf/modules/aggregation.config new file mode 100644 index 00000000..8b9dfa83 --- /dev/null +++ b/conf/modules/aggregation.config @@ -0,0 +1,10 @@ +process { + + withName: AGGREGATE_RESULTS { + publishDir = [ + path: { "${params.outdir}/aggregated" }, + mode: params.publish_dir_mode + ] + } + +} diff --git a/subworkflows/local/base_statistics/main.nf b/subworkflows/local/base_statistics/main.nf index 091090e8..80e0caf1 100644 --- a/subworkflows/local/base_statistics/main.nf +++ b/subworkflows/local/base_statistics/main.nf @@ -22,12 +22,12 @@ workflow BASE_STATISTICS { // ----------------------------------------------------------------- COMPUTE_BASE_STATISTICS_FOR_RNASEQ( - ch_rnaseq_counts.collect(), + ch_rnaseq_counts.collect(), // single item "rnaseq" ) COMPUTE_BASE_STATISTICS_FOR_MICROARRAY( - ch_microarray_counts.collect(), + ch_microarray_counts.collect(), // single item "microarray" ) @@ -36,7 +36,7 @@ workflow BASE_STATISTICS { // ----------------------------------------------------------------- COMPUTE_BASE_STATISTICS ( - ch_all_counts.collect(), + ch_all_counts.collect(), // single item [] ) diff --git a/subworkflows/local/genorm/main.nf b/subworkflows/local/genorm/main.nf index 340184c2..2a5a061b 100644 --- a/subworkflows/local/genorm/main.nf +++ b/subworkflows/local/genorm/main.nf @@ -40,7 +40,7 @@ workflow GENORM { COMPUTE_M_MEASURE( ch_counts, - RATIO_STANDARD_VARIATION.out.data.collect() + RATIO_STANDARD_VARIATION.out.data.collect( sort: true ) ) emit: diff --git a/subworkflows/local/idmapping/main.nf b/subworkflows/local/idmapping/main.nf index e2da01e3..3d6966c7 100644 --- a/subworkflows/local/idmapping/main.nf +++ b/subworkflows/local/idmapping/main.nf @@ -54,7 +54,8 @@ workflow ID_MAPPING { .unique() .collectFile( name: 'original_gene_ids.txt', - storeDir: "${outdir}/idmapping/" + storeDir: "${outdir}/idmapping/", + sort: true ) // ----------------------------------------------------------------- @@ -86,7 +87,8 @@ workflow ID_MAPPING { name: 'global_gene_id_mapping.csv', seed: "original_gene_id,gene_id", newLine: true, - storeDir: "${outdir}/idmapping/" + storeDir: "${outdir}/idmapping/", + sort: true ) { item -> "${item["original_gene_id"]},${item["gene_id"]}" } @@ -103,7 +105,8 @@ workflow ID_MAPPING { name: 'global_gene_metadata.csv', seed: "gene_id,name,description", newLine: true, - storeDir: "${outdir}/idmapping/" + storeDir: "${outdir}/idmapping/", + sort: true ) { item -> "${item["gene_id"]},${item["name"]},${item["description"]}" } diff --git a/subworkflows/local/merge_data/main.nf b/subworkflows/local/merge_data/main.nf index c18de814..44742b74 100644 --- a/subworkflows/local/merge_data/main.nf +++ b/subworkflows/local/merge_data/main.nf @@ -29,8 +29,8 @@ workflow MERGE_DATA { ch_whole_rnaseq_size = getWholeDatasetSize ( ch_normalised_rnaseq_counts ) MERGE_RNASEQ_COUNTS ( - ch_normalised_rnaseq_counts.map { meta, file -> file }.collect(), - ch_whole_rnaseq_size.collect() + ch_normalised_rnaseq_counts.map { meta, file -> file }.collect( sort: true ), + ch_whole_rnaseq_size.collect() // single item ) // MICROARRAY @@ -38,8 +38,8 @@ workflow MERGE_DATA { ch_whole_microarray_size = getWholeDatasetSize ( ch_normalised_microarray_counts ) MERGE_MICROARRAY_COUNTS ( - ch_normalised_microarray_counts.map { meta, file -> file }.collect(), - ch_whole_microarray_size.collect() + ch_normalised_microarray_counts.map { meta, file -> file }.collect( sort: true ), + ch_whole_microarray_size.collect() // single item ) // ----------------------------------------------------------------- @@ -55,8 +55,8 @@ workflow MERGE_DATA { .reduce { rnaseq_size, microarray_size -> rnaseq_size + microarray_size } MERGE_ALL_COUNTS( - ch_platform_counts.collect(), - ch_whole_size.collect() + ch_platform_counts.collect( sort: true ), + ch_whole_size.collect() // single item ) // ----------------------------------------------------------------- diff --git a/subworkflows/local/multiqc/main.nf b/subworkflows/local/multiqc/main.nf index 5a0db6d1..da711a63 100644 --- a/subworkflows/local/multiqc/main.nf +++ b/subworkflows/local/multiqc/main.nf @@ -71,7 +71,8 @@ workflow MULTIQC_WORKFLOW { name: 'eatlas_failure_reasons.csv', seed: "Accession,Reason", newLine: true, - storeDir: "${outdir}/errors/" + sort: true, + storeDir: "${outdir}/errors/", ) { item -> "${item[0]},${item[1]}" } @@ -82,6 +83,7 @@ workflow MULTIQC_WORKFLOW { name: 'eatlas_warning_reasons.csv', seed: "Accession,Reason", newLine: true, + sort: true, storeDir: "${outdir}/warnings/" ) { item -> "${item[0]},${item[1]}" @@ -93,6 +95,7 @@ workflow MULTIQC_WORKFLOW { name: 'geo_failure_reasons.csv', seed: "Accession,Reason", newLine: true, + sort: true, storeDir: "${outdir}/errors/" ) { item -> "${item[0]},${item[1]}" @@ -105,6 +108,7 @@ workflow MULTIQC_WORKFLOW { name: 'geo_warning_reasons.csv', seed: "Accession,Reason", newLine: true, + sort: true, storeDir: "${outdir}/warnings/" ) { item -> "${item[0]},${item[1]}" @@ -116,6 +120,7 @@ workflow MULTIQC_WORKFLOW { name: 'id_cleaning_failure_reasons.tsv', seed: "Dataset\tReason", newLine: true, + sort: true, storeDir: "${outdir}/errors/" ) { item -> "${item[0]}\t${item[1]}" @@ -127,6 +132,7 @@ workflow MULTIQC_WORKFLOW { name: 'renaming_warning_reasons.tsv', seed: "Dataset\tReason", newLine: true, + sort: true, storeDir: "${outdir}/warnings/" ) { item -> "${item[0]}\t${item[1]}" @@ -138,6 +144,7 @@ workflow MULTIQC_WORKFLOW { name: 'renaming_failure_reasons.tsv', seed: "Dataset\tReason", newLine: true, + sort: true, storeDir: "${outdir}/errors/" ) { item -> "${item[0]}\t${item[1]}" @@ -149,6 +156,7 @@ workflow MULTIQC_WORKFLOW { name: 'normalisation_warning_reasons.tsv', seed: "Dataset\tReason", newLine: true, + sort: true, storeDir: "${outdir}/warnings/" ) { item -> "${item[0]}\t${item[1]}" @@ -160,6 +168,7 @@ workflow MULTIQC_WORKFLOW { name: 'normalisation_failure_reasons.tsv', seed: "Dataset\tReason", newLine: true, + sort: true, storeDir: "${outdir}/errors/" ) { item -> "${item[0]}\t${item[1]}" @@ -171,11 +180,11 @@ workflow MULTIQC_WORKFLOW { // ------------------------------------------------------------------------------------ ch_multiqc_files - .mix( channel.topic('eatlas_all_datasets').collect() ) - .mix( channel.topic('eatlas_selected_datasets').collect() ) - .mix( channel.topic('geo_all_datasets').collect() ) - .mix( channel.topic('geo_selected_datasets').collect() ) - .mix( channel.topic('geo_rejected_datasets').collect() ) + .mix( channel.topic('eatlas_all_datasets').collect() ) // single item + .mix( channel.topic('eatlas_selected_datasets').collect() ) // single item + .mix( channel.topic('geo_all_datasets').collect() ) // single item + .mix( channel.topic('geo_selected_datasets').collect() ) // single item + .mix( channel.topic('geo_rejected_datasets').collect() ) // single item .mix( COLLECT_STATISTICS.out.csv ) .mix( ch_id_mapping_stats ) .mix( ch_eatlas_failure_reasons ) diff --git a/subworkflows/local/stability_scoring/main.nf b/subworkflows/local/stability_scoring/main.nf index 55785826..efdb0c8b 100644 --- a/subworkflows/local/stability_scoring/main.nf +++ b/subworkflows/local/stability_scoring/main.nf @@ -28,8 +28,8 @@ workflow STABILITY_SCORING { // ----------------------------------------------------------------- GET_CANDIDATE_GENES( - ch_counts.collect(), - ch_stats.collect(), + ch_counts.collect(), // single item + ch_stats.collect(), // single item candidate_selection_descriptor, nb_top_gene_candidates, min_expr_threshold @@ -41,8 +41,8 @@ workflow STABILITY_SCORING { // ----------------------------------------------------------------- NORMFINDER ( - ch_candidate_gene_counts.collect(), - ch_design.collect() + ch_candidate_gene_counts.collect(), // single item + ch_design.collect() // single item ) // ----------------------------------------------------------------- @@ -61,7 +61,7 @@ workflow STABILITY_SCORING { // ----------------------------------------------------------------- COMPUTE_STABILITY_SCORES ( - ch_stats.collect(), + ch_stats.collect(), // single item stability_score_weights, NORMFINDER.out.stability_values, ch_genorm_stability diff --git a/tests/default.nf.test b/tests/default.nf.test index 7c5438ed..5900b86c 100644 --- a/tests/default.nf.test +++ b/tests/default.nf.test @@ -58,7 +58,6 @@ nextflow_pipeline { } test("-profile test_accessions_only") { - tag "test_accessions_only" when { @@ -89,7 +88,6 @@ nextflow_pipeline { } test("-profile test_download_only") { - tag "test_download_only" when { @@ -120,13 +118,12 @@ nextflow_pipeline { } test("-profile test_one_accession_low_gene_count") { - tag "test_one_accession_low_gene_count" when { params { species = 'arabidopsis thaliana' - eatlas_accessions = "E-GEOD-51720" + accessions = "E-GEOD-51720" skip_fetch_eatlas_accessions = true outdir = "$outputDir" } @@ -238,6 +235,25 @@ nextflow_pipeline { } } + then { + assert !workflow.success + } + } + + test("-profile test_included_and_excluded_accessions") { + tag "test_included_and_excluded_accessions" + + when { + params { + species = "solanum tuberosum" + accessions = "E-MTAB-552,E-GEOD-61690" + excluded_accessions = "E-MTAB-4251" + accessions_file = "${projectDir}/tests/test_data/misc/accessions_to_include.txt" + excluded_accessions_file = "${projectDir}/tests/test_data/misc/excluded_accessions.txt" + outdir = "$outputDir" + } + } + then { // stable_name: All files + folders in ${params.outdir}/ with a stable name def stable_name = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}']) diff --git a/tests/default.nf.test.snap b/tests/default.nf.test.snap index 985c18a9..cc34c9cb 100644 --- a/tests/default.nf.test.snap +++ b/tests/default.nf.test.snap @@ -1,29 +1,111 @@ { "-profile test_dataset_only": { "content": [ - null, + { + "AGGREGATE_RESULTS": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "CLEAN_GENE_IDS": { + "pandas": "2.3.3", + "polars": "1.35.2", + "python": "3.14.0" + }, + "COLLECT_GENE_IDS": { + "pandas": "2.3.3", + "python": "3.14.0", + "tqdm": "4.67.1" + }, + "COLLECT_STATISTICS": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "COMPUTE_BASE_STATISTICS": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_BASE_STATISTICS_FOR_RNASEQ": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_DATASET_STATISTICS": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "COMPUTE_GENE_TRANSCRIPT_LENGTHS": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "COMPUTE_STABILITY_SCORES": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_TPM": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "DASH_APP": { + "python": "3.13.8", + "dash": "3.2.0", + "dash-extensions": "2.0.4", + "dash-mantine-components": "2.3.0", + "dash-ag-grid": "32.3.2", + "polars": "1.35.0", + "pandas": "2.3.3", + "pyarrow": "22.0.0", + "scipy": "1.16.3" + }, + "DOWNLOAD_ENSEMBL_ANNOTATION": { + "bs4": "4.14.2", + "pandas": "2.3.3", + "python": "3.14.0", + "requests": "2.32.5", + "tqdm": "4.67.1" + }, + "GET_CANDIDATE_GENES": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "GPROFILER_IDMAPPING": { + "pandas": "2.3.1", + "python": "3.13.5", + "requests": "2.32.4" + }, + "MERGE_ALL_COUNTS": { + "polars": "1.34.0", + "python": "3.14.0", + "tqdm": "4.67.1" + }, + "MERGE_RNASEQ_COUNTS": { + "polars": "1.34.0", + "python": "3.14.0", + "tqdm": "4.67.1" + }, + "NORMFINDER": { + "polars": "1.33.1", + "python": "3.13.7" + }, + "QUANTILE_NORMALISATION": { + "pandas": "2.2.3", + "pyarrow": "19.0.0", + "python": "3.12.8", + "scikit-learn": "1.6.1" + }, + "RENAME_GENE_IDS": { + "pandas": "2.3.3", + "polars": "1.35.2", + "python": "3.14.0" + }, + "Workflow": { + "nf-core/stableexpression": "v1.0dev" + } + }, [ - "aggregate_results", - "aggregate_results/all_counts_filtered.parquet", - "aggregate_results/all_genes_summary.csv", - "aggregate_results/top_stable_genes_summary.csv", - "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", - "clean_gene_ids", - "clean_gene_ids/SRP254919.salmon.merged.gene_counts.top1000cov.assay.cleaned.csv", - "collect_statistics", - "collect_statistics/ratio_zeros.transposed.csv", - "collect_statistics/skewness.transposed.csv", - "compute_base_statistics", - "compute_base_statistics/stats_all_genes.csv", - "compute_base_statistics_for_rnaseq", - "compute_base_statistics_for_rnaseq/rnaseq.stats_all_genes.csv", - "compute_dataset_statistics", - "compute_dataset_statistics/ratio_zeros.txt", - "compute_dataset_statistics/skewness.txt", - "compute_gene_transcript_lengths", - "compute_gene_transcript_lengths/gene_transcript_lengths.csv", - "compute_stability_scores", - "compute_stability_scores/stats_with_scores.csv", + "aggregated", + "aggregated/all_counts_filtered.parquet", + "aggregated/all_genes_summary.csv", + "aggregated/most_stable_genes_summary.csv", + "aggregated/most_stable_genes_transposed_counts_filtered.csv", "dash_app", "dash_app/app.py", "dash_app/assets", @@ -56,11 +138,7 @@ "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", "dash_app/versions.yml", - "download_ensembl_annotation", - "download_ensembl_annotation/Mus_musculus.GRCm39.115.chr.gff3.gz", "errors", - "get_candidate_genes", - "get_candidate_genes/candidate_counts.parquet", "idmapping", "idmapping/collected_gene_ids", "idmapping/collected_gene_ids/all_gene_ids.txt", @@ -75,10 +153,6 @@ "idmapping/renamed/warning_reason.txt", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", - "merge_all_counts", - "merge_all_counts/all_counts.parquet", - "merge_rnaseq_counts", - "merge_rnaseq_counts/all_counts.parquet", "merged_datasets", "merged_datasets/whole_design.csv", "multiqc", @@ -88,9 +162,10 @@ "multiqc/multiqc_data/multiqc.parquet", "multiqc/multiqc_data/multiqc_citations.txt", "multiqc/multiqc_data/multiqc_data.json", - "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", + "multiqc/multiqc_data/multiqc_expression_distributions_most_stable_genes.txt", "multiqc/multiqc_data/multiqc_gene_statistics.txt", - "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", + "multiqc/multiqc_data/multiqc_id_mapping_stats.txt", + "multiqc/multiqc_data/multiqc_ranked_most_stable_genes_summary.txt", "multiqc/multiqc_data/multiqc_ratio_zeros.txt", "multiqc/multiqc_data/multiqc_renaming_warning_reasons.txt", "multiqc/multiqc_data/multiqc_skewness.txt", @@ -98,23 +173,29 @@ "multiqc/multiqc_data/multiqc_sources.txt", "multiqc/multiqc_plots", "multiqc/multiqc_plots/pdf", - "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", + "multiqc/multiqc_plots/pdf/expression_distributions_most_stable_genes.pdf", "multiqc/multiqc_plots/pdf/gene_statistics.pdf", - "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", + "multiqc/multiqc_plots/pdf/id_mapping_stats-cnt.pdf", + "multiqc/multiqc_plots/pdf/id_mapping_stats-pct.pdf", + "multiqc/multiqc_plots/pdf/ranked_most_stable_genes_summary.pdf", "multiqc/multiqc_plots/pdf/ratio_zeros.pdf", "multiqc/multiqc_plots/pdf/renaming_warning_reasons.pdf", "multiqc/multiqc_plots/pdf/skewness.pdf", "multiqc/multiqc_plots/png", - "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", + "multiqc/multiqc_plots/png/expression_distributions_most_stable_genes.png", "multiqc/multiqc_plots/png/gene_statistics.png", - "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", + "multiqc/multiqc_plots/png/id_mapping_stats-cnt.png", + "multiqc/multiqc_plots/png/id_mapping_stats-pct.png", + "multiqc/multiqc_plots/png/ranked_most_stable_genes_summary.png", "multiqc/multiqc_plots/png/ratio_zeros.png", "multiqc/multiqc_plots/png/renaming_warning_reasons.png", "multiqc/multiqc_plots/png/skewness.png", "multiqc/multiqc_plots/svg", - "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", + "multiqc/multiqc_plots/svg/expression_distributions_most_stable_genes.svg", "multiqc/multiqc_plots/svg/gene_statistics.svg", - "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", + "multiqc/multiqc_plots/svg/id_mapping_stats-cnt.svg", + "multiqc/multiqc_plots/svg/id_mapping_stats-pct.svg", + "multiqc/multiqc_plots/svg/ranked_most_stable_genes_summary.svg", "multiqc/multiqc_plots/svg/ratio_zeros.svg", "multiqc/multiqc_plots/svg/renaming_warning_reasons.svg", "multiqc/multiqc_plots/svg/skewness.svg", @@ -126,10 +207,8 @@ "normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay/quantile_normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay.cleaned.renamed.tpm.quant_norm.parquet", "normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay/tpm", "normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay/tpm/SRP254919.salmon.merged.gene_counts.top1000cov.assay.cleaned.renamed.tpm.csv", - "normfinder", - "normfinder/stability_values.normfinder.csv", "pipeline_info", - "pipeline_info/software_mqc_versions.yml", + "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", "statistics", "statistics/id_mapping_stats.csv", "statistics/ratio_zeros.csv", @@ -139,17 +218,8 @@ ], [ "all_genes_summary.csv:md5,54b96cbfb5e464ec086c7e1308701e02", - "top_stable_genes_summary.csv:md5,54b96cbfb5e464ec086c7e1308701e02", - "top_stable_genes_transposed_counts_filtered.csv:md5,af21e36c540965846b73245678b74f36", - "SRP254919.salmon.merged.gene_counts.top1000cov.assay.cleaned.csv:md5,c5f077545a14e2078194217ff227d3fc", - "ratio_zeros.transposed.csv:md5,735d304aca8999a5f6e015de297ac1cd", - "skewness.transposed.csv:md5,3ff31df5b69ba9b12181024de818e54e", - "stats_all_genes.csv:md5,4e67b237e0c420363cd5e90add47eb21", - "rnaseq.stats_all_genes.csv:md5,5bcc5671388dc946d8f4e276de23c584", - "ratio_zeros.txt:md5,fa050b20490f0186d5aae53c3355b520", - "skewness.txt:md5,6b257ec852978ea594bec11668ada22f", - "gene_transcript_lengths.csv:md5,09e2d2a8881df9aa96ee71802e9c3451", - "stats_with_scores.csv:md5,d9236329475b316e1e1983ccb76f0c1b", + "most_stable_genes_summary.csv:md5,54b96cbfb5e464ec086c7e1308701e02", + "most_stable_genes_transposed_counts_filtered.csv:md5,af21e36c540965846b73245678b74f36", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", "all_genes_summary.csv:md5,54b96cbfb5e464ec086c7e1308701e02", @@ -172,57 +242,146 @@ "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", "style.py:md5,b9f1207f06464e43c05e6e3912f12731", "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "Mus_musculus.GRCm39.115.chr.gff3.gz:md5,66a5d70eeb2ce9685ca871fc7b0f4f96", - "all_gene_ids.txt:md5,6e8078e6c239924656b22f923948cc4e", - "global_gene_id_mapping.csv:md5,d907d4ba88b4f56a268ea6f9477e76bc", - "global_gene_metadata.csv:md5,48de6014d2f8461a5c1abc7647d202dc", - "gene_metadata.csv:md5,c69fa32bf2cc150958d3b0dbe809d946", - "mapped_gene_ids.csv:md5,9a27b6f030f45d39f05cd8fbf3388383", - "original_gene_ids.txt:md5,e6c4d2e009b8af56746d15806786f8b2", + "all_gene_ids.txt:md5,6b2ece983fd9da133e719914216852b0", + "global_gene_id_mapping.csv:md5,78934d2ac5fe7d863f114c5703f57a06", + "global_gene_metadata.csv:md5,bd76860b422e45eca7cd583212a977d2", + "gene_metadata.csv:md5,bd76860b422e45eca7cd583212a977d2", + "mapped_gene_ids.csv:md5,78934d2ac5fe7d863f114c5703f57a06", + "original_gene_ids.txt:md5,0c6b71845bdf783b426294fa7993da94", "SRP254919.salmon.merged.gene_counts.top1000cov.assay.cleaned.renamed.csv:md5,c855d1848a74842378c812556ddd2e1f", "warning_reason.txt:md5,b13d82afc1a3752e78dd796fb1c53d52", "whole_gene_id_mapping.csv:md5,78934d2ac5fe7d863f114c5703f57a06", "whole_gene_metadata.csv:md5,bd76860b422e45eca7cd583212a977d2", "whole_design.csv:md5,f29515bc2c783e593fb9028127342593", "SRP254919.salmon.merged.gene_counts.top1000cov.assay.cleaned.renamed.tpm.csv:md5,5b517b410a643c4e6fbffb55dc1cd1a7", - "stability_values.normfinder.csv:md5,000e97bed8b1c468ec71704d6accc804", - "id_mapping_stats.csv:md5,2b1b9478e4eafbfd43a078894d9be122", + "id_mapping_stats.csv:md5,14cdb53685228e5e5393cf9404856b41", "ratio_zeros.csv:md5,5a667d505cbd2cc7057ee47b70536c2e", "skewness.csv:md5,582683980eadf84d32853a21f9dce230", "renaming_warning_reasons.tsv:md5,0a11a59b5b547a39ab7a0e4dac622173" ] ], "meta": { - "nf-test": "0.9.2", + "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-04T11:38:53.191359064" + "timestamp": "2025-12-15T16:45:46.754894228" }, "-profile test_eatlas_only_with_keywords": { "content": [ - null, + { + "AGGREGATE_RESULTS": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "CLEAN_GENE_IDS": { + "pandas": "2.3.3", + "polars": "1.35.2", + "python": "3.14.0" + }, + "COLLECT_GENE_IDS": { + "pandas": "2.3.3", + "python": "3.14.0", + "tqdm": "4.67.1" + }, + "COLLECT_STATISTICS": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "COMPUTE_BASE_STATISTICS": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_BASE_STATISTICS_FOR_RNASEQ": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_DATASET_STATISTICS": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "COMPUTE_GENE_TRANSCRIPT_LENGTHS": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "COMPUTE_STABILITY_SCORES": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_TPM": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "DASH_APP": { + "python": "3.13.8", + "dash": "3.2.0", + "dash-extensions": "2.0.4", + "dash-mantine-components": "2.3.0", + "dash-ag-grid": "32.3.2", + "polars": "1.35.0", + "pandas": "2.3.3", + "pyarrow": "22.0.0", + "scipy": "1.16.3" + }, + "DOWNLOAD_ENSEMBL_ANNOTATION": { + "bs4": "4.14.2", + "pandas": "2.3.3", + "python": "3.14.0", + "requests": "2.32.5", + "tqdm": "4.67.1" + }, + "EXPRESSION_ATLAS": { + "ExpressionAtlas": "1.30.0", + "R": "4.3.3 (2024-02-29)", + "nltk": "3.9.1", + "pandas": "2.3.0", + "python": "3.13.5", + "pyyaml": "6.0.2", + "requests": "2.32.4" + }, + "GET_CANDIDATE_GENES": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "GPROFILER_IDMAPPING": { + "pandas": "2.3.1", + "python": "3.13.5", + "requests": "2.32.4" + }, + "MERGE_ALL_COUNTS": { + "polars": "1.34.0", + "python": "3.14.0", + "tqdm": "4.67.1" + }, + "MERGE_RNASEQ_COUNTS": { + "polars": "1.34.0", + "python": "3.14.0", + "tqdm": "4.67.1" + }, + "NORMFINDER": { + "polars": "1.33.1", + "python": "3.13.7" + }, + "QUANTILE_NORMALISATION": { + "pandas": "2.2.3", + "pyarrow": "19.0.0", + "python": "3.12.8", + "scikit-learn": "1.6.1" + }, + "RENAME_GENE_IDS": { + "pandas": "2.3.3", + "polars": "1.35.2", + "python": "3.14.0" + }, + "Workflow": { + "nf-core/stableexpression": "v1.0dev" + } + }, [ - "aggregate_results", - "aggregate_results/all_counts_filtered.parquet", - "aggregate_results/all_genes_summary.csv", - "aggregate_results/top_stable_genes_summary.csv", - "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", - "clean_gene_ids", - "clean_gene_ids/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.csv", - "collect_statistics", - "collect_statistics/ratio_zeros.transposed.csv", - "collect_statistics/skewness.transposed.csv", - "compute_base_statistics", - "compute_base_statistics/stats_all_genes.csv", - "compute_base_statistics_for_rnaseq", - "compute_base_statistics_for_rnaseq/rnaseq.stats_all_genes.csv", - "compute_dataset_statistics", - "compute_dataset_statistics/ratio_zeros.txt", - "compute_dataset_statistics/skewness.txt", - "compute_gene_transcript_lengths", - "compute_gene_transcript_lengths/gene_transcript_lengths.csv", - "compute_stability_scores", - "compute_stability_scores/stats_with_scores.csv", + "aggregated", + "aggregated/all_counts_filtered.parquet", + "aggregated/all_genes_summary.csv", + "aggregated/most_stable_genes_summary.csv", + "aggregated/most_stable_genes_transposed_counts_filtered.csv", "dash_app", "dash_app/app.py", "dash_app/assets", @@ -255,11 +414,7 @@ "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", "dash_app/versions.yml", - "download_ensembl_annotation", - "download_ensembl_annotation/Beta_vulgaris.RefBeet-1.2.2.62.gff3.gz", "errors", - "get_candidate_genes", - "get_candidate_genes/candidate_counts.parquet", "idmapping", "idmapping/collected_gene_ids", "idmapping/collected_gene_ids/all_gene_ids.txt", @@ -273,10 +428,6 @@ "idmapping/renamed/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", - "merge_all_counts", - "merge_all_counts/all_counts.parquet", - "merge_rnaseq_counts", - "merge_rnaseq_counts/all_counts.parquet", "merged_datasets", "merged_datasets/whole_design.csv", "multiqc", @@ -288,9 +439,10 @@ "multiqc/multiqc_data/multiqc_data.json", "multiqc/multiqc_data/multiqc_eatlas_all_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", + "multiqc/multiqc_data/multiqc_expression_distributions_most_stable_genes.txt", "multiqc/multiqc_data/multiqc_gene_statistics.txt", - "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", + "multiqc/multiqc_data/multiqc_id_mapping_stats.txt", + "multiqc/multiqc_data/multiqc_ranked_most_stable_genes_summary.txt", "multiqc/multiqc_data/multiqc_ratio_zeros.txt", "multiqc/multiqc_data/multiqc_skewness.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", @@ -299,25 +451,31 @@ "multiqc/multiqc_plots/pdf", "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", + "multiqc/multiqc_plots/pdf/expression_distributions_most_stable_genes.pdf", "multiqc/multiqc_plots/pdf/gene_statistics.pdf", - "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", + "multiqc/multiqc_plots/pdf/id_mapping_stats-cnt.pdf", + "multiqc/multiqc_plots/pdf/id_mapping_stats-pct.pdf", + "multiqc/multiqc_plots/pdf/ranked_most_stable_genes_summary.pdf", "multiqc/multiqc_plots/pdf/ratio_zeros.pdf", "multiqc/multiqc_plots/pdf/skewness.pdf", "multiqc/multiqc_plots/png", "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", - "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", + "multiqc/multiqc_plots/png/expression_distributions_most_stable_genes.png", "multiqc/multiqc_plots/png/gene_statistics.png", - "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", + "multiqc/multiqc_plots/png/id_mapping_stats-cnt.png", + "multiqc/multiqc_plots/png/id_mapping_stats-pct.png", + "multiqc/multiqc_plots/png/ranked_most_stable_genes_summary.png", "multiqc/multiqc_plots/png/ratio_zeros.png", "multiqc/multiqc_plots/png/skewness.png", "multiqc/multiqc_plots/svg", "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", + "multiqc/multiqc_plots/svg/expression_distributions_most_stable_genes.svg", "multiqc/multiqc_plots/svg/gene_statistics.svg", - "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", + "multiqc/multiqc_plots/svg/id_mapping_stats-cnt.svg", + "multiqc/multiqc_plots/svg/id_mapping_stats-pct.svg", + "multiqc/multiqc_plots/svg/ranked_most_stable_genes_summary.svg", "multiqc/multiqc_plots/svg/ratio_zeros.svg", "multiqc/multiqc_plots/svg/skewness.svg", "multiqc/multiqc_report.html", @@ -328,10 +486,8 @@ "normalised/E_MTAB_8187_rnaseq/quantile_normalised/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", "normalised/E_MTAB_8187_rnaseq/tpm", "normalised/E_MTAB_8187_rnaseq/tpm/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", - "normfinder", - "normfinder/stability_values.normfinder.csv", "pipeline_info", - "pipeline_info/software_mqc_versions.yml", + "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", "public_data", "public_data/expression_atlas", "public_data/expression_atlas/accessions", @@ -349,17 +505,8 @@ ], [ "all_genes_summary.csv:md5,32be3aeed5cd2715847ccc2fd45c09a0", - "top_stable_genes_summary.csv:md5,2e34161e2beb2f1e0921dbf6c0ce55ba", - "top_stable_genes_transposed_counts_filtered.csv:md5,5083c04020d1c504c86689e14f731ef7", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.csv:md5,c7f0662425535e1c04bbc58b22ba74eb", - "ratio_zeros.transposed.csv:md5,4a4dd03b969fbc1ae1d524bfdd90f92b", - "skewness.transposed.csv:md5,bc24ec43d4f638380fd7b0c726ddc157", - "stats_all_genes.csv:md5,43037fa4411410c93551f3d148cd428e", - "rnaseq.stats_all_genes.csv:md5,db157ff5e8c39681d82b9c75a36d3c75", - "ratio_zeros.txt:md5,ae64e9788a4e48e276ecdea3b33abb3f", - "skewness.txt:md5,ad6408796ac8bc51f29018c33f53cb55", - "gene_transcript_lengths.csv:md5,458c7dfd3598bdcbcb6ceb76ccba189f", - "stats_with_scores.csv:md5,3386b99dee6df83b3ea4ad7295f3fb97", + "most_stable_genes_summary.csv:md5,2e34161e2beb2f1e0921dbf6c0ce55ba", + "most_stable_genes_transposed_counts_filtered.csv:md5,5083c04020d1c504c86689e14f731ef7", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", "all_genes_summary.csv:md5,32be3aeed5cd2715847ccc2fd45c09a0", @@ -382,66 +529,408 @@ "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", "style.py:md5,b9f1207f06464e43c05e6e3912f12731", "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "Beta_vulgaris.RefBeet-1.2.2.62.gff3.gz:md5,6f2c45809441c8776e6578000db2b0e4", - "all_gene_ids.txt:md5,1e5fd5cf759295190a0f0d5355011f54", - "global_gene_id_mapping.csv:md5,c664fae78b7433ded148bd94bcd23c67", - "global_gene_metadata.csv:md5,68d3a0bf612bef3f20d370012400dca5", - "gene_metadata.csv:md5,be6187aa4ae724f7e37e5a07099c40af", - "mapped_gene_ids.csv:md5,f26e6d00ff48a07fe28c056bbd6cde8c", - "original_gene_ids.txt:md5,8b21a07b1b6ac1f3d5aa6e2d40b28fc0", + "all_gene_ids.txt:md5,ba79f5609df755c0f75de41357319c84", + "global_gene_id_mapping.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", + "global_gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", + "gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", + "mapped_gene_ids.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", + "original_gene_ids.txt:md5,60a0406c1d56424cfc394c438de50c99", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,ed1600e42df05fc885a16a66b77324a1", "whole_gene_id_mapping.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", "whole_gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,b407990bb10106de943f9e93cda6323e", - "stability_values.normfinder.csv:md5,c762f74840a6e604d43c8f727a5826f5", "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", - "id_mapping_stats.csv:md5,9907d7b6c08067e23cc1b69d5966c998", + "id_mapping_stats.csv:md5,6189d2a346cce55f182e00769e3fea5f", "ratio_zeros.csv:md5,d3b518709a097d9e41a05142b524f03c", "skewness.csv:md5,b38aabc94d60d93b979c3cef3a922299" ] ], "meta": { - "nf-test": "0.9.2", + "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-04T11:42:55.68050177" + "timestamp": "2025-12-15T16:51:02.033842652" + }, + "-profile test_included_and_excluded_accessions": { + "content": [ + { + "AGGREGATE_RESULTS": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "CLEAN_GENE_IDS": { + "pandas": "2.3.3", + "polars": "1.35.2", + "python": "3.14.0" + }, + "COLLECT_GENE_IDS": { + "pandas": "2.3.3", + "python": "3.14.0", + "tqdm": "4.67.1" + }, + "COLLECT_STATISTICS": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "COMPUTE_BASE_STATISTICS": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_BASE_STATISTICS_FOR_RNASEQ": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_DATASET_STATISTICS": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "COMPUTE_GENE_TRANSCRIPT_LENGTHS": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "COMPUTE_STABILITY_SCORES": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_TPM": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "DASH_APP": { + "python": "3.13.8", + "dash": "3.2.0", + "dash-extensions": "2.0.4", + "dash-mantine-components": "2.3.0", + "dash-ag-grid": "32.3.2", + "polars": "1.35.0", + "pandas": "2.3.3", + "pyarrow": "22.0.0", + "scipy": "1.16.3" + }, + "DOWNLOAD_ENSEMBL_ANNOTATION": { + "bs4": "4.14.2", + "pandas": "2.3.3", + "python": "3.14.0", + "requests": "2.32.5", + "tqdm": "4.67.1" + }, + "EXPRESSION_ATLAS": { + "ExpressionAtlas": "1.30.0", + "R": "4.3.3 (2024-02-29)", + "nltk": "3.9.1", + "pandas": "2.3.0", + "python": "3.13.5", + "pyyaml": "6.0.2", + "requests": "2.32.4" + }, + "GET_CANDIDATE_GENES": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "GPROFILER_IDMAPPING": { + "pandas": "2.3.1", + "python": "3.13.5", + "requests": "2.32.4" + }, + "MERGE_ALL_COUNTS": { + "polars": "1.34.0", + "python": "3.14.0", + "tqdm": "4.67.1" + }, + "MERGE_RNASEQ_COUNTS": { + "polars": "1.34.0", + "python": "3.14.0", + "tqdm": "4.67.1" + }, + "NORMFINDER": { + "polars": "1.33.1", + "python": "3.13.7" + }, + "QUANTILE_NORMALISATION": { + "pandas": "2.2.3", + "pyarrow": "19.0.0", + "python": "3.12.8", + "scikit-learn": "1.6.1" + }, + "RENAME_GENE_IDS": { + "pandas": "2.3.3", + "polars": "1.35.2", + "python": "3.14.0" + }, + "Workflow": { + "nf-core/stableexpression": "v1.0dev" + } + }, + [ + "aggregated", + "aggregated/all_counts_filtered.parquet", + "aggregated/all_genes_summary.csv", + "aggregated/most_stable_genes_summary.csv", + "aggregated/most_stable_genes_transposed_counts_filtered.csv", + "dash_app", + "dash_app/app.py", + "dash_app/assets", + "dash_app/assets/style.css", + "dash_app/data", + "dash_app/data/all_counts.parquet", + "dash_app/data/all_genes_summary.csv", + "dash_app/data/whole_design.csv", + "dash_app/environment.yml", + "dash_app/file_system_backend", + "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", + "dash_app/src", + "dash_app/src/callbacks", + "dash_app/src/callbacks/common.py", + "dash_app/src/callbacks/genes.py", + "dash_app/src/callbacks/samples.py", + "dash_app/src/components", + "dash_app/src/components/graphs.py", + "dash_app/src/components/icons.py", + "dash_app/src/components/right_sidebar.py", + "dash_app/src/components/settings", + "dash_app/src/components/settings/genes.py", + "dash_app/src/components/settings/samples.py", + "dash_app/src/components/stores.py", + "dash_app/src/components/tables.py", + "dash_app/src/components/tooltips.py", + "dash_app/src/components/top.py", + "dash_app/src/utils", + "dash_app/src/utils/config.py", + "dash_app/src/utils/data_management.py", + "dash_app/src/utils/style.py", + "dash_app/versions.yml", + "errors", + "errors/eatlas_failure_reasons.csv", + "errors/renaming_failure_reasons.tsv", + "idmapping", + "idmapping/collected_gene_ids", + "idmapping/collected_gene_ids/all_gene_ids.txt", + "idmapping/global_gene_id_mapping.csv", + "idmapping/global_gene_metadata.csv", + "idmapping/gprofiler", + "idmapping/gprofiler/gene_metadata.csv", + "idmapping/gprofiler/mapped_gene_ids.csv", + "idmapping/original_gene_ids.txt", + "idmapping/renamed", + "idmapping/renamed/E_GEOD_61690_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", + "idmapping/renamed/E_GEOD_77826_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", + "idmapping/renamed/E_MTAB_5038_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", + "idmapping/renamed/E_MTAB_5215_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", + "idmapping/renamed/E_MTAB_552_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", + "idmapping/renamed/E_MTAB_7711_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", + "idmapping/renamed/failure_reason.txt", + "idmapping/whole_gene_id_mapping.csv", + "idmapping/whole_gene_metadata.csv", + "merged_datasets", + "merged_datasets/whole_design.csv", + "multiqc", + "multiqc/multiqc_data", + "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_eatlas_all_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_eatlas_failure_reasons.txt", + "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_expression_distributions_most_stable_genes.txt", + "multiqc/multiqc_data/multiqc_gene_statistics.txt", + "multiqc/multiqc_data/multiqc_id_mapping_stats.txt", + "multiqc/multiqc_data/multiqc_ranked_most_stable_genes_summary.txt", + "multiqc/multiqc_data/multiqc_ratio_zeros.txt", + "multiqc/multiqc_data/multiqc_renaming_failure_reasons.txt", + "multiqc/multiqc_data/multiqc_skewness.txt", + "multiqc/multiqc_data/multiqc_software_versions.txt", + "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_plots", + "multiqc/multiqc_plots/pdf", + "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/eatlas_failure_reasons.pdf", + "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/expression_distributions_most_stable_genes.pdf", + "multiqc/multiqc_plots/pdf/gene_statistics.pdf", + "multiqc/multiqc_plots/pdf/id_mapping_stats-cnt.pdf", + "multiqc/multiqc_plots/pdf/id_mapping_stats-pct.pdf", + "multiqc/multiqc_plots/pdf/ranked_most_stable_genes_summary.pdf", + "multiqc/multiqc_plots/pdf/ratio_zeros.pdf", + "multiqc/multiqc_plots/pdf/renaming_failure_reasons.pdf", + "multiqc/multiqc_plots/pdf/skewness.pdf", + "multiqc/multiqc_plots/png", + "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", + "multiqc/multiqc_plots/png/eatlas_failure_reasons.png", + "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", + "multiqc/multiqc_plots/png/expression_distributions_most_stable_genes.png", + "multiqc/multiqc_plots/png/gene_statistics.png", + "multiqc/multiqc_plots/png/id_mapping_stats-cnt.png", + "multiqc/multiqc_plots/png/id_mapping_stats-pct.png", + "multiqc/multiqc_plots/png/ranked_most_stable_genes_summary.png", + "multiqc/multiqc_plots/png/ratio_zeros.png", + "multiqc/multiqc_plots/png/renaming_failure_reasons.png", + "multiqc/multiqc_plots/png/skewness.png", + "multiqc/multiqc_plots/svg", + "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/eatlas_failure_reasons.svg", + "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/expression_distributions_most_stable_genes.svg", + "multiqc/multiqc_plots/svg/gene_statistics.svg", + "multiqc/multiqc_plots/svg/id_mapping_stats-cnt.svg", + "multiqc/multiqc_plots/svg/id_mapping_stats-pct.svg", + "multiqc/multiqc_plots/svg/ranked_most_stable_genes_summary.svg", + "multiqc/multiqc_plots/svg/ratio_zeros.svg", + "multiqc/multiqc_plots/svg/renaming_failure_reasons.svg", + "multiqc/multiqc_plots/svg/skewness.svg", + "multiqc/multiqc_report.html", + "multiqc/versions.yml", + "normalised", + "normalised/E_GEOD_61690_rnaseq", + "normalised/E_GEOD_61690_rnaseq/quantile_normalised", + "normalised/E_GEOD_61690_rnaseq/quantile_normalised/E_GEOD_61690_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/E_GEOD_61690_rnaseq/tpm", + "normalised/E_GEOD_61690_rnaseq/tpm/E_GEOD_61690_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", + "normalised/E_GEOD_77826_rnaseq", + "normalised/E_GEOD_77826_rnaseq/quantile_normalised", + "normalised/E_GEOD_77826_rnaseq/quantile_normalised/E_GEOD_77826_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/E_GEOD_77826_rnaseq/tpm", + "normalised/E_GEOD_77826_rnaseq/tpm/E_GEOD_77826_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", + "normalised/E_MTAB_5038_rnaseq", + "normalised/E_MTAB_5038_rnaseq/quantile_normalised", + "normalised/E_MTAB_5038_rnaseq/quantile_normalised/E_MTAB_5038_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/E_MTAB_5038_rnaseq/tpm", + "normalised/E_MTAB_5038_rnaseq/tpm/E_MTAB_5038_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", + "normalised/E_MTAB_5215_rnaseq", + "normalised/E_MTAB_5215_rnaseq/quantile_normalised", + "normalised/E_MTAB_5215_rnaseq/quantile_normalised/E_MTAB_5215_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/E_MTAB_5215_rnaseq/tpm", + "normalised/E_MTAB_5215_rnaseq/tpm/E_MTAB_5215_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", + "normalised/E_MTAB_552_rnaseq", + "normalised/E_MTAB_552_rnaseq/quantile_normalised", + "normalised/E_MTAB_552_rnaseq/quantile_normalised/E_MTAB_552_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/E_MTAB_552_rnaseq/tpm", + "normalised/E_MTAB_552_rnaseq/tpm/E_MTAB_552_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", + "normalised/E_MTAB_7711_rnaseq", + "normalised/E_MTAB_7711_rnaseq/quantile_normalised", + "normalised/E_MTAB_7711_rnaseq/quantile_normalised/E_MTAB_7711_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/E_MTAB_7711_rnaseq/tpm", + "normalised/E_MTAB_7711_rnaseq/tpm/E_MTAB_7711_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", + "pipeline_info", + "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", + "public_data", + "public_data/expression_atlas", + "public_data/expression_atlas/accessions", + "public_data/expression_atlas/accessions/accessions.txt", + "public_data/expression_atlas/accessions/selected_experiments.metadata.tsv", + "public_data/expression_atlas/accessions/species_experiments.metadata.tsv", + "public_data/expression_atlas/datasets", + "public_data/expression_atlas/datasets/E_GEOD_61690_rnaseq.design.csv", + "public_data/expression_atlas/datasets/E_GEOD_61690_rnaseq.rnaseq.raw.counts.csv", + "public_data/expression_atlas/datasets/E_GEOD_77826_rnaseq.design.csv", + "public_data/expression_atlas/datasets/E_GEOD_77826_rnaseq.rnaseq.raw.counts.csv", + "public_data/expression_atlas/datasets/E_MTAB_4252_rnaseq.design.csv", + "public_data/expression_atlas/datasets/E_MTAB_4252_rnaseq.rnaseq.raw.counts.csv", + "public_data/expression_atlas/datasets/E_MTAB_5038_rnaseq.design.csv", + "public_data/expression_atlas/datasets/E_MTAB_5038_rnaseq.rnaseq.raw.counts.csv", + "public_data/expression_atlas/datasets/E_MTAB_5215_rnaseq.design.csv", + "public_data/expression_atlas/datasets/E_MTAB_5215_rnaseq.rnaseq.raw.counts.csv", + "public_data/expression_atlas/datasets/E_MTAB_552_rnaseq.design.csv", + "public_data/expression_atlas/datasets/E_MTAB_552_rnaseq.rnaseq.raw.counts.csv", + "public_data/expression_atlas/datasets/E_MTAB_7711_rnaseq.design.csv", + "public_data/expression_atlas/datasets/E_MTAB_7711_rnaseq.rnaseq.raw.counts.csv", + "public_data/expression_atlas/datasets/failure_reason.txt", + "statistics", + "statistics/id_mapping_stats.csv", + "statistics/ratio_zeros.csv", + "statistics/skewness.csv", + "warnings" + ], + [ + "all_genes_summary.csv:md5,467ef2a492f57390a815072fd27d4955", + "most_stable_genes_summary.csv:md5,049a1fae30bafa22cd91fa906bb33164", + "most_stable_genes_transposed_counts_filtered.csv:md5,f5e71d68baa7976a9781a2315b2fe22f", + "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", + "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", + "all_genes_summary.csv:md5,467ef2a492f57390a815072fd27d4955", + "whole_design.csv:md5,cc24405dce8d22b93b9999a2287113ef", + "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", + "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", + "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", + "genes.py:md5,680cb5f4e107a3b091821917d72a555c", + "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", + "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", + "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", + "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", + "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", + "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", + "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", + "tables.py:md5,92e8cc03fd8c4192af9201a7ff3cd979", + "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", + "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", + "config.py:md5,b629adc64e497622f000945ee6e6aed2", + "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", + "style.py:md5,b9f1207f06464e43c05e6e3912f12731", + "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", + "eatlas_failure_reasons.csv:md5,2a8cd0ed795e82647d19c484a79acde6", + "renaming_failure_reasons.tsv:md5,3d83b7001eb0554a7e988f770f06d2b1", + "all_gene_ids.txt:md5,e9681582a09fe58f5258977db1d9da3f", + "global_gene_id_mapping.csv:md5,a86823539deb80c0aa44378d3078969d", + "global_gene_metadata.csv:md5,e33e0ed63a3dec26bc95fe422f02844c", + "gene_metadata.csv:md5,e33e0ed63a3dec26bc95fe422f02844c", + "mapped_gene_ids.csv:md5,a86823539deb80c0aa44378d3078969d", + "original_gene_ids.txt:md5,ce844044e326975477db11703390d406", + "E_GEOD_61690_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,dd3367748f4d9b8f92daf0d3dd2fb141", + "E_GEOD_77826_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,9b143e768316f992efe1762359bcb3a9", + "E_MTAB_5038_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,e5e5bf37be6c1689b9be6579d18e7eba", + "E_MTAB_5215_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,e4b3b8b84f5a2ea80d9efda6a8e5a271", + "E_MTAB_552_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,b243dfc62677733049d4c579f265d016", + "E_MTAB_7711_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,2e4aa7a16e060ee5dd4092d8419913cb", + "failure_reason.txt:md5,32e95a7ef5c9e4d026676aadcba8e8b8", + "whole_gene_id_mapping.csv:md5,a86823539deb80c0aa44378d3078969d", + "whole_gene_metadata.csv:md5,e33e0ed63a3dec26bc95fe422f02844c", + "whole_design.csv:md5,cc24405dce8d22b93b9999a2287113ef", + "E_GEOD_61690_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,b1f6d3392501f1670c0afed437c9d6c2", + "E_GEOD_77826_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,b736359148994197d258de62585fa6ff", + "E_MTAB_5038_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,856f32fa3c5a3a92578474de586e08e0", + "E_MTAB_5215_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,c235eed546c50a6b335e45bd238de940", + "E_MTAB_552_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,256158add9a0ee0cfcc800104dcaeeae", + "E_MTAB_7711_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,8731c38eeb16bbbb6fb87f5a80665efd", + "accessions.txt:md5,e38a0aaf5191ba5f94cb7a96b8d30aa7", + "selected_experiments.metadata.tsv:md5,15baa430176fcadf7bf03034fa9e95c6", + "species_experiments.metadata.tsv:md5,15baa430176fcadf7bf03034fa9e95c6", + "E_GEOD_61690_rnaseq.design.csv:md5,ba807caf74e0b55f5c3fe23810a89560", + "E_GEOD_61690_rnaseq.rnaseq.raw.counts.csv:md5,2e3f1a125b3d41d622e2d24447620eb3", + "E_GEOD_77826_rnaseq.design.csv:md5,5aa61df754aa9c6c107b247c642d2e53", + "E_GEOD_77826_rnaseq.rnaseq.raw.counts.csv:md5,85cea79c602a9924d5a4d6b597ef5530", + "E_MTAB_4252_rnaseq.design.csv:md5,5aef2d1f8b78b3e60225855a6aafe6ad", + "E_MTAB_4252_rnaseq.rnaseq.raw.counts.csv:md5,80d4fdb7f02fc7875827b61e104da56e", + "E_MTAB_5038_rnaseq.design.csv:md5,352ed3163d7deef2be35d899418d5ad4", + "E_MTAB_5038_rnaseq.rnaseq.raw.counts.csv:md5,b4acb3d7c39cdb2bd6cef6c9314c5b2a", + "E_MTAB_5215_rnaseq.design.csv:md5,2741dcd5b45bacce865db632f626a273", + "E_MTAB_5215_rnaseq.rnaseq.raw.counts.csv:md5,273704bdf762c342271b33958a84d1e7", + "E_MTAB_552_rnaseq.design.csv:md5,b81490696f638e90c1cf14236bb0c08c", + "E_MTAB_552_rnaseq.rnaseq.raw.counts.csv:md5,830f50b60b17b62f9ca2f6a163a2879f", + "E_MTAB_7711_rnaseq.design.csv:md5,3e7748b54a0c25c008d9bd2ddbf1bf00", + "E_MTAB_7711_rnaseq.rnaseq.raw.counts.csv:md5,3c02cf432c29d3751c978439539df388", + "failure_reason.txt:md5,bf97c58555bcb575f0e36df513e1e4c4", + "id_mapping_stats.csv:md5,61a42a2fe5ca0ace6ced97dfa9082e97", + "ratio_zeros.csv:md5,febc5ccc4635c814492b2234cbb167f1", + "skewness.csv:md5,0b7016ec048e578addf7bb669f405b67" + ] + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.2" + }, + "timestamp": "2025-12-15T16:37:09.430245577" }, "-profile test_skip_id_mapping": { "content": [ [ - "collect_statistics", - "collect_statistics/ratio_zeros.transposed.csv", - "collect_statistics/skewness.transposed.csv", - "compute_base_statistics", - "compute_base_statistics/stats_all_genes.csv", - "compute_base_statistics_for_microarray", - "compute_base_statistics_for_microarray/microarray.stats_all_genes.csv", - "compute_base_statistics_for_rnaseq", - "compute_base_statistics_for_rnaseq/rnaseq.stats_all_genes.csv", - "compute_dataset_statistics", - "compute_dataset_statistics/ratio_zeros.txt", - "compute_dataset_statistics/skewness.txt", - "compute_gene_transcript_lengths", - "compute_gene_transcript_lengths/gene_transcript_lengths.csv", - "compute_stability_scores", - "compute_stability_scores/stats_with_scores.csv", - "download_ensembl_annotation", - "download_ensembl_annotation/Solanum_tuberosum.SolTub_3.0.62.gff3.gz", "errors", - "get_candidate_genes", - "get_candidate_genes/candidate_counts.parquet", "idmapping", - "merge_all_counts", - "merge_all_counts/all_counts.parquet", - "merge_microarray_counts", - "merge_microarray_counts/all_counts.parquet", - "merge_rnaseq_counts", - "merge_rnaseq_counts/all_counts.parquet", "merged_datasets", "merged_datasets/whole_design.csv", "multiqc", @@ -476,64 +965,34 @@ "normalised/rnaseq.raw/quantile_normalised/rnaseq.raw.tpm.quant_norm.parquet", "normalised/rnaseq.raw/tpm", "normalised/rnaseq.raw/tpm/rnaseq.raw.tpm.csv", - "normfinder", - "normfinder/stability_values.normfinder.csv", "pipeline_info", - "pipeline_info/software_mqc_versions.yml", + "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", "statistics", "statistics/ratio_zeros.csv", "statistics/skewness.csv", "warnings" ], [ - "ratio_zeros.transposed.csv:md5,08ac547e8c31c2eefeead06c58f1fa3d", - "skewness.transposed.csv:md5,19618bb423fd67de0505f41710bff55f", - "stats_all_genes.csv:md5,c6b5b2687e436d44f1d2310e8a3f9ae5", - "microarray.stats_all_genes.csv:md5,42524c0c0c6da5a516759e4383b5d733", - "rnaseq.stats_all_genes.csv:md5,db8fb501793a807a66798c38b5415beb", - "ratio_zeros.txt:md5,97e541c27b33caea07d5d1632c2b69af", - "skewness.txt:md5,1b5f5d7422e74dccb09d7ec752da3b8f", - "gene_transcript_lengths.csv:md5,217aa7c1e227ce2f78a905138d8e5b39", - "stats_with_scores.csv:md5,6810d14306d7d6f8b8f25a20e20f9a24", - "Solanum_tuberosum.SolTub_3.0.62.gff3.gz:md5,cca99141f43d57d697f6df75de790e05", "whole_design.csv:md5,70d6c2673e619ca52d2774fb3e368382", "rnaseq.raw.tpm.csv:md5,3938c48a7d3e5b33de0d16db7e786c78", - "stability_values.normfinder.csv:md5,04e67e4538a9bc4d8c8a02ca573c2daf", "ratio_zeros.csv:md5,d206e45c16e6bd13de75ea6d20bbd30d", - "skewness.csv:md5,14e2a88e24c48522b03d6cbe8f276023" + "skewness.csv:md5,9917b39dfe5ee6e680fa1783f8a096c4" ] ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-12-04T10:19:20.611079049" + "timestamp": "2025-12-12T19:43:58.840498623" }, "-profile test": { "content": [ [ - "aggregate_results", - "aggregate_results/all_counts_filtered.parquet", - "aggregate_results/all_genes_summary.csv", - "aggregate_results/top_stable_genes_summary.csv", - "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", - "clean_gene_ids", - "clean_gene_ids/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.csv", - "clean_gene_ids/beta_vulgaris.rnaseq.raw.counts.cleaned.csv", - "collect_statistics", - "collect_statistics/ratio_zeros.transposed.csv", - "collect_statistics/skewness.transposed.csv", - "compute_base_statistics", - "compute_base_statistics/stats_all_genes.csv", - "compute_base_statistics_for_rnaseq", - "compute_base_statistics_for_rnaseq/rnaseq.stats_all_genes.csv", - "compute_dataset_statistics", - "compute_dataset_statistics/ratio_zeros.txt", - "compute_dataset_statistics/skewness.txt", - "compute_gene_transcript_lengths", - "compute_gene_transcript_lengths/gene_transcript_lengths.csv", - "compute_stability_scores", - "compute_stability_scores/stats_with_scores.csv", + "aggregated", + "aggregated/all_counts_filtered.parquet", + "aggregated/all_genes_summary.csv", + "aggregated/most_stable_genes_summary.csv", + "aggregated/most_stable_genes_transposed_counts_filtered.csv", "dash_app", "dash_app/app.py", "dash_app/assets", @@ -566,12 +1025,8 @@ "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", "dash_app/versions.yml", - "download_ensembl_annotation", - "download_ensembl_annotation/Beta_vulgaris.RefBeet-1.2.2.62.gff3.gz", "errors", "geo", - "get_candidate_genes", - "get_candidate_genes/candidate_counts.parquet", "idmapping", "idmapping/collected_gene_ids", "idmapping/collected_gene_ids/all_gene_ids.txt", @@ -586,10 +1041,6 @@ "idmapping/renamed/beta_vulgaris.rnaseq.raw.counts.cleaned.renamed.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", - "merge_all_counts", - "merge_all_counts/all_counts.parquet", - "merge_rnaseq_counts", - "merge_rnaseq_counts/all_counts.parquet", "merged_datasets", "merged_datasets/whole_design.csv", "multiqc", @@ -601,13 +1052,14 @@ "multiqc/multiqc_data/multiqc_data.json", "multiqc/multiqc_data/multiqc_eatlas_all_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", + "multiqc/multiqc_data/multiqc_expression_distributions_most_stable_genes.txt", "multiqc/multiqc_data/multiqc_gene_statistics.txt", "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_geo_rejected_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_geo_selected_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_geo_warning_reasons.txt", - "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", + "multiqc/multiqc_data/multiqc_id_mapping_stats.txt", + "multiqc/multiqc_data/multiqc_ranked_most_stable_genes_summary.txt", "multiqc/multiqc_data/multiqc_ratio_zeros.txt", "multiqc/multiqc_data/multiqc_skewness.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", @@ -616,37 +1068,43 @@ "multiqc/multiqc_plots/pdf", "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", + "multiqc/multiqc_plots/pdf/expression_distributions_most_stable_genes.pdf", "multiqc/multiqc_plots/pdf/gene_statistics.pdf", "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/geo_rejected_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/geo_selected_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/geo_warning_reasons.pdf", - "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", + "multiqc/multiqc_plots/pdf/id_mapping_stats-cnt.pdf", + "multiqc/multiqc_plots/pdf/id_mapping_stats-pct.pdf", + "multiqc/multiqc_plots/pdf/ranked_most_stable_genes_summary.pdf", "multiqc/multiqc_plots/pdf/ratio_zeros.pdf", "multiqc/multiqc_plots/pdf/skewness.pdf", "multiqc/multiqc_plots/png", "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", - "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", + "multiqc/multiqc_plots/png/expression_distributions_most_stable_genes.png", "multiqc/multiqc_plots/png/gene_statistics.png", "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", "multiqc/multiqc_plots/png/geo_rejected_experiments_metadata.png", "multiqc/multiqc_plots/png/geo_selected_experiments_metadata.png", "multiqc/multiqc_plots/png/geo_warning_reasons.png", - "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", + "multiqc/multiqc_plots/png/id_mapping_stats-cnt.png", + "multiqc/multiqc_plots/png/id_mapping_stats-pct.png", + "multiqc/multiqc_plots/png/ranked_most_stable_genes_summary.png", "multiqc/multiqc_plots/png/ratio_zeros.png", "multiqc/multiqc_plots/png/skewness.png", "multiqc/multiqc_plots/svg", "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", + "multiqc/multiqc_plots/svg/expression_distributions_most_stable_genes.svg", "multiqc/multiqc_plots/svg/gene_statistics.svg", "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", "multiqc/multiqc_plots/svg/geo_rejected_experiments_metadata.svg", "multiqc/multiqc_plots/svg/geo_selected_experiments_metadata.svg", "multiqc/multiqc_plots/svg/geo_warning_reasons.svg", - "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", + "multiqc/multiqc_plots/svg/id_mapping_stats-cnt.svg", + "multiqc/multiqc_plots/svg/id_mapping_stats-pct.svg", + "multiqc/multiqc_plots/svg/ranked_most_stable_genes_summary.svg", "multiqc/multiqc_plots/svg/ratio_zeros.svg", "multiqc/multiqc_plots/svg/skewness.svg", "multiqc/multiqc_report.html", @@ -662,10 +1120,8 @@ "normalised/beta_vulgaris.rnaseq.raw.counts/quantile_normalised/beta_vulgaris.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", "normalised/beta_vulgaris.rnaseq.raw.counts/tpm", "normalised/beta_vulgaris.rnaseq.raw.counts/tpm/beta_vulgaris.rnaseq.raw.counts.cleaned.renamed.tpm.csv", - "normfinder", - "normfinder/stability_values.normfinder.csv", "pipeline_info", - "pipeline_info/software_mqc_versions.yml", + "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", "public_data", "public_data/expression_atlas", "public_data/expression_atlas/accessions", @@ -692,18 +1148,8 @@ ], [ "all_genes_summary.csv:md5,ec52af6957845539143919c0e6eec89c", - "top_stable_genes_summary.csv:md5,81d590008d5582658d307bf8c101e60a", - "top_stable_genes_transposed_counts_filtered.csv:md5,8069bd4eb5749c206912a57e35cf7357", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.csv:md5,c7f0662425535e1c04bbc58b22ba74eb", - "beta_vulgaris.rnaseq.raw.counts.cleaned.csv:md5,7fe33ef9f337b2bb66392ca78fd8d343", - "ratio_zeros.transposed.csv:md5,1cbb8d2b3f8ff38a523d3d80471dba3e", - "skewness.transposed.csv:md5,1851259185418be9d83a72bef48d7075", - "stats_all_genes.csv:md5,c9441dd7f473f50b37d7eb64ae2b6d52", - "rnaseq.stats_all_genes.csv:md5,4a46edac1a4158fb861e54bf3b34c5cd", - "ratio_zeros.txt:md5,ae64e9788a4e48e276ecdea3b33abb3f", - "skewness.txt:md5,ad6408796ac8bc51f29018c33f53cb55", - "gene_transcript_lengths.csv:md5,458c7dfd3598bdcbcb6ceb76ccba189f", - "stats_with_scores.csv:md5,6e457b48ca46c4a4965e108403346e94", + "most_stable_genes_summary.csv:md5,81d590008d5582658d307bf8c101e60a", + "most_stable_genes_transposed_counts_filtered.csv:md5,9b52b81241cb4f57be781e29afe52cdc", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", "all_genes_summary.csv:md5,ec52af6957845539143919c0e6eec89c", @@ -726,13 +1172,12 @@ "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", "style.py:md5,b9f1207f06464e43c05e6e3912f12731", "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "Beta_vulgaris.RefBeet-1.2.2.62.gff3.gz:md5,6f2c45809441c8776e6578000db2b0e4", - "all_gene_ids.txt:md5,be909a6a1fbbf2c66b3f646b3b24975a", - "global_gene_id_mapping.csv:md5,c664fae78b7433ded148bd94bcd23c67", - "global_gene_metadata.csv:md5,68d3a0bf612bef3f20d370012400dca5", - "gene_metadata.csv:md5,be6187aa4ae724f7e37e5a07099c40af", - "mapped_gene_ids.csv:md5,f26e6d00ff48a07fe28c056bbd6cde8c", - "original_gene_ids.txt:md5,8b21a07b1b6ac1f3d5aa6e2d40b28fc0", + "all_gene_ids.txt:md5,ba79f5609df755c0f75de41357319c84", + "global_gene_id_mapping.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", + "global_gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", + "gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", + "mapped_gene_ids.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", + "original_gene_ids.txt:md5,60a0406c1d56424cfc394c438de50c99", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,ed1600e42df05fc885a16a66b77324a1", "beta_vulgaris.rnaseq.raw.counts.cleaned.renamed.csv:md5,d90b7475356d812ed289bc9fb3cb1acd", "whole_gene_id_mapping.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", @@ -740,35 +1185,44 @@ "whole_design.csv:md5,3c1e14c9bd7ad250326b070a0dd4d81f", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,b407990bb10106de943f9e93cda6323e", "beta_vulgaris.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,2c326f3e419341f955bb757fc8bf4357", - "stability_values.normfinder.csv:md5,c762f74840a6e604d43c8f727a5826f5", "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", "accessions.txt:md5,63a651d9df354aef24400cebe56dd5ec", - "geo_all_datasets.metadata.tsv:md5,06a371a9a891d50d40c94239f53db000", + "geo_all_datasets.metadata.tsv:md5,465212e30807744519ad3999db01a318", "geo_rejected_datasets.metadata.tsv:md5,0a66c9d519b4590e48b04e4c37d66416", - "geo_selected_datasets.metadata.tsv:md5,df6a09c2511ebb84c192b9eac3b02e02", + "geo_selected_datasets.metadata.tsv:md5,dd37fb452bd34dda97ac5b3c33516519", "warning_reason.txt:md5,603e30b732b7a6b501b59adf9d0e8837", - "id_mapping_stats.csv:md5,ba2014297db7749253dae3922859ae0a", + "id_mapping_stats.csv:md5,7c20b1e561989fcd2ce038c4a061caa5", "ratio_zeros.csv:md5,9794647ae1d7c87ec212c0c12b658d4e", - "skewness.csv:md5,386dcdb8064e85a1388aad5b72244692", - "geo_warning_reasons.csv:md5,b44f494b756cc0297f1d7b234bea1e13" + "skewness.csv:md5,7e1ecb86c9c51394a0dacfdaca05899b", + "geo_warning_reasons.csv:md5,0a77f9268abb1084fde8cb4c5cc96eca" ] ], "meta": { - "nf-test": "0.9.2", + "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-04T11:37:43.741998193" + "timestamp": "2025-12-15T16:44:28.441072301" }, "-profile test_accessions_only": { "content": [ - null, + { + "EXPRESSION_ATLAS": { + "nltk": "3.9.1", + "pandas": "2.3.0", + "python": "3.13.5", + "pyyaml": "6.0.2", + "requests": "2.32.4" + }, + "Workflow": { + "nf-core/stableexpression": "v1.0dev" + } + }, [ "errors", - "geo", "multiqc", "multiqc/multiqc_data", "multiqc/multiqc_data/llms-full.txt", @@ -778,115 +1232,202 @@ "multiqc/multiqc_data/multiqc_data.json", "multiqc/multiqc_data/multiqc_eatlas_all_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_rejected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_selected_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", "multiqc/multiqc_plots", "multiqc/multiqc_plots/pdf", "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_rejected_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_selected_experiments_metadata.pdf", "multiqc/multiqc_plots/png", "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_rejected_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_selected_experiments_metadata.png", "multiqc/multiqc_plots/svg", "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_rejected_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_selected_experiments_metadata.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", "pipeline_info", - "pipeline_info/software_mqc_versions.yml", + "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", "public_data", "public_data/expression_atlas", "public_data/expression_atlas/accessions", "public_data/expression_atlas/accessions/accessions.txt", "public_data/expression_atlas/accessions/selected_experiments.metadata.tsv", "public_data/expression_atlas/accessions/species_experiments.metadata.tsv", - "public_data/geo", - "public_data/geo/accessions", - "public_data/geo/accessions/accessions.txt", - "public_data/geo/accessions/geo_all_datasets.metadata.tsv", - "public_data/geo/accessions/geo_rejected_datasets.metadata.tsv", - "public_data/geo/accessions/geo_selected_datasets.metadata.tsv", "statistics", "warnings" ], [ "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", - "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", - "accessions.txt:md5,a850f625a78be7b4b10ce08a5b638e23", - "geo_all_datasets.metadata.tsv:md5,7fd4e7b26f1348f86e97aa16f3fd40c0", - "geo_rejected_datasets.metadata.tsv:md5,4b4908f7ae40b84a1ac1bd5addfc8dd2", - "geo_selected_datasets.metadata.tsv:md5,62bdb8eb0e87155d149020e65d110e80" + "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2" ] ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-12-04T10:15:09.663028204" + "timestamp": "2025-12-11T14:37:17.749448911" }, "-profile test_one_accession_low_gene_count": { "content": [ - null, - [ - "compute_gene_transcript_lengths", - "compute_gene_transcript_lengths/gene_transcript_lengths.csv", - "download_ensembl_annotation", - "download_ensembl_annotation/Arabidopsis_thaliana.TAIR10.62.gff3.gz", - "errors", - "idmapping", - "merged_datasets", - "multiqc", - "multiqc/multiqc_data", - "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_software_versions.txt", - "multiqc/multiqc_data/multiqc_sources.txt", - "multiqc/multiqc_report.html", - "multiqc/versions.yml", - "pipeline_info", - "pipeline_info/software_mqc_versions.yml", - "statistics", - "warnings" - ], - [ - "gene_transcript_lengths.csv:md5,06b4612031f4f300a6d67f36e7625492", - "Arabidopsis_thaliana.TAIR10.62.gff3.gz:md5,b02566c301d47461db70747b3adaa6ce" - ] - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-12-03T18:39:25.158526321" - }, - "-profile test_no_dataset_found": { - "content": [ - null, + { + "AGGREGATE_RESULTS": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "CLEAN_GENE_IDS": { + "pandas": "2.3.3", + "polars": "1.35.2", + "python": "3.14.0" + }, + "COLLECT_GENE_IDS": { + "pandas": "2.3.3", + "python": "3.14.0", + "tqdm": "4.67.1" + }, + "COLLECT_STATISTICS": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "COMPUTE_BASE_STATISTICS": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_BASE_STATISTICS_FOR_RNASEQ": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_DATASET_STATISTICS": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "COMPUTE_GENE_TRANSCRIPT_LENGTHS": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "COMPUTE_STABILITY_SCORES": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_TPM": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "DASH_APP": { + "python": "3.13.8", + "dash": "3.2.0", + "dash-extensions": "2.0.4", + "dash-mantine-components": "2.3.0", + "dash-ag-grid": "32.3.2", + "polars": "1.35.0", + "pandas": "2.3.3", + "pyarrow": "22.0.0", + "scipy": "1.16.3" + }, + "DOWNLOAD_ENSEMBL_ANNOTATION": { + "bs4": "4.14.2", + "pandas": "2.3.3", + "python": "3.14.0", + "requests": "2.32.5", + "tqdm": "4.67.1" + }, + "EXPRESSION_ATLAS": { + "ExpressionAtlas": "1.30.0", + "R": "4.3.3 (2024-02-29)" + }, + "GET_CANDIDATE_GENES": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "GPROFILER_IDMAPPING": { + "pandas": "2.3.1", + "python": "3.13.5", + "requests": "2.32.4" + }, + "MERGE_ALL_COUNTS": { + "polars": "1.34.0", + "python": "3.14.0", + "tqdm": "4.67.1" + }, + "MERGE_RNASEQ_COUNTS": { + "polars": "1.34.0", + "python": "3.14.0", + "tqdm": "4.67.1" + }, + "NORMFINDER": { + "polars": "1.33.1", + "python": "3.13.7" + }, + "QUANTILE_NORMALISATION": { + "pandas": "2.2.3", + "pyarrow": "19.0.0", + "python": "3.12.8", + "scikit-learn": "1.6.1" + }, + "RENAME_GENE_IDS": { + "pandas": "2.3.3", + "polars": "1.35.2", + "python": "3.14.0" + }, + "Workflow": { + "nf-core/stableexpression": "v1.0dev" + } + }, [ - "compute_gene_transcript_lengths", - "compute_gene_transcript_lengths/gene_transcript_lengths.csv", - "download_ensembl_annotation", - "download_ensembl_annotation/Marmota_marmota_marmota.marMar2.1.115.gff3.gz", + "aggregated", + "aggregated/all_counts_filtered.parquet", + "aggregated/all_genes_summary.csv", + "aggregated/most_stable_genes_summary.csv", + "aggregated/most_stable_genes_transposed_counts_filtered.csv", + "dash_app", + "dash_app/app.py", + "dash_app/assets", + "dash_app/assets/style.css", + "dash_app/data", + "dash_app/data/all_counts.parquet", + "dash_app/data/all_genes_summary.csv", + "dash_app/data/whole_design.csv", + "dash_app/environment.yml", + "dash_app/file_system_backend", + "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", + "dash_app/src", + "dash_app/src/callbacks", + "dash_app/src/callbacks/common.py", + "dash_app/src/callbacks/genes.py", + "dash_app/src/callbacks/samples.py", + "dash_app/src/components", + "dash_app/src/components/graphs.py", + "dash_app/src/components/icons.py", + "dash_app/src/components/right_sidebar.py", + "dash_app/src/components/settings", + "dash_app/src/components/settings/genes.py", + "dash_app/src/components/settings/samples.py", + "dash_app/src/components/stores.py", + "dash_app/src/components/tables.py", + "dash_app/src/components/tooltips.py", + "dash_app/src/components/top.py", + "dash_app/src/utils", + "dash_app/src/utils/config.py", + "dash_app/src/utils/data_management.py", + "dash_app/src/utils/style.py", + "dash_app/versions.yml", "errors", - "geo", "idmapping", + "idmapping/collected_gene_ids", + "idmapping/collected_gene_ids/all_gene_ids.txt", + "idmapping/global_gene_id_mapping.csv", + "idmapping/global_gene_metadata.csv", + "idmapping/gprofiler", + "idmapping/gprofiler/gene_metadata.csv", + "idmapping/gprofiler/mapped_gene_ids.csv", + "idmapping/original_gene_ids.txt", + "idmapping/renamed", + "idmapping/renamed/E_GEOD_51720_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", + "idmapping/whole_gene_id_mapping.csv", + "idmapping/whole_gene_metadata.csv", "merged_datasets", + "merged_datasets/whole_design.csv", "multiqc", "multiqc/multiqc_data", "multiqc/multiqc_data/llms-full.txt", @@ -894,43 +1435,128 @@ "multiqc/multiqc_data/multiqc.parquet", "multiqc/multiqc_data/multiqc_citations.txt", "multiqc/multiqc_data/multiqc_data.json", + "multiqc/multiqc_data/multiqc_expression_distributions_most_stable_genes.txt", + "multiqc/multiqc_data/multiqc_gene_statistics.txt", + "multiqc/multiqc_data/multiqc_id_mapping_stats.txt", + "multiqc/multiqc_data/multiqc_ranked_most_stable_genes_summary.txt", + "multiqc/multiqc_data/multiqc_ratio_zeros.txt", + "multiqc/multiqc_data/multiqc_skewness.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_plots", + "multiqc/multiqc_plots/pdf", + "multiqc/multiqc_plots/pdf/expression_distributions_most_stable_genes.pdf", + "multiqc/multiqc_plots/pdf/gene_statistics.pdf", + "multiqc/multiqc_plots/pdf/id_mapping_stats-cnt.pdf", + "multiqc/multiqc_plots/pdf/id_mapping_stats-pct.pdf", + "multiqc/multiqc_plots/pdf/ranked_most_stable_genes_summary.pdf", + "multiqc/multiqc_plots/pdf/ratio_zeros.pdf", + "multiqc/multiqc_plots/pdf/skewness.pdf", + "multiqc/multiqc_plots/png", + "multiqc/multiqc_plots/png/expression_distributions_most_stable_genes.png", + "multiqc/multiqc_plots/png/gene_statistics.png", + "multiqc/multiqc_plots/png/id_mapping_stats-cnt.png", + "multiqc/multiqc_plots/png/id_mapping_stats-pct.png", + "multiqc/multiqc_plots/png/ranked_most_stable_genes_summary.png", + "multiqc/multiqc_plots/png/ratio_zeros.png", + "multiqc/multiqc_plots/png/skewness.png", + "multiqc/multiqc_plots/svg", + "multiqc/multiqc_plots/svg/expression_distributions_most_stable_genes.svg", + "multiqc/multiqc_plots/svg/gene_statistics.svg", + "multiqc/multiqc_plots/svg/id_mapping_stats-cnt.svg", + "multiqc/multiqc_plots/svg/id_mapping_stats-pct.svg", + "multiqc/multiqc_plots/svg/ranked_most_stable_genes_summary.svg", + "multiqc/multiqc_plots/svg/ratio_zeros.svg", + "multiqc/multiqc_plots/svg/skewness.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", + "normalised", + "normalised/E_GEOD_51720_rnaseq", + "normalised/E_GEOD_51720_rnaseq/quantile_normalised", + "normalised/E_GEOD_51720_rnaseq/quantile_normalised/E_GEOD_51720_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/E_GEOD_51720_rnaseq/tpm", + "normalised/E_GEOD_51720_rnaseq/tpm/E_GEOD_51720_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", "pipeline_info", - "pipeline_info/software_mqc_versions.yml", + "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", "public_data", "public_data/expression_atlas", - "public_data/expression_atlas/accessions", - "public_data/expression_atlas/accessions/accessions.txt", - "public_data/expression_atlas/accessions/species_experiments.metadata.tsv", - "public_data/geo", - "public_data/geo/accessions", - "public_data/geo/accessions/accessions.txt", + "public_data/expression_atlas/datasets", + "public_data/expression_atlas/datasets/E_GEOD_51720_rnaseq.design.csv", + "public_data/expression_atlas/datasets/E_GEOD_51720_rnaseq.rnaseq.raw.counts.csv", "statistics", + "statistics/id_mapping_stats.csv", + "statistics/ratio_zeros.csv", + "statistics/skewness.csv", "warnings" ], [ - "gene_transcript_lengths.csv:md5,d03318b6a34355e8897e9d43f02c8deb", - "Marmota_marmota_marmota.marMar2.1.115.gff3.gz:md5,f67b6ec1b7de4c7fa606183982a3704b", - "accessions.txt:md5,d41d8cd98f00b204e9800998ecf8427e", - "species_experiments.metadata.tsv:md5,68b329da9893e34099c7d8ad5cb9c940", - "accessions.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + "all_genes_summary.csv:md5,8308c2f930f305e3db913d992a6acf36", + "most_stable_genes_summary.csv:md5,bd5c71953b259d05d024f68eb4b62942", + "most_stable_genes_transposed_counts_filtered.csv:md5,d6a99e3a8a422af722dea852d84bdc94", + "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", + "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", + "all_genes_summary.csv:md5,8308c2f930f305e3db913d992a6acf36", + "whole_design.csv:md5,d3aa542c4ad07d0051a84482fe6cd81c", + "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", + "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", + "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", + "genes.py:md5,680cb5f4e107a3b091821917d72a555c", + "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", + "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", + "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", + "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", + "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", + "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", + "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", + "tables.py:md5,92e8cc03fd8c4192af9201a7ff3cd979", + "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", + "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", + "config.py:md5,b629adc64e497622f000945ee6e6aed2", + "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", + "style.py:md5,b9f1207f06464e43c05e6e3912f12731", + "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", + "all_gene_ids.txt:md5,9c463e6c1754d4f4c7ea684578aa6849", + "global_gene_id_mapping.csv:md5,42491ef436cce231258c0358e1af5745", + "global_gene_metadata.csv:md5,b35e20500269d4e6787ef1a3468f16bc", + "gene_metadata.csv:md5,bb7db05964749de50bee10afdded87b0", + "mapped_gene_ids.csv:md5,42491ef436cce231258c0358e1af5745", + "original_gene_ids.txt:md5,31c068e2e8b054ea4d696dec754caed4", + "E_GEOD_51720_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,6f3c401caaf89e534cc13db8976b296c", + "whole_gene_id_mapping.csv:md5,42491ef436cce231258c0358e1af5745", + "whole_gene_metadata.csv:md5,b35e20500269d4e6787ef1a3468f16bc", + "whole_design.csv:md5,d3aa542c4ad07d0051a84482fe6cd81c", + "E_GEOD_51720_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,281e8c83c27ad9d6c732a7c013e627cf", + "E_GEOD_51720_rnaseq.design.csv:md5,80805afb29837b6fbb73a6aa6f3a461b", + "E_GEOD_51720_rnaseq.rnaseq.raw.counts.csv:md5,07cd448196fc2fea4663bd9705da2b98", + "id_mapping_stats.csv:md5,c5d20cc6298f0863617b1139f41a2da4", + "ratio_zeros.csv:md5,2cff16880a965af8acc438786b9fb110", + "skewness.csv:md5,bdbc6ee3d4c19b907a71d02ad9cac149" ] ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-12-03T18:43:26.049573567" + "timestamp": "2025-12-15T16:48:56.125523436" }, "-profile test_download_only": { "content": [ - null, + { + "EXPRESSION_ATLAS": { + "ExpressionAtlas": "1.30.0", + "R": "4.3.3 (2024-02-29)", + "nltk": "3.9.1", + "pandas": "2.3.0", + "python": "3.13.5", + "pyyaml": "6.0.2", + "requests": "2.32.4" + }, + "Workflow": { + "nf-core/stableexpression": "v1.0dev" + } + }, [ "errors", - "geo", "multiqc", "multiqc/multiqc_data", "multiqc/multiqc_data/llms-full.txt", @@ -940,38 +1566,22 @@ "multiqc/multiqc_data/multiqc_data.json", "multiqc/multiqc_data/multiqc_eatlas_all_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_rejected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_selected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_warning_reasons.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", "multiqc/multiqc_plots", "multiqc/multiqc_plots/pdf", "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_rejected_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_selected_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_warning_reasons.pdf", "multiqc/multiqc_plots/png", "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_rejected_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_selected_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_warning_reasons.png", "multiqc/multiqc_plots/svg", "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_rejected_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_selected_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_warning_reasons.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", "pipeline_info", - "pipeline_info/software_mqc_versions.yml", + "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", "public_data", "public_data/expression_atlas", "public_data/expression_atlas/accessions", @@ -981,69 +1591,31 @@ "public_data/expression_atlas/datasets", "public_data/expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", "public_data/expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", - "public_data/geo", - "public_data/geo/accessions", - "public_data/geo/accessions/accessions.txt", - "public_data/geo/accessions/geo_all_datasets.metadata.tsv", - "public_data/geo/accessions/geo_rejected_datasets.metadata.tsv", - "public_data/geo/accessions/geo_selected_datasets.metadata.tsv", - "public_data/geo/datasets", - "public_data/geo/datasets/GSE55951_GPL18429.microarray.normalised.counts.csv", - "public_data/geo/datasets/GSE55951_GPL18429.microarray.normalised.design.csv", - "public_data/geo/datasets/warning_reason.txt", "statistics", - "warnings", - "warnings/geo_warning_reasons.csv" + "warnings" ], [ "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", - "accessions.txt:md5,a850f625a78be7b4b10ce08a5b638e23", - "geo_all_datasets.metadata.tsv:md5,7fd4e7b26f1348f86e97aa16f3fd40c0", - "geo_rejected_datasets.metadata.tsv:md5,4b4908f7ae40b84a1ac1bd5addfc8dd2", - "geo_selected_datasets.metadata.tsv:md5,62bdb8eb0e87155d149020e65d110e80", - "GSE55951_GPL18429.microarray.normalised.counts.csv:md5,18fd2d728ad2ec5cb78f994f73375144", - "GSE55951_GPL18429.microarray.normalised.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d", - "warning_reason.txt:md5,603e30b732b7a6b501b59adf9d0e8837", - "geo_warning_reasons.csv:md5,e08541568733e7eac853f73480679e15" + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1" ] ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-12-04T10:16:23.021095391" + "timestamp": "2025-12-11T10:42:14.842517715" }, "-profile test_gprofiler_target_database_entrez": { "content": [ [ - "aggregate_results", - "aggregate_results/all_counts_filtered.parquet", - "aggregate_results/all_genes_summary.csv", - "aggregate_results/top_stable_genes_summary.csv", - "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", - "clean_gene_ids", - "clean_gene_ids/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.csv", - "clean_gene_ids/GSE55951_GPL18429.microarray.normalised.counts.cleaned.csv", - "collect_statistics", - "collect_statistics/ratio_zeros.transposed.csv", - "collect_statistics/skewness.transposed.csv", - "compute_base_statistics", - "compute_base_statistics/stats_all_genes.csv", - "compute_base_statistics_for_microarray", - "compute_base_statistics_for_microarray/microarray.stats_all_genes.csv", - "compute_base_statistics_for_rnaseq", - "compute_base_statistics_for_rnaseq/rnaseq.stats_all_genes.csv", - "compute_dataset_statistics", - "compute_dataset_statistics/ratio_zeros.txt", - "compute_dataset_statistics/skewness.txt", - "compute_gene_transcript_lengths", - "compute_gene_transcript_lengths/gene_transcript_lengths.csv", - "compute_stability_scores", - "compute_stability_scores/stats_with_scores.csv", + "aggregated", + "aggregated/all_counts_filtered.parquet", + "aggregated/all_genes_summary.csv", + "aggregated/most_stable_genes_summary.csv", + "aggregated/most_stable_genes_transposed_counts_filtered.csv", "dash_app", "dash_app/app.py", "dash_app/assets", @@ -1076,12 +1648,7 @@ "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", "dash_app/versions.yml", - "download_ensembl_annotation", - "download_ensembl_annotation/Beta_vulgaris.RefBeet-1.2.2.62.gff3.gz", "errors", - "geo", - "get_candidate_genes", - "get_candidate_genes/candidate_counts.parquet", "idmapping", "idmapping/collected_gene_ids", "idmapping/collected_gene_ids/all_gene_ids.txt", @@ -1093,16 +1660,8 @@ "idmapping/original_gene_ids.txt", "idmapping/renamed", "idmapping/renamed/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", - "idmapping/renamed/GSE55951_GPL18429.microarray.normalised.counts.cleaned.renamed.csv", - "idmapping/renamed/warning_reason.txt", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", - "merge_all_counts", - "merge_all_counts/all_counts.parquet", - "merge_microarray_counts", - "merge_microarray_counts/all_counts.parquet", - "merge_rnaseq_counts", - "merge_rnaseq_counts/all_counts.parquet", "merged_datasets", "merged_datasets/whole_design.csv", "multiqc", @@ -1114,15 +1673,11 @@ "multiqc/multiqc_data/multiqc_data.json", "multiqc/multiqc_data/multiqc_eatlas_all_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", + "multiqc/multiqc_data/multiqc_expression_distributions_most_stable_genes.txt", "multiqc/multiqc_data/multiqc_gene_statistics.txt", - "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_rejected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_selected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_warning_reasons.txt", - "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", + "multiqc/multiqc_data/multiqc_id_mapping_stats.txt", + "multiqc/multiqc_data/multiqc_ranked_most_stable_genes_summary.txt", "multiqc/multiqc_data/multiqc_ratio_zeros.txt", - "multiqc/multiqc_data/multiqc_renaming_warning_reasons.txt", "multiqc/multiqc_data/multiqc_skewness.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", @@ -1130,41 +1685,32 @@ "multiqc/multiqc_plots/pdf", "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", + "multiqc/multiqc_plots/pdf/expression_distributions_most_stable_genes.pdf", "multiqc/multiqc_plots/pdf/gene_statistics.pdf", - "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_rejected_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_selected_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_warning_reasons.pdf", - "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", + "multiqc/multiqc_plots/pdf/id_mapping_stats-cnt.pdf", + "multiqc/multiqc_plots/pdf/id_mapping_stats-pct.pdf", + "multiqc/multiqc_plots/pdf/ranked_most_stable_genes_summary.pdf", "multiqc/multiqc_plots/pdf/ratio_zeros.pdf", - "multiqc/multiqc_plots/pdf/renaming_warning_reasons.pdf", "multiqc/multiqc_plots/pdf/skewness.pdf", "multiqc/multiqc_plots/png", "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", - "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", + "multiqc/multiqc_plots/png/expression_distributions_most_stable_genes.png", "multiqc/multiqc_plots/png/gene_statistics.png", - "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_rejected_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_selected_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_warning_reasons.png", - "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", + "multiqc/multiqc_plots/png/id_mapping_stats-cnt.png", + "multiqc/multiqc_plots/png/id_mapping_stats-pct.png", + "multiqc/multiqc_plots/png/ranked_most_stable_genes_summary.png", "multiqc/multiqc_plots/png/ratio_zeros.png", - "multiqc/multiqc_plots/png/renaming_warning_reasons.png", "multiqc/multiqc_plots/png/skewness.png", "multiqc/multiqc_plots/svg", "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", + "multiqc/multiqc_plots/svg/expression_distributions_most_stable_genes.svg", "multiqc/multiqc_plots/svg/gene_statistics.svg", - "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_rejected_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_selected_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_warning_reasons.svg", - "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", + "multiqc/multiqc_plots/svg/id_mapping_stats-cnt.svg", + "multiqc/multiqc_plots/svg/id_mapping_stats-pct.svg", + "multiqc/multiqc_plots/svg/ranked_most_stable_genes_summary.svg", "multiqc/multiqc_plots/svg/ratio_zeros.svg", - "multiqc/multiqc_plots/svg/renaming_warning_reasons.svg", "multiqc/multiqc_plots/svg/skewness.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", @@ -1174,13 +1720,8 @@ "normalised/E_MTAB_8187_rnaseq/quantile_normalised/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", "normalised/E_MTAB_8187_rnaseq/tpm", "normalised/E_MTAB_8187_rnaseq/tpm/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", - "normalised/GSE55951_GPL18429", - "normalised/GSE55951_GPL18429/quantile_normalised", - "normalised/GSE55951_GPL18429/quantile_normalised/GSE55951_GPL18429.microarray.normalised.counts.cleaned.renamed.quant_norm.parquet", - "normfinder", - "normfinder/stability_values.normfinder.csv", "pipeline_info", - "pipeline_info/software_mqc_versions.yml", + "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", "public_data", "public_data/expression_atlas", "public_data/expression_atlas/accessions", @@ -1190,43 +1731,20 @@ "public_data/expression_atlas/datasets", "public_data/expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", "public_data/expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", - "public_data/geo", - "public_data/geo/accessions", - "public_data/geo/accessions/accessions.txt", - "public_data/geo/accessions/geo_all_datasets.metadata.tsv", - "public_data/geo/accessions/geo_rejected_datasets.metadata.tsv", - "public_data/geo/accessions/geo_selected_datasets.metadata.tsv", - "public_data/geo/datasets", - "public_data/geo/datasets/GSE55951_GPL18429.microarray.normalised.counts.csv", - "public_data/geo/datasets/GSE55951_GPL18429.microarray.normalised.design.csv", - "public_data/geo/datasets/warning_reason.txt", "statistics", "statistics/id_mapping_stats.csv", "statistics/ratio_zeros.csv", "statistics/skewness.csv", - "warnings", - "warnings/geo_warning_reasons.csv", - "warnings/renaming_warning_reasons.tsv" + "warnings" ], [ - "all_genes_summary.csv:md5,534ab07b53769ccac9ef1e6e11208930", - "top_stable_genes_summary.csv:md5,d479f790ffce5fcf655075848b942469", - "top_stable_genes_transposed_counts_filtered.csv:md5,a7a651bac95ed27c24e29175312cb507", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.csv:md5,c7f0662425535e1c04bbc58b22ba74eb", - "GSE55951_GPL18429.microarray.normalised.counts.cleaned.csv:md5,5d3bbad7d8b0bf824f883d2e84a229b3", - "ratio_zeros.transposed.csv:md5,4d9cccc1eb3f96b5b8489df2f2adf4d1", - "skewness.transposed.csv:md5,df6ef6363c95095e1dffae4b83358e8f", - "stats_all_genes.csv:md5,de1075167d01aa5e411185662157b70b", - "microarray.stats_all_genes.csv:md5,e686dce74f7006c5af95e5fb595c0902", - "rnaseq.stats_all_genes.csv:md5,db157ff5e8c39681d82b9c75a36d3c75", - "ratio_zeros.txt:md5,ae64e9788a4e48e276ecdea3b33abb3f", - "skewness.txt:md5,ad6408796ac8bc51f29018c33f53cb55", - "gene_transcript_lengths.csv:md5,458c7dfd3598bdcbcb6ceb76ccba189f", - "stats_with_scores.csv:md5,6b8eefabd6992ed69d1b633e9f64c7b8", + "all_genes_summary.csv:md5,32be3aeed5cd2715847ccc2fd45c09a0", + "most_stable_genes_summary.csv:md5,2e34161e2beb2f1e0921dbf6c0ce55ba", + "most_stable_genes_transposed_counts_filtered.csv:md5,5083c04020d1c504c86689e14f731ef7", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_genes_summary.csv:md5,534ab07b53769ccac9ef1e6e11208930", - "whole_design.csv:md5,f2723ea20ce675a7027a4b4285df7a96", + "all_genes_summary.csv:md5,32be3aeed5cd2715847ccc2fd45c09a0", + "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", @@ -1245,227 +1763,169 @@ "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", "style.py:md5,b9f1207f06464e43c05e6e3912f12731", "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "Beta_vulgaris.RefBeet-1.2.2.62.gff3.gz:md5,6f2c45809441c8776e6578000db2b0e4", - "all_gene_ids.txt:md5,bfbaf9423c4b18047a2cc43bc8a81436", - "global_gene_id_mapping.csv:md5,f07bb1847f253d252208e1a0c8dfa372", - "global_gene_metadata.csv:md5,68d3a0bf612bef3f20d370012400dca5", - "gene_metadata.csv:md5,995a94e8b5b5fe870a1e8297f281d02f", - "mapped_gene_ids.csv:md5,03cff6ad2356752016c7467008f22cca", - "original_gene_ids.txt:md5,b1459baf97ad5857115c5b9dced18417", + "all_gene_ids.txt:md5,ba79f5609df755c0f75de41357319c84", + "global_gene_id_mapping.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", + "global_gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", + "gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", + "mapped_gene_ids.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", + "original_gene_ids.txt:md5,60a0406c1d56424cfc394c438de50c99", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,ed1600e42df05fc885a16a66b77324a1", - "GSE55951_GPL18429.microarray.normalised.counts.cleaned.renamed.csv:md5,b0780db1cbdd32da955dae3375750713", - "warning_reason.txt:md5,314a4ad5c5016c98cb3adb16fd069234", - "whole_gene_id_mapping.csv:md5,63f67fb73898870c360293d30362bc33", + "whole_gene_id_mapping.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", "whole_gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", - "whole_design.csv:md5,f2723ea20ce675a7027a4b4285df7a96", + "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,b407990bb10106de943f9e93cda6323e", - "stability_values.normfinder.csv:md5,47c70a4e517d88da6402b79534e54767", "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", - "accessions.txt:md5,a850f625a78be7b4b10ce08a5b638e23", - "geo_all_datasets.metadata.tsv:md5,7fd4e7b26f1348f86e97aa16f3fd40c0", - "geo_rejected_datasets.metadata.tsv:md5,4b4908f7ae40b84a1ac1bd5addfc8dd2", - "geo_selected_datasets.metadata.tsv:md5,62bdb8eb0e87155d149020e65d110e80", - "GSE55951_GPL18429.microarray.normalised.counts.csv:md5,18fd2d728ad2ec5cb78f994f73375144", - "GSE55951_GPL18429.microarray.normalised.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d", - "warning_reason.txt:md5,603e30b732b7a6b501b59adf9d0e8837", - "id_mapping_stats.csv:md5,a433448030d9dfe86c57f634752cebe6", - "ratio_zeros.csv:md5,47c251137e39613a86f53a9ce7fec780", - "skewness.csv:md5,e3551c3c495314b4e81bd0d9838527db", - "geo_warning_reasons.csv:md5,e08541568733e7eac853f73480679e15", - "renaming_warning_reasons.tsv:md5,ae651ff0a559e025e014412009eac136" + "id_mapping_stats.csv:md5,6189d2a346cce55f182e00769e3fea5f", + "ratio_zeros.csv:md5,d3b518709a097d9e41a05142b524f03c", + "skewness.csv:md5,b38aabc94d60d93b979c3cef3a922299" ] ], "meta": { - "nf-test": "0.9.2", + "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-04T11:48:31.290055961" + "timestamp": "2025-12-15T16:58:27.415680085" }, "-profile test_full": { "content": [ - null, + { + "AGGREGATE_RESULTS": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "CLEAN_GENE_IDS": { + "pandas": "2.3.3", + "polars": "1.35.2", + "python": "3.14.0" + }, + "COLLECT_GENE_IDS": { + "pandas": "2.3.3", + "python": "3.14.0", + "tqdm": "4.67.1" + }, + "COLLECT_STATISTICS": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "COMPUTE_BASE_STATISTICS": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_BASE_STATISTICS_FOR_RNASEQ": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_DATASET_STATISTICS": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "COMPUTE_GENE_TRANSCRIPT_LENGTHS": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "COMPUTE_M_MEASURE": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_STABILITY_SCORES": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_TPM": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "CROSS_JOIN": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "DASH_APP": { + "python": "3.13.8", + "dash": "3.2.0", + "dash-extensions": "2.0.4", + "dash-mantine-components": "2.3.0", + "dash-ag-grid": "32.3.2", + "polars": "1.35.0", + "pandas": "2.3.3", + "pyarrow": "22.0.0", + "scipy": "1.16.3" + }, + "DOWNLOAD_ENSEMBL_ANNOTATION": { + "bs4": "4.14.2", + "pandas": "2.3.3", + "python": "3.14.0", + "requests": "2.32.5", + "tqdm": "4.67.1" + }, + "EXPRESSION_ATLAS": { + "ExpressionAtlas": "1.30.0", + "R": "4.3.3 (2024-02-29)", + "nltk": "3.9.1", + "pandas": "2.3.0", + "python": "3.13.5", + "pyyaml": "6.0.2", + "requests": "2.32.4" + }, + "EXPRESSION_RATIO": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "GET_CANDIDATE_GENES": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "GPROFILER_IDMAPPING": { + "pandas": "2.3.1", + "python": "3.13.5", + "requests": "2.32.4" + }, + "MAKE_CHUNKS": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "MERGE_ALL_COUNTS": { + "polars": "1.34.0", + "python": "3.14.0", + "tqdm": "4.67.1" + }, + "MERGE_RNASEQ_COUNTS": { + "polars": "1.34.0", + "python": "3.14.0", + "tqdm": "4.67.1" + }, + "NORMFINDER": { + "polars": "1.33.1", + "python": "3.13.7" + }, + "QUANTILE_NORMALISATION": { + "pandas": "2.2.3", + "pyarrow": "19.0.0", + "python": "3.12.8", + "scikit-learn": "1.6.1" + }, + "RATIO_STANDARD_VARIATION": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "RENAME_GENE_IDS": { + "pandas": "2.3.3", + "polars": "1.35.2", + "python": "3.14.0" + }, + "Workflow": { + "nf-core/stableexpression": "v1.0dev" + } + }, [ - "aggregate_results", - "aggregate_results/all_counts_filtered.parquet", - "aggregate_results/all_genes_summary.csv", - "aggregate_results/top_stable_genes_summary.csv", - "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", - "clean_gene_ids", - "clean_gene_ids/E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.csv", - "collect_statistics", - "collect_statistics/ratio_zeros.transposed.csv", - "collect_statistics/skewness.transposed.csv", - "compute_base_statistics", - "compute_base_statistics/stats_all_genes.csv", - "compute_base_statistics_for_rnaseq", - "compute_base_statistics_for_rnaseq/rnaseq.stats_all_genes.csv", - "compute_dataset_statistics", - "compute_dataset_statistics/ratio_zeros.txt", - "compute_dataset_statistics/skewness.txt", - "compute_gene_transcript_lengths", - "compute_gene_transcript_lengths/gene_transcript_lengths.csv", - "compute_m_measure", - "compute_m_measure/m_measures.csv", - "compute_stability_scores", - "compute_stability_scores/stats_with_scores.csv", - "cross_join", - "cross_join/cross_join.0.0.parquet", - "cross_join/cross_join.0.1.parquet", - "cross_join/cross_join.0.10.parquet", - "cross_join/cross_join.0.11.parquet", - "cross_join/cross_join.0.12.parquet", - "cross_join/cross_join.0.13.parquet", - "cross_join/cross_join.0.14.parquet", - "cross_join/cross_join.0.15.parquet", - "cross_join/cross_join.0.16.parquet", - "cross_join/cross_join.0.2.parquet", - "cross_join/cross_join.0.3.parquet", - "cross_join/cross_join.0.4.parquet", - "cross_join/cross_join.0.5.parquet", - "cross_join/cross_join.0.6.parquet", - "cross_join/cross_join.0.7.parquet", - "cross_join/cross_join.0.8.parquet", - "cross_join/cross_join.0.9.parquet", - "cross_join/cross_join.1.1.parquet", - "cross_join/cross_join.1.10.parquet", - "cross_join/cross_join.1.11.parquet", - "cross_join/cross_join.1.12.parquet", - "cross_join/cross_join.1.13.parquet", - "cross_join/cross_join.1.14.parquet", - "cross_join/cross_join.1.15.parquet", - "cross_join/cross_join.1.16.parquet", - "cross_join/cross_join.1.2.parquet", - "cross_join/cross_join.1.3.parquet", - "cross_join/cross_join.1.4.parquet", - "cross_join/cross_join.1.5.parquet", - "cross_join/cross_join.1.6.parquet", - "cross_join/cross_join.1.7.parquet", - "cross_join/cross_join.1.8.parquet", - "cross_join/cross_join.1.9.parquet", - "cross_join/cross_join.10.10.parquet", - "cross_join/cross_join.10.11.parquet", - "cross_join/cross_join.10.12.parquet", - "cross_join/cross_join.10.13.parquet", - "cross_join/cross_join.10.14.parquet", - "cross_join/cross_join.10.15.parquet", - "cross_join/cross_join.10.16.parquet", - "cross_join/cross_join.10.2.parquet", - "cross_join/cross_join.10.3.parquet", - "cross_join/cross_join.10.4.parquet", - "cross_join/cross_join.10.5.parquet", - "cross_join/cross_join.10.6.parquet", - "cross_join/cross_join.10.7.parquet", - "cross_join/cross_join.10.8.parquet", - "cross_join/cross_join.10.9.parquet", - "cross_join/cross_join.11.11.parquet", - "cross_join/cross_join.11.12.parquet", - "cross_join/cross_join.11.13.parquet", - "cross_join/cross_join.11.14.parquet", - "cross_join/cross_join.11.15.parquet", - "cross_join/cross_join.11.16.parquet", - "cross_join/cross_join.11.2.parquet", - "cross_join/cross_join.11.3.parquet", - "cross_join/cross_join.11.4.parquet", - "cross_join/cross_join.11.5.parquet", - "cross_join/cross_join.11.6.parquet", - "cross_join/cross_join.11.7.parquet", - "cross_join/cross_join.11.8.parquet", - "cross_join/cross_join.11.9.parquet", - "cross_join/cross_join.12.12.parquet", - "cross_join/cross_join.12.13.parquet", - "cross_join/cross_join.12.14.parquet", - "cross_join/cross_join.12.15.parquet", - "cross_join/cross_join.12.16.parquet", - "cross_join/cross_join.12.2.parquet", - "cross_join/cross_join.12.3.parquet", - "cross_join/cross_join.12.4.parquet", - "cross_join/cross_join.12.5.parquet", - "cross_join/cross_join.12.6.parquet", - "cross_join/cross_join.12.7.parquet", - "cross_join/cross_join.12.8.parquet", - "cross_join/cross_join.12.9.parquet", - "cross_join/cross_join.13.13.parquet", - "cross_join/cross_join.13.14.parquet", - "cross_join/cross_join.13.15.parquet", - "cross_join/cross_join.13.16.parquet", - "cross_join/cross_join.13.2.parquet", - "cross_join/cross_join.13.3.parquet", - "cross_join/cross_join.13.4.parquet", - "cross_join/cross_join.13.5.parquet", - "cross_join/cross_join.13.6.parquet", - "cross_join/cross_join.13.7.parquet", - "cross_join/cross_join.13.8.parquet", - "cross_join/cross_join.13.9.parquet", - "cross_join/cross_join.14.14.parquet", - "cross_join/cross_join.14.15.parquet", - "cross_join/cross_join.14.16.parquet", - "cross_join/cross_join.14.2.parquet", - "cross_join/cross_join.14.3.parquet", - "cross_join/cross_join.14.4.parquet", - "cross_join/cross_join.14.5.parquet", - "cross_join/cross_join.14.6.parquet", - "cross_join/cross_join.14.7.parquet", - "cross_join/cross_join.14.8.parquet", - "cross_join/cross_join.14.9.parquet", - "cross_join/cross_join.15.15.parquet", - "cross_join/cross_join.15.16.parquet", - "cross_join/cross_join.15.2.parquet", - "cross_join/cross_join.15.3.parquet", - "cross_join/cross_join.15.4.parquet", - "cross_join/cross_join.15.5.parquet", - "cross_join/cross_join.15.6.parquet", - "cross_join/cross_join.15.7.parquet", - "cross_join/cross_join.15.8.parquet", - "cross_join/cross_join.15.9.parquet", - "cross_join/cross_join.16.16.parquet", - "cross_join/cross_join.16.2.parquet", - "cross_join/cross_join.16.3.parquet", - "cross_join/cross_join.16.4.parquet", - "cross_join/cross_join.16.5.parquet", - "cross_join/cross_join.16.6.parquet", - "cross_join/cross_join.16.7.parquet", - "cross_join/cross_join.16.8.parquet", - "cross_join/cross_join.16.9.parquet", - "cross_join/cross_join.2.2.parquet", - "cross_join/cross_join.2.3.parquet", - "cross_join/cross_join.2.4.parquet", - "cross_join/cross_join.2.5.parquet", - "cross_join/cross_join.2.6.parquet", - "cross_join/cross_join.2.7.parquet", - "cross_join/cross_join.2.8.parquet", - "cross_join/cross_join.2.9.parquet", - "cross_join/cross_join.3.3.parquet", - "cross_join/cross_join.3.4.parquet", - "cross_join/cross_join.3.5.parquet", - "cross_join/cross_join.3.6.parquet", - "cross_join/cross_join.3.7.parquet", - "cross_join/cross_join.3.8.parquet", - "cross_join/cross_join.3.9.parquet", - "cross_join/cross_join.4.4.parquet", - "cross_join/cross_join.4.5.parquet", - "cross_join/cross_join.4.6.parquet", - "cross_join/cross_join.4.7.parquet", - "cross_join/cross_join.4.8.parquet", - "cross_join/cross_join.4.9.parquet", - "cross_join/cross_join.5.5.parquet", - "cross_join/cross_join.5.6.parquet", - "cross_join/cross_join.5.7.parquet", - "cross_join/cross_join.5.8.parquet", - "cross_join/cross_join.5.9.parquet", - "cross_join/cross_join.6.6.parquet", - "cross_join/cross_join.6.7.parquet", - "cross_join/cross_join.6.8.parquet", - "cross_join/cross_join.6.9.parquet", - "cross_join/cross_join.7.7.parquet", - "cross_join/cross_join.7.8.parquet", - "cross_join/cross_join.7.9.parquet", - "cross_join/cross_join.8.8.parquet", - "cross_join/cross_join.8.9.parquet", - "cross_join/cross_join.9.9.parquet", + "aggregated", + "aggregated/all_counts_filtered.parquet", + "aggregated/all_genes_summary.csv", + "aggregated/most_stable_genes_summary.csv", + "aggregated/most_stable_genes_transposed_counts_filtered.csv", "dash_app", "dash_app/app.py", "dash_app/assets", @@ -1498,166 +1958,7 @@ "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", "dash_app/versions.yml", - "download_ensembl_annotation", - "download_ensembl_annotation/Arabidopsis_lyrata.v.1.0.62.gff3.gz", "errors", - "expression_ratio", - "expression_ratio/ratios.0.0.parquet", - "expression_ratio/ratios.0.1.parquet", - "expression_ratio/ratios.0.10.parquet", - "expression_ratio/ratios.0.11.parquet", - "expression_ratio/ratios.0.12.parquet", - "expression_ratio/ratios.0.13.parquet", - "expression_ratio/ratios.0.14.parquet", - "expression_ratio/ratios.0.15.parquet", - "expression_ratio/ratios.0.16.parquet", - "expression_ratio/ratios.0.2.parquet", - "expression_ratio/ratios.0.3.parquet", - "expression_ratio/ratios.0.4.parquet", - "expression_ratio/ratios.0.5.parquet", - "expression_ratio/ratios.0.6.parquet", - "expression_ratio/ratios.0.7.parquet", - "expression_ratio/ratios.0.8.parquet", - "expression_ratio/ratios.0.9.parquet", - "expression_ratio/ratios.1.1.parquet", - "expression_ratio/ratios.1.10.parquet", - "expression_ratio/ratios.1.11.parquet", - "expression_ratio/ratios.1.12.parquet", - "expression_ratio/ratios.1.13.parquet", - "expression_ratio/ratios.1.14.parquet", - "expression_ratio/ratios.1.15.parquet", - "expression_ratio/ratios.1.16.parquet", - "expression_ratio/ratios.1.2.parquet", - "expression_ratio/ratios.1.3.parquet", - "expression_ratio/ratios.1.4.parquet", - "expression_ratio/ratios.1.5.parquet", - "expression_ratio/ratios.1.6.parquet", - "expression_ratio/ratios.1.7.parquet", - "expression_ratio/ratios.1.8.parquet", - "expression_ratio/ratios.1.9.parquet", - "expression_ratio/ratios.10.10.parquet", - "expression_ratio/ratios.10.11.parquet", - "expression_ratio/ratios.10.12.parquet", - "expression_ratio/ratios.10.13.parquet", - "expression_ratio/ratios.10.14.parquet", - "expression_ratio/ratios.10.15.parquet", - "expression_ratio/ratios.10.16.parquet", - "expression_ratio/ratios.10.2.parquet", - "expression_ratio/ratios.10.3.parquet", - "expression_ratio/ratios.10.4.parquet", - "expression_ratio/ratios.10.5.parquet", - "expression_ratio/ratios.10.6.parquet", - "expression_ratio/ratios.10.7.parquet", - "expression_ratio/ratios.10.8.parquet", - "expression_ratio/ratios.10.9.parquet", - "expression_ratio/ratios.11.11.parquet", - "expression_ratio/ratios.11.12.parquet", - "expression_ratio/ratios.11.13.parquet", - "expression_ratio/ratios.11.14.parquet", - "expression_ratio/ratios.11.15.parquet", - "expression_ratio/ratios.11.16.parquet", - "expression_ratio/ratios.11.2.parquet", - "expression_ratio/ratios.11.3.parquet", - "expression_ratio/ratios.11.4.parquet", - "expression_ratio/ratios.11.5.parquet", - "expression_ratio/ratios.11.6.parquet", - "expression_ratio/ratios.11.7.parquet", - "expression_ratio/ratios.11.8.parquet", - "expression_ratio/ratios.11.9.parquet", - "expression_ratio/ratios.12.12.parquet", - "expression_ratio/ratios.12.13.parquet", - "expression_ratio/ratios.12.14.parquet", - "expression_ratio/ratios.12.15.parquet", - "expression_ratio/ratios.12.16.parquet", - "expression_ratio/ratios.12.2.parquet", - "expression_ratio/ratios.12.3.parquet", - "expression_ratio/ratios.12.4.parquet", - "expression_ratio/ratios.12.5.parquet", - "expression_ratio/ratios.12.6.parquet", - "expression_ratio/ratios.12.7.parquet", - "expression_ratio/ratios.12.8.parquet", - "expression_ratio/ratios.12.9.parquet", - "expression_ratio/ratios.13.13.parquet", - "expression_ratio/ratios.13.14.parquet", - "expression_ratio/ratios.13.15.parquet", - "expression_ratio/ratios.13.16.parquet", - "expression_ratio/ratios.13.2.parquet", - "expression_ratio/ratios.13.3.parquet", - "expression_ratio/ratios.13.4.parquet", - "expression_ratio/ratios.13.5.parquet", - "expression_ratio/ratios.13.6.parquet", - "expression_ratio/ratios.13.7.parquet", - "expression_ratio/ratios.13.8.parquet", - "expression_ratio/ratios.13.9.parquet", - "expression_ratio/ratios.14.14.parquet", - "expression_ratio/ratios.14.15.parquet", - "expression_ratio/ratios.14.16.parquet", - "expression_ratio/ratios.14.2.parquet", - "expression_ratio/ratios.14.3.parquet", - "expression_ratio/ratios.14.4.parquet", - "expression_ratio/ratios.14.5.parquet", - "expression_ratio/ratios.14.6.parquet", - "expression_ratio/ratios.14.7.parquet", - "expression_ratio/ratios.14.8.parquet", - "expression_ratio/ratios.14.9.parquet", - "expression_ratio/ratios.15.15.parquet", - "expression_ratio/ratios.15.16.parquet", - "expression_ratio/ratios.15.2.parquet", - "expression_ratio/ratios.15.3.parquet", - "expression_ratio/ratios.15.4.parquet", - "expression_ratio/ratios.15.5.parquet", - "expression_ratio/ratios.15.6.parquet", - "expression_ratio/ratios.15.7.parquet", - "expression_ratio/ratios.15.8.parquet", - "expression_ratio/ratios.15.9.parquet", - "expression_ratio/ratios.16.16.parquet", - "expression_ratio/ratios.16.2.parquet", - "expression_ratio/ratios.16.3.parquet", - "expression_ratio/ratios.16.4.parquet", - "expression_ratio/ratios.16.5.parquet", - "expression_ratio/ratios.16.6.parquet", - "expression_ratio/ratios.16.7.parquet", - "expression_ratio/ratios.16.8.parquet", - "expression_ratio/ratios.16.9.parquet", - "expression_ratio/ratios.2.2.parquet", - "expression_ratio/ratios.2.3.parquet", - "expression_ratio/ratios.2.4.parquet", - "expression_ratio/ratios.2.5.parquet", - "expression_ratio/ratios.2.6.parquet", - "expression_ratio/ratios.2.7.parquet", - "expression_ratio/ratios.2.8.parquet", - "expression_ratio/ratios.2.9.parquet", - "expression_ratio/ratios.3.3.parquet", - "expression_ratio/ratios.3.4.parquet", - "expression_ratio/ratios.3.5.parquet", - "expression_ratio/ratios.3.6.parquet", - "expression_ratio/ratios.3.7.parquet", - "expression_ratio/ratios.3.8.parquet", - "expression_ratio/ratios.3.9.parquet", - "expression_ratio/ratios.4.4.parquet", - "expression_ratio/ratios.4.5.parquet", - "expression_ratio/ratios.4.6.parquet", - "expression_ratio/ratios.4.7.parquet", - "expression_ratio/ratios.4.8.parquet", - "expression_ratio/ratios.4.9.parquet", - "expression_ratio/ratios.5.5.parquet", - "expression_ratio/ratios.5.6.parquet", - "expression_ratio/ratios.5.7.parquet", - "expression_ratio/ratios.5.8.parquet", - "expression_ratio/ratios.5.9.parquet", - "expression_ratio/ratios.6.6.parquet", - "expression_ratio/ratios.6.7.parquet", - "expression_ratio/ratios.6.8.parquet", - "expression_ratio/ratios.6.9.parquet", - "expression_ratio/ratios.7.7.parquet", - "expression_ratio/ratios.7.8.parquet", - "expression_ratio/ratios.7.9.parquet", - "expression_ratio/ratios.8.8.parquet", - "expression_ratio/ratios.8.9.parquet", - "expression_ratio/ratios.9.9.parquet", - "geo", - "get_candidate_genes", - "get_candidate_genes/candidate_counts.parquet", "idmapping", "idmapping/collected_gene_ids", "idmapping/collected_gene_ids/all_gene_ids.txt", @@ -1671,28 +1972,6 @@ "idmapping/renamed/E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", - "make_chunks", - "make_chunks/count_chunk.0.parquet", - "make_chunks/count_chunk.1.parquet", - "make_chunks/count_chunk.10.parquet", - "make_chunks/count_chunk.11.parquet", - "make_chunks/count_chunk.12.parquet", - "make_chunks/count_chunk.13.parquet", - "make_chunks/count_chunk.14.parquet", - "make_chunks/count_chunk.15.parquet", - "make_chunks/count_chunk.16.parquet", - "make_chunks/count_chunk.2.parquet", - "make_chunks/count_chunk.3.parquet", - "make_chunks/count_chunk.4.parquet", - "make_chunks/count_chunk.5.parquet", - "make_chunks/count_chunk.6.parquet", - "make_chunks/count_chunk.7.parquet", - "make_chunks/count_chunk.8.parquet", - "make_chunks/count_chunk.9.parquet", - "merge_all_counts", - "merge_all_counts/all_counts.parquet", - "merge_rnaseq_counts", - "merge_rnaseq_counts/all_counts.parquet", "merged_datasets", "merged_datasets/whole_design.csv", "multiqc", @@ -1704,13 +1983,10 @@ "multiqc/multiqc_data/multiqc_data.json", "multiqc/multiqc_data/multiqc_eatlas_all_experiments_metadata.txt", "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", + "multiqc/multiqc_data/multiqc_expression_distributions_most_stable_genes.txt", "multiqc/multiqc_data/multiqc_gene_statistics.txt", - "multiqc/multiqc_data/multiqc_geo_all_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_rejected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_selected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_geo_warning_reasons.txt", - "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", + "multiqc/multiqc_data/multiqc_id_mapping_stats.txt", + "multiqc/multiqc_data/multiqc_ranked_most_stable_genes_summary.txt", "multiqc/multiqc_data/multiqc_ratio_zeros.txt", "multiqc/multiqc_data/multiqc_skewness.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", @@ -1719,37 +1995,31 @@ "multiqc/multiqc_plots/pdf", "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", + "multiqc/multiqc_plots/pdf/expression_distributions_most_stable_genes.pdf", "multiqc/multiqc_plots/pdf/gene_statistics.pdf", - "multiqc/multiqc_plots/pdf/geo_all_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_rejected_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_selected_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/geo_warning_reasons.pdf", - "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", + "multiqc/multiqc_plots/pdf/id_mapping_stats-cnt.pdf", + "multiqc/multiqc_plots/pdf/id_mapping_stats-pct.pdf", + "multiqc/multiqc_plots/pdf/ranked_most_stable_genes_summary.pdf", "multiqc/multiqc_plots/pdf/ratio_zeros.pdf", "multiqc/multiqc_plots/pdf/skewness.pdf", "multiqc/multiqc_plots/png", "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", - "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", + "multiqc/multiqc_plots/png/expression_distributions_most_stable_genes.png", "multiqc/multiqc_plots/png/gene_statistics.png", - "multiqc/multiqc_plots/png/geo_all_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_rejected_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_selected_experiments_metadata.png", - "multiqc/multiqc_plots/png/geo_warning_reasons.png", - "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", + "multiqc/multiqc_plots/png/id_mapping_stats-cnt.png", + "multiqc/multiqc_plots/png/id_mapping_stats-pct.png", + "multiqc/multiqc_plots/png/ranked_most_stable_genes_summary.png", "multiqc/multiqc_plots/png/ratio_zeros.png", "multiqc/multiqc_plots/png/skewness.png", "multiqc/multiqc_plots/svg", "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", + "multiqc/multiqc_plots/svg/expression_distributions_most_stable_genes.svg", "multiqc/multiqc_plots/svg/gene_statistics.svg", - "multiqc/multiqc_plots/svg/geo_all_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_rejected_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_selected_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/geo_warning_reasons.svg", - "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", + "multiqc/multiqc_plots/svg/id_mapping_stats-cnt.svg", + "multiqc/multiqc_plots/svg/id_mapping_stats-pct.svg", + "multiqc/multiqc_plots/svg/ranked_most_stable_genes_summary.svg", "multiqc/multiqc_plots/svg/ratio_zeros.svg", "multiqc/multiqc_plots/svg/skewness.svg", "multiqc/multiqc_report.html", @@ -1760,10 +2030,8 @@ "normalised/E_MTAB_5072_rnaseq/quantile_normalised/E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", "normalised/E_MTAB_5072_rnaseq/tpm", "normalised/E_MTAB_5072_rnaseq/tpm/E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", - "normfinder", - "normfinder/stability_values.normfinder.csv", "pipeline_info", - "pipeline_info/software_mqc_versions.yml", + "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", "public_data", "public_data/expression_atlas", "public_data/expression_atlas/accessions", @@ -1773,189 +2041,16 @@ "public_data/expression_atlas/datasets", "public_data/expression_atlas/datasets/E_MTAB_5072_rnaseq.design.csv", "public_data/expression_atlas/datasets/E_MTAB_5072_rnaseq.rnaseq.raw.counts.csv", - "public_data/geo", - "public_data/geo/accessions", - "public_data/geo/accessions/accessions.txt", - "public_data/geo/accessions/geo_all_datasets.metadata.tsv", - "public_data/geo/accessions/geo_rejected_datasets.metadata.tsv", - "public_data/geo/accessions/geo_selected_datasets.metadata.tsv", - "public_data/geo/datasets", - "public_data/geo/datasets/warning_reason.txt", - "ratio_standard_variation", - "ratio_standard_variation/std.0.0.parquet", - "ratio_standard_variation/std.0.1.parquet", - "ratio_standard_variation/std.0.10.parquet", - "ratio_standard_variation/std.0.11.parquet", - "ratio_standard_variation/std.0.12.parquet", - "ratio_standard_variation/std.0.13.parquet", - "ratio_standard_variation/std.0.14.parquet", - "ratio_standard_variation/std.0.15.parquet", - "ratio_standard_variation/std.0.16.parquet", - "ratio_standard_variation/std.0.2.parquet", - "ratio_standard_variation/std.0.3.parquet", - "ratio_standard_variation/std.0.4.parquet", - "ratio_standard_variation/std.0.5.parquet", - "ratio_standard_variation/std.0.6.parquet", - "ratio_standard_variation/std.0.7.parquet", - "ratio_standard_variation/std.0.8.parquet", - "ratio_standard_variation/std.0.9.parquet", - "ratio_standard_variation/std.1.1.parquet", - "ratio_standard_variation/std.1.10.parquet", - "ratio_standard_variation/std.1.11.parquet", - "ratio_standard_variation/std.1.12.parquet", - "ratio_standard_variation/std.1.13.parquet", - "ratio_standard_variation/std.1.14.parquet", - "ratio_standard_variation/std.1.15.parquet", - "ratio_standard_variation/std.1.16.parquet", - "ratio_standard_variation/std.1.2.parquet", - "ratio_standard_variation/std.1.3.parquet", - "ratio_standard_variation/std.1.4.parquet", - "ratio_standard_variation/std.1.5.parquet", - "ratio_standard_variation/std.1.6.parquet", - "ratio_standard_variation/std.1.7.parquet", - "ratio_standard_variation/std.1.8.parquet", - "ratio_standard_variation/std.1.9.parquet", - "ratio_standard_variation/std.10.10.parquet", - "ratio_standard_variation/std.10.11.parquet", - "ratio_standard_variation/std.10.12.parquet", - "ratio_standard_variation/std.10.13.parquet", - "ratio_standard_variation/std.10.14.parquet", - "ratio_standard_variation/std.10.15.parquet", - "ratio_standard_variation/std.10.16.parquet", - "ratio_standard_variation/std.10.2.parquet", - "ratio_standard_variation/std.10.3.parquet", - "ratio_standard_variation/std.10.4.parquet", - "ratio_standard_variation/std.10.5.parquet", - "ratio_standard_variation/std.10.6.parquet", - "ratio_standard_variation/std.10.7.parquet", - "ratio_standard_variation/std.10.8.parquet", - "ratio_standard_variation/std.10.9.parquet", - "ratio_standard_variation/std.11.11.parquet", - "ratio_standard_variation/std.11.12.parquet", - "ratio_standard_variation/std.11.13.parquet", - "ratio_standard_variation/std.11.14.parquet", - "ratio_standard_variation/std.11.15.parquet", - "ratio_standard_variation/std.11.16.parquet", - "ratio_standard_variation/std.11.2.parquet", - "ratio_standard_variation/std.11.3.parquet", - "ratio_standard_variation/std.11.4.parquet", - "ratio_standard_variation/std.11.5.parquet", - "ratio_standard_variation/std.11.6.parquet", - "ratio_standard_variation/std.11.7.parquet", - "ratio_standard_variation/std.11.8.parquet", - "ratio_standard_variation/std.11.9.parquet", - "ratio_standard_variation/std.12.12.parquet", - "ratio_standard_variation/std.12.13.parquet", - "ratio_standard_variation/std.12.14.parquet", - "ratio_standard_variation/std.12.15.parquet", - "ratio_standard_variation/std.12.16.parquet", - "ratio_standard_variation/std.12.2.parquet", - "ratio_standard_variation/std.12.3.parquet", - "ratio_standard_variation/std.12.4.parquet", - "ratio_standard_variation/std.12.5.parquet", - "ratio_standard_variation/std.12.6.parquet", - "ratio_standard_variation/std.12.7.parquet", - "ratio_standard_variation/std.12.8.parquet", - "ratio_standard_variation/std.12.9.parquet", - "ratio_standard_variation/std.13.13.parquet", - "ratio_standard_variation/std.13.14.parquet", - "ratio_standard_variation/std.13.15.parquet", - "ratio_standard_variation/std.13.16.parquet", - "ratio_standard_variation/std.13.2.parquet", - "ratio_standard_variation/std.13.3.parquet", - "ratio_standard_variation/std.13.4.parquet", - "ratio_standard_variation/std.13.5.parquet", - "ratio_standard_variation/std.13.6.parquet", - "ratio_standard_variation/std.13.7.parquet", - "ratio_standard_variation/std.13.8.parquet", - "ratio_standard_variation/std.13.9.parquet", - "ratio_standard_variation/std.14.14.parquet", - "ratio_standard_variation/std.14.15.parquet", - "ratio_standard_variation/std.14.16.parquet", - "ratio_standard_variation/std.14.2.parquet", - "ratio_standard_variation/std.14.3.parquet", - "ratio_standard_variation/std.14.4.parquet", - "ratio_standard_variation/std.14.5.parquet", - "ratio_standard_variation/std.14.6.parquet", - "ratio_standard_variation/std.14.7.parquet", - "ratio_standard_variation/std.14.8.parquet", - "ratio_standard_variation/std.14.9.parquet", - "ratio_standard_variation/std.15.15.parquet", - "ratio_standard_variation/std.15.16.parquet", - "ratio_standard_variation/std.15.2.parquet", - "ratio_standard_variation/std.15.3.parquet", - "ratio_standard_variation/std.15.4.parquet", - "ratio_standard_variation/std.15.5.parquet", - "ratio_standard_variation/std.15.6.parquet", - "ratio_standard_variation/std.15.7.parquet", - "ratio_standard_variation/std.15.8.parquet", - "ratio_standard_variation/std.15.9.parquet", - "ratio_standard_variation/std.16.16.parquet", - "ratio_standard_variation/std.16.2.parquet", - "ratio_standard_variation/std.16.3.parquet", - "ratio_standard_variation/std.16.4.parquet", - "ratio_standard_variation/std.16.5.parquet", - "ratio_standard_variation/std.16.6.parquet", - "ratio_standard_variation/std.16.7.parquet", - "ratio_standard_variation/std.16.8.parquet", - "ratio_standard_variation/std.16.9.parquet", - "ratio_standard_variation/std.2.2.parquet", - "ratio_standard_variation/std.2.3.parquet", - "ratio_standard_variation/std.2.4.parquet", - "ratio_standard_variation/std.2.5.parquet", - "ratio_standard_variation/std.2.6.parquet", - "ratio_standard_variation/std.2.7.parquet", - "ratio_standard_variation/std.2.8.parquet", - "ratio_standard_variation/std.2.9.parquet", - "ratio_standard_variation/std.3.3.parquet", - "ratio_standard_variation/std.3.4.parquet", - "ratio_standard_variation/std.3.5.parquet", - "ratio_standard_variation/std.3.6.parquet", - "ratio_standard_variation/std.3.7.parquet", - "ratio_standard_variation/std.3.8.parquet", - "ratio_standard_variation/std.3.9.parquet", - "ratio_standard_variation/std.4.4.parquet", - "ratio_standard_variation/std.4.5.parquet", - "ratio_standard_variation/std.4.6.parquet", - "ratio_standard_variation/std.4.7.parquet", - "ratio_standard_variation/std.4.8.parquet", - "ratio_standard_variation/std.4.9.parquet", - "ratio_standard_variation/std.5.5.parquet", - "ratio_standard_variation/std.5.6.parquet", - "ratio_standard_variation/std.5.7.parquet", - "ratio_standard_variation/std.5.8.parquet", - "ratio_standard_variation/std.5.9.parquet", - "ratio_standard_variation/std.6.6.parquet", - "ratio_standard_variation/std.6.7.parquet", - "ratio_standard_variation/std.6.8.parquet", - "ratio_standard_variation/std.6.9.parquet", - "ratio_standard_variation/std.7.7.parquet", - "ratio_standard_variation/std.7.8.parquet", - "ratio_standard_variation/std.7.9.parquet", - "ratio_standard_variation/std.8.8.parquet", - "ratio_standard_variation/std.8.9.parquet", - "ratio_standard_variation/std.9.9.parquet", "statistics", "statistics/id_mapping_stats.csv", "statistics/ratio_zeros.csv", "statistics/skewness.csv", - "warnings", - "warnings/geo_warning_reasons.csv" + "warnings" ], [ "all_genes_summary.csv:md5,d4b5d36061b8fd31ba27edc55f738a01", - "top_stable_genes_summary.csv:md5,b47891fe0414abd0a4b7c9137d53fc1d", - "top_stable_genes_transposed_counts_filtered.csv:md5,46d96c944cc9ca3dd83e16da52c528f2", - "E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.csv:md5,d0f6092dc2e4b4f8ecb4f594ec3ce953", - "ratio_zeros.transposed.csv:md5,ba08c111e0148809a0807130a9489220", - "skewness.transposed.csv:md5,6adfce705b65fc2aa878759bb491b0d4", - "stats_all_genes.csv:md5,4d4f56b0b517e95d20cc39e4c3c124e9", - "rnaseq.stats_all_genes.csv:md5,e0562faf910765c3e99589f9bdd26fd3", - "ratio_zeros.txt:md5,2c1811ac25e8cbb3d5bdd7f1882522da", - "skewness.txt:md5,e8d0870cf3f35d0bab5c5d73c3b4a818", - "gene_transcript_lengths.csv:md5,d5dbdbab0b6306896988ed8accec67af", - "m_measures.csv:md5,22cadc22042ae601b11e5f6ba81893d9", - "stats_with_scores.csv:md5,28f7d769cbcc1164da56cf009091171d", + "most_stable_genes_summary.csv:md5,b47891fe0414abd0a4b7c9137d53fc1d", + "most_stable_genes_transposed_counts_filtered.csv:md5,46d96c944cc9ca3dd83e16da52c528f2", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", "all_genes_summary.csv:md5,d4b5d36061b8fd31ba27edc55f738a01", @@ -1978,66 +2073,134 @@ "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", "style.py:md5,b9f1207f06464e43c05e6e3912f12731", "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "Arabidopsis_lyrata.v.1.0.62.gff3.gz:md5,35e546e88c7cd204870b18e888e17dae", - "all_gene_ids.txt:md5,ec39408cbfb18e25b282202122718367", - "global_gene_id_mapping.csv:md5,0271a33d981a74c62590ea3ef17dea9d", - "global_gene_metadata.csv:md5,20009019168849adeb1c1ba984a78c1e", - "gene_metadata.csv:md5,7e16813046376d2425ad2f36f601c72a", - "mapped_gene_ids.csv:md5,8e1393b156f79c2874960dc13afef854", - "original_gene_ids.txt:md5,566c596744746d6f3dd2d0552ea2ffc4", + "all_gene_ids.txt:md5,13ae1b52833134f8ed6d982c00487927", + "global_gene_id_mapping.csv:md5,efc95a8e276be1eb0af9639f72e48145", + "global_gene_metadata.csv:md5,1d342577587bc48c1eff077a594929fa", + "gene_metadata.csv:md5,1d342577587bc48c1eff077a594929fa", + "mapped_gene_ids.csv:md5,efc95a8e276be1eb0af9639f72e48145", + "original_gene_ids.txt:md5,5b88757be20075a2458a257221703f2a", "E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,6244da0761b4437a6de5cff49e4d2687", "whole_gene_id_mapping.csv:md5,efc95a8e276be1eb0af9639f72e48145", "whole_gene_metadata.csv:md5,1d342577587bc48c1eff077a594929fa", "whole_design.csv:md5,c9bfd7bc8ca365222e03e67eb24b9a76", "E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,4a2aa87d0bd2990c13e088f0117cc682", - "stability_values.normfinder.csv:md5,4dbd2449b2919edc7dd80f1876e2abf6", "accessions.txt:md5,561a967c16b2ef6c29fb643cd4002947", "selected_experiments.metadata.tsv:md5,385b4d7ed40829f1e764857e805d6dc4", "species_experiments.metadata.tsv:md5,385b4d7ed40829f1e764857e805d6dc4", "E_MTAB_5072_rnaseq.design.csv:md5,a1f33d126dde387a2d542381c44bc1f3", "E_MTAB_5072_rnaseq.rnaseq.raw.counts.csv:md5,c41c84899a380515d759b99eeccfe43e", - "accessions.txt:md5,48a6870e7e7e7d481e9b28002e68880e", - "geo_all_datasets.metadata.tsv:md5,4a775b1e2045ddc3cf87d30173fcf86b", - "geo_rejected_datasets.metadata.tsv:md5,68ced9fbcab8af30a81cc5e23c68c179", - "geo_selected_datasets.metadata.tsv:md5,96ec5af87cd9ee034726b43ed8757d01", - "warning_reason.txt:md5,46bd94872631702e89a304c0adb7a8c1", - "id_mapping_stats.csv:md5,f0d869ee4b9896f37be649c371ffb747", + "id_mapping_stats.csv:md5,6a84babdbb434ffd5975fc771ec2db44", "ratio_zeros.csv:md5,15eb210c4ffff8a826e0c9f45d0ab4bd", - "skewness.csv:md5,6dbe4934961471c31fc6a1671577a8fc", - "geo_warning_reasons.csv:md5,06fc59caef4088ee67a9c27c3b5a2574" + "skewness.csv:md5,6dbe4934961471c31fc6a1671577a8fc" ] ], "meta": { - "nf-test": "0.9.2", + "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-04T11:53:54.330900136" + "timestamp": "2025-12-15T17:03:12.971932361" }, "-profile test_dataset_custom_mapping": { "content": [ - null, + { + "AGGREGATE_RESULTS": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COLLECT_STATISTICS": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "COMPUTE_BASE_STATISTICS": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_BASE_STATISTICS_FOR_MICROARRAY": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_BASE_STATISTICS_FOR_RNASEQ": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_DATASET_STATISTICS": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "COMPUTE_GENE_TRANSCRIPT_LENGTHS": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "COMPUTE_STABILITY_SCORES": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_TPM": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "DASH_APP": { + "python": "3.13.8", + "dash": "3.2.0", + "dash-extensions": "2.0.4", + "dash-mantine-components": "2.3.0", + "dash-ag-grid": "32.3.2", + "polars": "1.35.0", + "pandas": "2.3.3", + "pyarrow": "22.0.0", + "scipy": "1.16.3" + }, + "DOWNLOAD_ENSEMBL_ANNOTATION": { + "bs4": "4.14.2", + "pandas": "2.3.3", + "python": "3.14.0", + "requests": "2.32.5", + "tqdm": "4.67.1" + }, + "GET_CANDIDATE_GENES": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "MERGE_ALL_COUNTS": { + "polars": "1.34.0", + "python": "3.14.0", + "tqdm": "4.67.1" + }, + "MERGE_MICROARRAY_COUNTS": { + "polars": "1.34.0", + "python": "3.14.0", + "tqdm": "4.67.1" + }, + "MERGE_RNASEQ_COUNTS": { + "polars": "1.34.0", + "python": "3.14.0", + "tqdm": "4.67.1" + }, + "NORMFINDER": { + "polars": "1.33.1", + "python": "3.13.7" + }, + "QUANTILE_NORMALISATION": { + "pandas": "2.2.3", + "pyarrow": "19.0.0", + "python": "3.12.8", + "scikit-learn": "1.6.1" + }, + "RENAME_GENE_IDS": { + "pandas": "2.3.3", + "polars": "1.35.2", + "python": "3.14.0" + }, + "Workflow": { + "nf-core/stableexpression": "v1.0dev" + } + }, [ - "aggregate_results", - "aggregate_results/all_counts_filtered.parquet", - "aggregate_results/all_genes_summary.csv", - "aggregate_results/top_stable_genes_summary.csv", - "aggregate_results/top_stable_genes_transposed_counts_filtered.csv", - "collect_statistics", - "collect_statistics/ratio_zeros.transposed.csv", - "collect_statistics/skewness.transposed.csv", - "compute_base_statistics", - "compute_base_statistics/stats_all_genes.csv", - "compute_base_statistics_for_microarray", - "compute_base_statistics_for_microarray/microarray.stats_all_genes.csv", - "compute_base_statistics_for_rnaseq", - "compute_base_statistics_for_rnaseq/rnaseq.stats_all_genes.csv", - "compute_dataset_statistics", - "compute_dataset_statistics/ratio_zeros.txt", - "compute_dataset_statistics/skewness.txt", - "compute_gene_transcript_lengths", - "compute_gene_transcript_lengths/gene_transcript_lengths.csv", - "compute_stability_scores", - "compute_stability_scores/stats_with_scores.csv", + "aggregated", + "aggregated/all_counts_filtered.parquet", + "aggregated/all_genes_summary.csv", + "aggregated/most_stable_genes_summary.csv", + "aggregated/most_stable_genes_transposed_counts_filtered.csv", "dash_app", "dash_app/app.py", "dash_app/assets", @@ -2070,11 +2233,7 @@ "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", "dash_app/versions.yml", - "download_ensembl_annotation", - "download_ensembl_annotation/Solanum_tuberosum.SolTub_3.0.62.gff3.gz", "errors", - "get_candidate_genes", - "get_candidate_genes/candidate_counts.parquet", "idmapping", "idmapping/global_gene_id_mapping.csv", "idmapping/global_gene_metadata.csv", @@ -2083,12 +2242,6 @@ "idmapping/renamed/rnaseq.raw.renamed.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", - "merge_all_counts", - "merge_all_counts/all_counts.parquet", - "merge_microarray_counts", - "merge_microarray_counts/all_counts.parquet", - "merge_rnaseq_counts", - "merge_rnaseq_counts/all_counts.parquet", "merged_datasets", "merged_datasets/whole_design.csv", "multiqc", @@ -2098,30 +2251,37 @@ "multiqc/multiqc_data/multiqc.parquet", "multiqc/multiqc_data/multiqc_citations.txt", "multiqc/multiqc_data/multiqc_data.json", - "multiqc/multiqc_data/multiqc_expression_distributions_top_stable_genes.txt", + "multiqc/multiqc_data/multiqc_expression_distributions_most_stable_genes.txt", "multiqc/multiqc_data/multiqc_gene_statistics.txt", - "multiqc/multiqc_data/multiqc_ranked_top_stable_genes_summary.txt", + "multiqc/multiqc_data/multiqc_id_mapping_stats.txt", + "multiqc/multiqc_data/multiqc_ranked_most_stable_genes_summary.txt", "multiqc/multiqc_data/multiqc_ratio_zeros.txt", "multiqc/multiqc_data/multiqc_skewness.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", "multiqc/multiqc_plots", "multiqc/multiqc_plots/pdf", - "multiqc/multiqc_plots/pdf/expression_distributions_top_stable_genes.pdf", + "multiqc/multiqc_plots/pdf/expression_distributions_most_stable_genes.pdf", "multiqc/multiqc_plots/pdf/gene_statistics.pdf", - "multiqc/multiqc_plots/pdf/ranked_top_stable_genes_summary.pdf", + "multiqc/multiqc_plots/pdf/id_mapping_stats-cnt.pdf", + "multiqc/multiqc_plots/pdf/id_mapping_stats-pct.pdf", + "multiqc/multiqc_plots/pdf/ranked_most_stable_genes_summary.pdf", "multiqc/multiqc_plots/pdf/ratio_zeros.pdf", "multiqc/multiqc_plots/pdf/skewness.pdf", "multiqc/multiqc_plots/png", - "multiqc/multiqc_plots/png/expression_distributions_top_stable_genes.png", + "multiqc/multiqc_plots/png/expression_distributions_most_stable_genes.png", "multiqc/multiqc_plots/png/gene_statistics.png", - "multiqc/multiqc_plots/png/ranked_top_stable_genes_summary.png", + "multiqc/multiqc_plots/png/id_mapping_stats-cnt.png", + "multiqc/multiqc_plots/png/id_mapping_stats-pct.png", + "multiqc/multiqc_plots/png/ranked_most_stable_genes_summary.png", "multiqc/multiqc_plots/png/ratio_zeros.png", "multiqc/multiqc_plots/png/skewness.png", "multiqc/multiqc_plots/svg", - "multiqc/multiqc_plots/svg/expression_distributions_top_stable_genes.svg", + "multiqc/multiqc_plots/svg/expression_distributions_most_stable_genes.svg", "multiqc/multiqc_plots/svg/gene_statistics.svg", - "multiqc/multiqc_plots/svg/ranked_top_stable_genes_summary.svg", + "multiqc/multiqc_plots/svg/id_mapping_stats-cnt.svg", + "multiqc/multiqc_plots/svg/id_mapping_stats-pct.svg", + "multiqc/multiqc_plots/svg/ranked_most_stable_genes_summary.svg", "multiqc/multiqc_plots/svg/ratio_zeros.svg", "multiqc/multiqc_plots/svg/skewness.svg", "multiqc/multiqc_report.html", @@ -2135,10 +2295,8 @@ "normalised/rnaseq.raw/quantile_normalised/rnaseq.raw.renamed.tpm.quant_norm.parquet", "normalised/rnaseq.raw/tpm", "normalised/rnaseq.raw/tpm/rnaseq.raw.renamed.tpm.csv", - "normfinder", - "normfinder/stability_values.normfinder.csv", "pipeline_info", - "pipeline_info/software_mqc_versions.yml", + "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", "statistics", "statistics/id_mapping_stats.csv", "statistics/ratio_zeros.csv", @@ -2147,17 +2305,8 @@ ], [ "all_genes_summary.csv:md5,03b370a040cac715b58c31865e7c8557", - "top_stable_genes_summary.csv:md5,03b370a040cac715b58c31865e7c8557", - "top_stable_genes_transposed_counts_filtered.csv:md5,9ee131e180ccaa879342af5873cdcf19", - "ratio_zeros.transposed.csv:md5,08ac547e8c31c2eefeead06c58f1fa3d", - "skewness.transposed.csv:md5,19618bb423fd67de0505f41710bff55f", - "stats_all_genes.csv:md5,38187d6b5b5069799021ae30356a7344", - "microarray.stats_all_genes.csv:md5,edd118adc5300d8bf0c6568742b2fa42", - "rnaseq.stats_all_genes.csv:md5,f3405a820aae655b84e1dda4aadc9074", - "ratio_zeros.txt:md5,ccb973ac5669e90633ff8285ef519341", - "skewness.txt:md5,0dd5498dd111ef24fcd4af8c07d1f3bf", - "gene_transcript_lengths.csv:md5,217aa7c1e227ce2f78a905138d8e5b39", - "stats_with_scores.csv:md5,cfd82cf2f9a27c4c97f8165c987a0da6", + "most_stable_genes_summary.csv:md5,03b370a040cac715b58c31865e7c8557", + "most_stable_genes_transposed_counts_filtered.csv:md5,9ee131e180ccaa879342af5873cdcf19", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", "all_genes_summary.csv:md5,03b370a040cac715b58c31865e7c8557", @@ -2180,25 +2329,23 @@ "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", "style.py:md5,b9f1207f06464e43c05e6e3912f12731", "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "Solanum_tuberosum.SolTub_3.0.62.gff3.gz:md5,cca99141f43d57d697f6df75de790e05", - "global_gene_id_mapping.csv:md5,60b6fbc3f18201059a3dadc8417a8940", - "global_gene_metadata.csv:md5,01a7ee010cb92a630dcd079530e7bdff", + "global_gene_id_mapping.csv:md5,187a86074197044846bb8565e122eb8e", + "global_gene_metadata.csv:md5,5ae2d701ca0cb6384d9e1e08a345e452", "microarray.normalised.renamed.csv:md5,6adb74d67379a5a3d3309b10a0c4bec5", "rnaseq.raw.renamed.csv:md5,aa22384ba73d180629add4e174c7f37d", "whole_gene_id_mapping.csv:md5,187a86074197044846bb8565e122eb8e", "whole_gene_metadata.csv:md5,5ae2d701ca0cb6384d9e1e08a345e452", "whole_design.csv:md5,70d6c2673e619ca52d2774fb3e368382", "rnaseq.raw.renamed.tpm.csv:md5,dde1d8ea5d271c4e0486c7ba0936a972", - "stability_values.normfinder.csv:md5,8cc6f5d4bd6432d6756f93a2a3257470", - "id_mapping_stats.csv:md5,e2d63d850b210a088111cd040d34f6e7", + "id_mapping_stats.csv:md5,ee400af7734b2226406fc7ba986dccfa", "ratio_zeros.csv:md5,d206e45c16e6bd13de75ea6d20bbd30d", - "skewness.csv:md5,14e2a88e24c48522b03d6cbe8f276023" + "skewness.csv:md5,9917b39dfe5ee6e680fa1783f8a096c4" ] ], "meta": { - "nf-test": "0.9.2", + "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-04T11:44:39.599729388" + "timestamp": "2025-12-15T16:52:41.144998246" } } \ No newline at end of file diff --git a/tests/modules/local/aggregate_results/main.nf.test.snap b/tests/modules/local/aggregate_results/main.nf.test.snap index 5b5a1939..554a48e6 100644 --- a/tests/modules/local/aggregate_results/main.nf.test.snap +++ b/tests/modules/local/aggregate_results/main.nf.test.snap @@ -26,10 +26,10 @@ "all_genes_summary": [ ], - "top_stable_genes_summary": [ + "most_stable_genes_summary": [ ], - "top_stable_genes_transposed_counts_filtered": [ + "most_stable_genes_transposed_counts_filtered": [ ] } @@ -67,10 +67,10 @@ "all_genes_summary": [ ], - "top_stable_genes_summary": [ + "most_stable_genes_summary": [ ], - "top_stable_genes_transposed_counts_filtered": [ + "most_stable_genes_transposed_counts_filtered": [ ] } diff --git a/tests/modules/local/expressionatlas/getaccessions/main.nf.test.snap b/tests/modules/local/expressionatlas/getaccessions/main.nf.test.snap index 51068350..54c55c29 100644 --- a/tests/modules/local/expressionatlas/getaccessions/main.nf.test.snap +++ b/tests/modules/local/expressionatlas/getaccessions/main.nf.test.snap @@ -1,45 +1,48 @@ { - "Solanum tuberosum no keyword": { + "Solanum tuberosum two keywords - microarray": { "content": [ { "0": [ "accessions.txt:md5,d41d8cd98f00b204e9800998ecf8427e" ], "1": [ - + "ok" ], "2": [ - "species_experiments.metadata.tsv:md5,68b329da9893e34099c7d8ad5cb9c940" + ], "3": [ + "species_experiments.metadata.tsv:md5,68b329da9893e34099c7d8ad5cb9c940" + ], + "4": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "python", "3.13.5" ] ], - "4": [ + "5": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "requests", "2.32.4" ] ], - "5": [ + "6": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "nltk", "3.9.1" ] ], - "6": [ + "7": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "pyyaml", "6.0.2" ] ], - "7": [ + "8": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "pandas", @@ -48,56 +51,62 @@ ], "accessions": [ "accessions.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ], + "sampling_quota": [ + "ok" ] } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-11-08T12:14:53.220718399" + "timestamp": "2025-12-11T10:59:26.647904855" }, - "Solanum tuberosum two keywords": { + "Solanum tuberosum no keyword": { "content": [ { "0": [ "accessions.txt:md5,d41d8cd98f00b204e9800998ecf8427e" ], "1": [ - + "ok" ], "2": [ - "species_experiments.metadata.tsv:md5,68b329da9893e34099c7d8ad5cb9c940" + ], "3": [ + "species_experiments.metadata.tsv:md5,68b329da9893e34099c7d8ad5cb9c940" + ], + "4": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "python", "3.13.5" ] ], - "4": [ + "5": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "requests", "2.32.4" ] ], - "5": [ + "6": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "nltk", "3.9.1" ] ], - "6": [ + "7": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "pyyaml", "6.0.2" ] ], - "7": [ + "8": [ [ "EXPRESSIONATLAS_GETACCESSIONS", "pandas", @@ -106,13 +115,16 @@ ], "accessions": [ "accessions.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ], + "sampling_quota": [ + "ok" ] } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-11-08T12:14:45.907461526" + "timestamp": "2025-12-11T10:59:38.871741855" } } \ No newline at end of file diff --git a/tests/modules/local/geo/getdata/main.nf.test.snap b/tests/modules/local/geo/getdata/main.nf.test.snap index ee7715de..79e5e54a 100644 --- a/tests/modules/local/geo/getdata/main.nf.test.snap +++ b/tests/modules/local/geo/getdata/main.nf.test.snap @@ -43,14 +43,17 @@ ], "design": [ + ], + "rejected": [ + ] } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-11-17T11:28:08.073509318" + "timestamp": "2025-12-11T15:57:14.702700023" }, "Drosophila simulans - No data found": { "content": [ @@ -96,14 +99,17 @@ ], "design": [ + ], + "rejected": [ + ] } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-11-17T11:27:41.033275401" + "timestamp": "2025-12-11T15:56:37.133179219" }, "Drosophila simulans - Mismatch in suppl data colnames / design": { "content": [ @@ -149,14 +155,17 @@ ], "design": [ + ], + "rejected": [ + ] } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-12-03T18:57:21.960035842" + "timestamp": "2025-12-11T15:57:39.828336686" }, "Accession does not exist": { "content": [ @@ -202,14 +211,17 @@ ], "design": [ + ], + "rejected": [ + ] } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-11-17T11:27:24.164943756" + "timestamp": "2025-12-11T15:56:24.827278212" }, "Drosophila simulans - Expression profiling by array": { "content": [ @@ -255,14 +267,17 @@ ], "design": [ + ], + "rejected": [ + ] } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-11-17T11:27:49.459757662" + "timestamp": "2025-12-11T15:56:49.691858912" }, "Drosophila simulans - Expression profiling by high throughput sequencing / Some raw counts found": { "content": [ @@ -308,14 +323,17 @@ ], "design": [ + ], + "rejected": [ + ] } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-11-17T11:27:58.80892872" + "timestamp": "2025-12-11T15:57:02.033741199" }, "Drosophila simulans - Only series suppl data but multiple species": { "content": [ @@ -361,14 +379,17 @@ ], "design": [ + ], + "rejected": [ + ] } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-12-03T18:57:10.8714341" + "timestamp": "2025-12-11T15:57:27.287712988" }, "Beta vulgaris - Small RNA of sugar beet in response to drought stress": { "content": [ @@ -414,13 +435,16 @@ ], "design": [ + ], + "rejected": [ + ] } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-11-17T11:27:16.009504875" + "timestamp": "2025-12-11T15:56:12.348217731" } } \ No newline at end of file diff --git a/tests/modules/local/gprofiler/idmapping/main.nf.test b/tests/modules/local/gprofiler/idmapping/main.nf.test index 2e4742ba..05444be6 100644 --- a/tests/modules/local/gprofiler/idmapping/main.nf.test +++ b/tests/modules/local/gprofiler/idmapping/main.nf.test @@ -48,7 +48,7 @@ nextflow_process { then { assertAll( - { assert !process.success } + { assert process.success } ) } } diff --git a/tests/modules/local/gprofiler/idmapping/main.nf.test.snap b/tests/modules/local/gprofiler/idmapping/main.nf.test.snap index c557b593..0f33a601 100644 --- a/tests/modules/local/gprofiler/idmapping/main.nf.test.snap +++ b/tests/modules/local/gprofiler/idmapping/main.nf.test.snap @@ -1,45 +1,4 @@ { - "ENtrez - No mapping found": { - "content": [ - { - "0": [ - - ], - "1": [ - - ], - "2": [ - [ - "GPROFILER_IDMAPPING", - "python", - "3.13.5" - ] - ], - "3": [ - [ - "GPROFILER_IDMAPPING", - "pandas", - "2.3.1" - ] - ], - "4": [ - [ - "GPROFILER_IDMAPPING", - "requests", - "2.32.4" - ] - ], - "metadata": [ - - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-11-20T18:15:42.597266116" - }, "ENSG - Mapping found": { "content": [ { @@ -80,34 +39,5 @@ "nextflow": "25.04.8" }, "timestamp": "2025-11-20T18:15:36.900376779" - }, - "Entrez - No mapping found": { - "content": [ - { - "0": [ - - ], - "1": [ - - ], - "2": [ - - ], - "3": [ - - ], - "4": [ - - ], - "metadata": [ - - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-12-03T18:57:58.500424421" } } \ No newline at end of file diff --git a/tests/subworkflows/local/download_public_datasets/main.nf.test b/tests/subworkflows/local/download_public_datasets/main.nf.test index e7759b47..54391b25 100644 --- a/tests/subworkflows/local/download_public_datasets/main.nf.test +++ b/tests/subworkflows/local/download_public_datasets/main.nf.test @@ -3,6 +3,7 @@ nextflow_workflow { name "Test Workflow DOWNLOAD_PUBLIC_DATASETS" script "subworkflows/local/download_public_datasets/main.nf" workflow "DOWNLOAD_PUBLIC_DATASETS" + tag "download_public_datasets" test("Beta vulgaris - Eatlas + GEO - all accessions") { @@ -12,7 +13,7 @@ nextflow_workflow { } workflow { """ - input[0] = channel.value( params.species.split(' ').join('_') ) + input[0] = params.species.split(' ').join('_') input[1] = channel.fromList(['E-MTAB-8187', 'GSE107627', 'GSE114968', 'GSE135555', 'GSE205413', 'GSE269454', 'GSE281272', 'GSE55951', 'GSE79526', 'GSE92859']) """ } @@ -33,7 +34,7 @@ nextflow_workflow { } workflow { """ - input[0] = channel.value( params.species.split(' ').join('_') ) + input[0] = params.species.split(' ').join('_') input[1] = channel.fromList(['E-MTAB-8187']) """ } @@ -46,25 +47,6 @@ nextflow_workflow { } - test("No accessions") { - when { - params { - species = 'beta vulgaris' - } - workflow { - """ - input[0] = channel.value( params.species.split(' ').join('_') ) - input[1] = channel.empty() - """ - } - } - - then { - assert workflow.success - assert snapshot(workflow.out).match() - } - - } } diff --git a/tests/subworkflows/local/download_public_datasets/main.nf.test.snap b/tests/subworkflows/local/download_public_datasets/main.nf.test.snap index fc907c93..f31464a9 100644 --- a/tests/subworkflows/local/download_public_datasets/main.nf.test.snap +++ b/tests/subworkflows/local/download_public_datasets/main.nf.test.snap @@ -23,27 +23,10 @@ } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-12-04T10:36:40.174934839" - }, - "No accessions": { - "content": [ - { - "0": [ - - ], - "datasets": [ - - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-12-04T10:36:44.555546839" + "timestamp": "2025-12-15T21:46:42.719832457" }, "Beta vulgaris - Eatlas + GEO - all accessions": { "content": [ @@ -52,14 +35,18 @@ [ { "dataset": "E_MTAB_8187_rnaseq", - "design": "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf" + "design": "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", + "normalised": false, + "platform": "rnaseq" }, "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1" ], [ { "dataset": "GSE55951_GPL18429", - "design": "GSE55951_GPL18429.microarray.normalised.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d" + "design": "GSE55951_GPL18429.microarray.normalised.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d", + "normalised": true, + "platform": "microarray" }, "GSE55951_GPL18429.microarray.normalised.counts.csv:md5,18fd2d728ad2ec5cb78f994f73375144" ] @@ -68,14 +55,18 @@ [ { "dataset": "E_MTAB_8187_rnaseq", - "design": "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf" + "design": "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", + "normalised": false, + "platform": "rnaseq" }, "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1" ], [ { "dataset": "GSE55951_GPL18429", - "design": "GSE55951_GPL18429.microarray.normalised.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d" + "design": "GSE55951_GPL18429.microarray.normalised.design.csv:md5,f4872dff0edbe441d1600ffe2b67a25d", + "normalised": true, + "platform": "microarray" }, "GSE55951_GPL18429.microarray.normalised.counts.csv:md5,18fd2d728ad2ec5cb78f994f73375144" ] @@ -83,9 +74,9 @@ } ], "meta": { - "nf-test": "0.9.2", + "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-04T13:05:11.570641957" + "timestamp": "2025-12-15T21:46:26.386631545" } } \ No newline at end of file diff --git a/tests/subworkflows/local/get_public_accessions/main.nf.test b/tests/subworkflows/local/get_public_accessions/main.nf.test index 01f11274..d1d0e64b 100644 --- a/tests/subworkflows/local/get_public_accessions/main.nf.test +++ b/tests/subworkflows/local/get_public_accessions/main.nf.test @@ -3,7 +3,7 @@ nextflow_workflow { name "Test Workflow GET_PUBLIC_ACCESSIONS" script "subworkflows/local/get_public_accessions/main.nf" workflow "GET_PUBLIC_ACCESSIONS" - + tag "get_public_accessions" test("Fetch eatlas accessions without keywords") { @@ -26,7 +26,7 @@ nextflow_workflow { workflow { """ - input[0] = channel.value( params.species.split(' ').join('_') ) + input[0] = params.species.split(' ').join('_') input[1] = params.skip_fetch_eatlas_accessions input[2] = params.fetch_geo_accessions input[3] = params.platform @@ -72,7 +72,7 @@ nextflow_workflow { workflow { """ - input[0] = channel.value( params.species.split(' ').join('_') ) + input[0] = params.species.split(' ').join('_') input[1] = params.skip_fetch_eatlas_accessions input[2] = params.fetch_geo_accessions input[3] = params.platform @@ -118,7 +118,7 @@ nextflow_workflow { workflow { """ - input[0] = channel.value( params.species.split(' ').join('_') ) + input[0] = params.species.split(' ').join('_') input[1] = params.skip_fetch_eatlas_accessions input[2] = params.fetch_geo_accessions input[3] = params.platform @@ -163,7 +163,7 @@ nextflow_workflow { workflow { """ - input[0] = channel.value( params.species.split(' ').join('_') ) + input[0] = params.species.split(' ').join('_') input[1] = params.skip_fetch_eatlas_accessions input[2] = params.fetch_geo_accessions input[3] = params.platform diff --git a/tests/subworkflows/local/get_public_accessions/main.nf.test.snap b/tests/subworkflows/local/get_public_accessions/main.nf.test.snap index e6970aff..85090531 100644 --- a/tests/subworkflows/local/get_public_accessions/main.nf.test.snap +++ b/tests/subworkflows/local/get_public_accessions/main.nf.test.snap @@ -5,22 +5,20 @@ "0": [ "E-GEOD-61690", "E-MTAB-552", - "E-MTAB-8187", "E-PROT-138" ], "accessions": [ "E-GEOD-61690", "E-MTAB-552", - "E-MTAB-8187", "E-PROT-138" ] } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-12-04T10:38:22.46774778" + "timestamp": "2025-12-11T11:13:39.15754592" }, "No GEO + accessions provided": { "content": [ @@ -28,83 +26,53 @@ "0": [ "E-GEOD-61690", "E-MTAB-552", - "E-MTAB-8187", "E-PROT-138" ], "accessions": [ "E-GEOD-61690", "E-MTAB-552", - "E-MTAB-8187", "E-PROT-138" ] } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-12-04T10:38:15.084508905" + "timestamp": "2025-12-11T11:13:13.734553188" }, - "Fetch public accessions without keywords": { + "Fetch eatlas accessions without keywords": { "content": [ { "0": [ - "E-MTAB-8187", - "GSE107627", - "GSE114968", - "GSE135555", - "GSE205413", - "GSE269454", - "GSE281272", - "GSE55951", - "GSE79526", - "GSE92859" + ], "accessions": [ - "E-MTAB-8187", - "GSE107627", - "GSE114968", - "GSE135555", - "GSE205413", - "GSE269454", - "GSE281272", - "GSE55951", - "GSE79526", - "GSE92859" + ] } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-12-04T10:37:50.665649081" + "timestamp": "2025-12-11T11:12:37.434155203" }, "Fetch public accessions with keywords": { "content": [ { "0": [ - "E-MTAB-8187", - "GSE107627", - "GSE114968", - "GSE269454", - "GSE281272", - "GSE79526" + ], "accessions": [ - "E-MTAB-8187", - "GSE107627", - "GSE114968", - "GSE269454", - "GSE281272", - "GSE79526" + ] } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-12-04T10:38:07.290638456" + "timestamp": "2025-12-11T14:12:14.94130631" } } \ No newline at end of file diff --git a/tests/workflows/nextflow.config b/tests/workflows/nextflow.config deleted file mode 100644 index 20ab42b2..00000000 --- a/tests/workflows/nextflow.config +++ /dev/null @@ -1,3 +0,0 @@ -params { - outdir = "results" -} diff --git a/tests/workflows/stableexpression.nf.test b/tests/workflows/stableexpression.nf.test deleted file mode 100644 index a7073c16..00000000 --- a/tests/workflows/stableexpression.nf.test +++ /dev/null @@ -1,198 +0,0 @@ -nextflow_workflow { - - name "Test Workflow STABLEEXPRESSION" - script "workflows/stableexpression.nf" - workflow "STABLEEXPRESSION" - config "./nextflow.config" - tag "workflow" - - test("Two Expression Atlas accessions provided") { - - tag "workflow_eatlas_accessions" - - when { - params { - species = "beta vulgaris" - eatlas_accessions = "E-MTAB-552,E-GEOD-61690" - } - workflow { - """ - input[0] = channel.empty() - """ - } - } - - then { - assert workflow.success - with(workflow.out.multiqc_report[0]) { - assertAll( - { assert path(get(0)).readLines().any { it.contains('MultiQC: A modular tool') } }, - { assert path(get(0)).readLines().any { it.contains('Data was processed using nf-core/stableexpression') } } - ) - } - } - } - - test("Two Expression Atlas no keyword (whole species)") { - - tag "workflow_eatlas_no_kw" - - when { - params { - species = "beta vulgaris" - fetch_eatlas_accessions = true - } - workflow { - """ - input[0] = channel.empty() - """ - } - } - - then { - assert workflow.success - with(workflow.out.multiqc_report[0]) { - assertAll( - { assert path(get(0)).readLines().any { it.contains('MultiQC: A modular tool') } }, - { assert path(get(0)).readLines().any { it.contains('Data was processed using nf-core/stableexpression') } } - ) - } - } - } - - test("Two Expression Atlas keywords provided") { - - tag "workflow_eatlas_kw" - - when { - params { - species = "beta vulgaris" - keywords = "potato,stress" - } - workflow { - """ - input[0] = channel.empty() - """ - } - } - - then { - assert workflow.success - with(workflow.out.multiqc_report[0]) { - assertAll( - { assert path(get(0)).readLines().any { it.contains('MultiQC: A modular tool') } }, - { assert path(get(0)).readLines().any { it.contains('Data was processed using nf-core/stableexpression') } } - ) - } - } - } - - test("Two Expression Atlas accessions provided - normalisation with EdgeR") { - - tag "workflow_eatlas_accessions_edger" - - when { - params { - species = "solanum tuberosum" - eatlas_accessions = "E-MTAB-552,E-GEOD-61690" - normalisation_method = "edger" - } - workflow { - """ - input[0] = channel.empty() - """ - } - } - - then { - assert workflow.success - with(workflow.out.multiqc_report[0]) { - assertAll( - { assert path(get(0)).readLines().any { it.contains('MultiQC: A modular tool') } }, - { assert path(get(0)).readLines().any { it.contains('Data was processed using nf-core/stableexpression') } } - ) - } - } - } - - test("Test workflow - common accession (E-MTAB-552) between manual and auto") { - - tag "workflow_accession_E-MTAB-552" - - when { - params { - species = "solanum tuberosum" - eatlas_accessions = "E-MTAB-552,E-GEOD-61690" - keywords = "phloem" - } - workflow { - """ - input[0] = channel.empty() - """ - } - } - - then { - assert workflow.success - with(workflow.out.multiqc_report[0]) { - assertAll( - { assert path(get(0)).readLines().any { it.contains('MultiQC: A modular tool') } }, - { assert path(get(0)).readLines().any { it.contains('Data was processed using nf-core/stableexpression') } } - ) - } - } - - } - - test("Accessions only") { - - tag "workflow_accessions_only" - - when { - params { - species = "beta vulgaris" - accessions_only = true - } - workflow { - """ - input[0] = channel.empty() - """ - } - } - - then { - assert workflow.success - } - } - - test("Included and excluded accessions") { - - tag "workflow_included_excluded_accessions" - - when { - params { - species = "solanum tuberosum" - eatlas_accessions = "E-MTAB-552,E-GEOD-61690" - excluded_eatlas_accessions = "E-MTAB-4251" - eatlas_accessions_file = file( '$projectDir/tests/test_data/misc/accessions_to_include.txt.txt', checkIfExists: true) - excluded_eatlas_accessions_file = file( '$projectDir/tests/test_data/misc/excluded_accessions.txt', checkIfExists: true) - } - workflow { - """ - input[0] = channel.empty() - """ - } - } - - then { - assert workflow.success - with(workflow.out.multiqc_report[0]) { - assertAll( - { assert path(get(0)).readLines().any { it.contains('MultiQC: A modular tool') } }, - { assert path(get(0)).readLines().any { it.contains('Data was processed using nf-core/stableexpression') } } - ) - } - } - } - -} diff --git a/tests/workflows/stableexpression.nf.test.snap b/tests/workflows/stableexpression.nf.test.snap deleted file mode 100644 index 20a4fb3d..00000000 --- a/tests/workflows/stableexpression.nf.test.snap +++ /dev/null @@ -1,113 +0,0 @@ -{ - "Expression Atlas accession - two output datasets": { - "content": [ - [ - "stats_most_stable_genes.csv:md5,24892b65e3569872371f79c3afcb863d" - ], - [ - "stats_all_genes.csv:md5,7867fdf5168199d20dc16c47006ef8ce" - ], - [ - "count_summary.csv:md5,3ce9f092b0863842fc08db3dd0bc947e" - ] - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "24.10.3" - }, - "timestamp": "2025-01-09T10:46:57.423487611" - }, - "Two Expression Atlas keywords provided": { - "content": [ - [ - "stats_most_stable_genes.csv:md5,a06dceee03c9d413d6c8ec22329f5262" - ], - [ - "stats_all_genes.csv:md5,ef718e06c30989d1e8341cbd2f5b8b44" - ], - [ - "count_summary.csv:md5,42fd89bef2f7a1e6f43ae91bc443e584" - ] - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "24.10.3" - }, - "timestamp": "2025-01-09T10:55:01.778957099" - }, - "Two Expression Atlas no keyword (whole species)": { - "content": [ - [ - "stats_most_stable_genes.csv:md5,efb81b25c691f8758da1c1f44f315016" - ], - [ - "stats_all_genes.csv:md5,0ec1c7005ad2c0ef43d4b43787293cfc" - ], - [ - "count_summary.csv:md5,7bffef35d4d295abd25391ff42def670" - ] - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "24.10.3" - }, - "timestamp": "2025-01-09T10:50:56.83921757" - }, - "Test workflow - common accession (E-MTAB-552) between manual and auto": { - "content": [ - [ - "stats_most_stable_genes.csv:md5,b311ed6e0abab127f180ece1737c4835" - ], - [ - "stats_all_genes.csv:md5,a1ee2aa140ec3f2968b662e46e65a7ae" - ], - [ - "count_summary.csv:md5,ff830820097999847752e1e553628c18" - ] - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "24.10.3" - }, - "timestamp": "2025-01-09T15:21:40.286621572" - }, - "Two Expression Atlas accessions provided - normalisation with EdgeR": { - "content": [ - [ - "stats_most_stable_genes.csv:md5,9af4528cb8ea2695e0aff66c1ea464a7" - ], - [ - "stats_all_genes.csv:md5,7339175b11aeaeac1a9552a294f9f7b3" - ], - [ - "count_summary.csv:md5,06ba6597605876cc7152c67db0545075" - ] - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "24.10.3" - }, - "timestamp": "2025-01-09T10:56:26.563087788" - }, - "Two Expression Atlas accessions provided": { - "content": [ - { - "0": [ - [ - "multiqc_report.html:md5,eb870e24fa1d767bdddc89be9add54b6" - ] - ], - "multiqc_report": [ - [ - "multiqc_report.html:md5,eb870e24fa1d767bdddc89be9add54b6" - ] - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.0" - }, - "timestamp": "2025-05-11T22:54:00.699548935" - } -} \ No newline at end of file From d9b58e74f677608d13e6ca31b2e89ec0b0f81c6e Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 15 Dec 2025 23:25:26 +0100 Subject: [PATCH 235/258] improve doc --- README.md | 3 +-- docs/output.md | 7 +----- docs/troubleshooting.md | 52 +++++++++++++++++++++++++++++++++-------- docs/usage.md | 8 +------ nextflow_schema.json | 4 ++-- tests/default.nf.test | 4 ++-- 6 files changed, 49 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 846483b2..12123d78 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ It takes as main inputs : **Use cases**: - **find the most suitable genes as RT-qPCR reference genes for a specific species (and optionally specific conditions)** -- download all Expression Atlas and NCBI GEO datasets for a species +- download all Expression Atlas and / or NCBI GEO datasets for a species (and optionally keywords) ## Basic usage @@ -44,7 +44,6 @@ To search the most stable genes in a species considering all public datasets, si ```bash nextflow run nf-core/stableexpression \ - -r dev \ -profile \ --species \ --outdir diff --git a/docs/output.md b/docs/output.md index 4eeb6c43..a88d9667 100644 --- a/docs/output.md +++ b/docs/output.md @@ -32,14 +32,9 @@ The pipeline is built using [Nextflow](https://www.nextflow.io/) and processes d 4. Data normalisation -- Normalize RNAseq raw data using [DESeq2](https://bioconductor.org/packages/release/bioc/html/DESeq2.html) or [EdgeR](https://bioconductor.org/packages/release/bioc/html/edgeR.html) +- Normalize RNAseq raw data using TPM (necessitates downloading the corresponding genome and computing transcript lengths) or CPM. - Perform quantile normalisation on each dataset separately using [scikit-learn](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.quantile_transform.html) -5. Data cleaning - -- Get statistics for each sample in each dataset -- Remove samples that diverge too much from the expected normalised profile - 6. Merge all data 7. Compute base statistics for each gene, platform-wide and for each platform (RNAseq and microarray) 8. Compute stability scoring diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 034ab8d6..dcef6570 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,18 +1,56 @@ # nf-core/stableexpression: Troubleshooting +## Error 139 on macOS + +If you are running the pipeline on macOS with containers (`docker`, `apptainer`, `singularity`, ...), you may encounter issues like: + +``` +NOTE: Process `NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:ID_MAPPING:CLEAN_GENE_IDS ()` terminated with an error exit status (139) -- Execution is retried (1) +``` +eventually leading to pipeline failure. + +This is likely due to the python polars library not being compatible with macOS when run inside a container. + +You should run the pipeline with `-profile micromamba` or `-profile conda`. + ## Ǹo dataset found -For species that are not on Expression Atlas and that do not have microarray data on NCBI data, the pipeline will not be able to find suitable datasets and will log the following message: +For species that are not on Expression Atlas, the pipeline will not be able to find suitable datasets and will log the following message: ``` WARN: No dataset found. Please note that for the moment only Microarray count datasets are fetched from NCBI GEO. You can check at https://www.ncbi.nlm.nih.gov/gds if there are raw RNA-seq count datasets for this species. ``` -You may want to check if there are any raw RNA-seq count datasets available for this species on [NCBI GEO](https://www.ncbi.nlm.nih.gov/gds). You can then relaunch the pipeline by providing your own prepared count datasets. +>[!TIP] +>You can first try to have the pipeline fetch suitable datasets from NCBI GEO by providing the `--fetch_geo_accessions` flag. + +In case no datasets are found, you'll have to find a way to get count datasets and to prepare them for the pipeline. +A good start is to check in the folder `/public_data/geo/datasets/` if there are `rejected` subfolders. Such subfolders contain datasets that were downloaded (together with their experimental design) but failed to pass checks. Quite often, some of them be manually reprocessed to be suitable for the pipeline. + +Finally, you may want to check by yourself on [NCBI GEO](https://www.ncbi.nlm.nih.gov/gds). + +Alternatively, some public websites contain expression datasets that may be suitable for the pipeline, such as: +- [Bgee](https://www.bgee.org/) ## Not enough memory +The pipeline limits the number of downloaded datasets to a certain number in order to limit RAM usage, especially for `homo sapiens`. + +However, on small computers, the limit may be too permissive and lead to RAM overhead. You can reduce the number of datasets downloaded by setting the `--random_sampling_size` to a lower value. + +## Why do I get only a fraction of the public datasets available on Expression Atlas or NCBI GEO? Give them back! + +To reduce the RAM overhead, the pipeline selects randomly a certain number of datasets, based on the number of samples they contain. To increase the number of collected datasets, you can increase the `--random_sampling_size` parameter. + +[!TIP] +>A seed is also set in order to make the runs reproducible. You can change the subset of chosen datasets by changing the `--random_sampling_seed`. + + +## The pipeline fails because it does not find a genome for the specified species + +If you are working on a species for which there is no genome assembly available on Ensembl, you cannot (for now) perform TPM normalisation. A fallback is to use CPM normalisation by setting `--normalisation_method cpm`. It will introduce a small bias towards long genes, but this should not result in big changes. + ## Java heap space In some cases, in particular when running the pipeline on a very large number of datasets (such as for `Homo sapiens`), the Nextflow Java virtual machines can start to request a large amount of memory. You may happen to see the following error: @@ -21,14 +59,8 @@ In some cases, in particular when running the pipeline on a very large number of java.lang.OutOfMemoryError: Java heap space ``` -We recommend adding the following line to your environment to limit this (typically in `~/.bashrc` or `~./bash_profile`): - -```bash -NXF_OPTS='-Xms1g -Xmx4g' -``` - -or running the pipeline with: +We recommend to increase the memory available to Java: ```bash -NXF_OPTS='-Xms1g -Xmx4g' nextflow run nf-core/stableexpression ... +export NXF_OPTS='-Xms1g -Xmx4g' ``` diff --git a/docs/usage.md b/docs/usage.md index 36e47f82..a08ca211 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -16,7 +16,6 @@ This pipeline fetches Expression Atlas and GEO accessions for the provided speci ```bash nextflow run nf-core/stableexpression \ - -r dev \ -profile \ --species \ --outdir @@ -31,7 +30,6 @@ You can provide keywords to restrict downloaded datasets to specific conditions. ```bash nextflow run nf-core/stableexpression \ - -r dev \ -profile \ --species \ --keywords @@ -52,7 +50,6 @@ In this case, you can provide them directly to the pipeline. ```bash nextflow run nf-core/stableexpression \ - -r dev \ -profile \ --species \ --skip_fetch_eatlas_accessions \ @@ -73,7 +70,6 @@ In case you do not know which accessions you want but you would like to control ```bash nextflow run nf-core/stableexpression \ - -r dev \ -profile \ --species \ --accessions_only \ @@ -158,7 +154,6 @@ Now run the pipeline with: ```bash nextflow run nf-core/stableexpression \ - -r dev \ -profile \ --species \ --datasets \ @@ -170,7 +165,7 @@ nextflow run nf-core/stableexpression \ > The `--skip_fetch_eatlas_accessions` parameter is supplied here to show how to analyse **only your own dataset**. You may remove this parameter if you want to mix you dataset(s) with public ones. > [!IMPORTANT] -> By default, the pipeline tries to map gene IDs to NCBI Entrez Gene IDs. **All genes that cannot be mapped are discarded from the analysis**. This ensures that all genes are named the same between datasets and allows comparing multiple datasets with each other. If you are confident that your genes have the same name between your different datasets or if you think that your gene IDs won't be mapped properly, you can disable this mapping by adding the `--skip_id_mapping` parameter. In such case, you may supply your own gene id mapping file and gene metadata file with the `--gene_id_mapping` and `--gene_metadata` parameters respectively. See [next section](#5-custom-gene-id-mapping-and-metadata) for further details. +> By default, the pipeline tries to map gene IDs to NCBI Entrez Gene IDs. **All genes that cannot be mapped are discarded from the analysis**. This ensures that all genes are named the same between datasets and allows comparing multiple datasets with each other. If you are confident that your genes have the same name between your different datasets or if you think on the contrary that your gene IDs just won't be mapped properly, you can disable this mapping by adding the `--skip_id_mapping` parameter. In such case, you may supply your own gene id mapping file and gene metadata file with the `--gene_id_mapping` and `--gene_metadata` parameters respectively. See [next section](#5-custom-gene-id-mapping-and-metadata) for further details. > [!TIP] > You can check if your gene IDs can be mapped using the [g:Profiler server](https://biit.cs.ut.ee/gprofiler/convert). @@ -181,7 +176,6 @@ You can supply your own gene id mapping file and optionally gene metadata with: ```bash nextflow run nf-core/stableexpression \ - -r dev \ -profile \ --species \ --datasets \ diff --git a/nextflow_schema.json b/nextflow_schema.json index 8ef0cbe5..e9616b1c 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -255,10 +255,10 @@ "properties": { "random_sampling_size": { "type": "integer", - "description": "Number of public dataset accessions (Expression Atlas + GEO) to sample randomly before downloading.", + "description": "Number of public dataset samples to choose randomly before downloading.", "fa_icon": "fas fa-sort-numeric-up-alt", "minimum": 1, - "help_text": "When dealing with species for which there is a large number (eg. >1000) of public datasets, users may encounter RAM issues (eg. errors with `137` exit codes). In such cases, it is recommended to sample a random subset of these datasets to reduce the computational load. The subsampling is performed after getting proper accessions from Expression Atlas and GEO, AFTER excluding unwanted accessions (obtained through `--excluded_accessions` or `--excluded_accession_file`) but BEFORE including user provided accessions (obtained through `--accessions` or `--accession_file`)." + "help_text": "When dealing with species for which there is a large number (eg. >10000) of samples considering all the downloaded datasets, users may encounter RAM issues (eg. errors with `137` exit codes). In such cases, it is recommended to sample a random subset of these datasets to reduce the computational load. A first subsampling is performedduring the search for Expression Atlas accessions. In case there is still room for datasets and if the `--fetch_geo_accessions` flag was set, a second ssubsampling is performed during the search for NCBI GEO accessions." }, "random_sampling_seed": { "type": "integer", diff --git a/tests/default.nf.test b/tests/default.nf.test index 5900b86c..b33b3359 100644 --- a/tests/default.nf.test +++ b/tests/default.nf.test @@ -296,8 +296,8 @@ nextflow_pipeline { } } - test("-profile test_full") { - tag "test_full" + test("-profile test_bigger_with_genorm") { + tag "test_bigger_with_genorm" when { params { From e30e73628386e4317f2739677b21cfda6acb1523 Mon Sep 17 00:00:00 2001 From: Olivier Date: Tue, 16 Dec 2025 15:20:01 +0100 Subject: [PATCH 236/258] fix test failures --- bin/merge_counts.py | 33 +- modules/local/gprofiler/idmapping/main.nf | 4 +- .../local/download_public_datasets/main.nf | 2 +- .../main.nf | 10 +- tests/default.nf.test.snap | 490 +++++++++--------- .../local/gprofiler/idmapping/main.nf.test | 15 +- .../main.nf.test.snap | 12 +- 7 files changed, 298 insertions(+), 268 deletions(-) diff --git a/bin/merge_counts.py b/bin/merge_counts.py index 84f78c9b..7ec9352b 100755 --- a/bin/merge_counts.py +++ b/bin/merge_counts.py @@ -3,6 +3,8 @@ # Written by Olivier Coen. Released under the MIT license. import argparse +import hashlib +import json import logging from functools import reduce from operator import attrgetter @@ -89,6 +91,30 @@ def get_count_columns(df: pl.DataFrame) -> list[str]: return df.select(pl.exclude(config.GENE_ID_COLNAME)).columns +def reproducible_hash(tpl: tuple[str]) -> str: + """ + Return a deterministic MD5 hash for the given tuple. + + Steps: + 1. Convert the tuple (and any nested structures) to a canonical JSON string. + - `sort_keys=True` guarantees that dictionaries are ordered consistently. + - `separators=(',', ':')` removes unnecessary whitespace. + 2. Encode the string as UTF‑8 bytes. + 3. Feed the bytes to hashlib.md5 and return the hex digest. + + The result is a 64‑character hexadecimal string that will be identical + across Python runs, machines, and even different Python versions + (provided the data types are JSON‑compatible). + """ + # Canonical JSON representation + canonical_str = json.dumps(tpl, sort_keys=True, separators=(",", ":")) + # Encode to bytes + data_bytes = canonical_str.encode("utf-8") + # Compute MD5 + hash_obj = hashlib.md5(data_bytes) + return hash_obj.hexdigest() + + def get_counts(files: list[Path]) -> pl.DataFrame: """Get all count data from a list of files. @@ -98,6 +124,11 @@ def get_counts(files: list[Path]) -> pl.DataFrame: logger.info("Parsing counts") dfs = get_valid_dfs(files) + # sorting dataframes by a hash on column names + # this is crucial for consistent output of the script + # in case multiple files have the same name + dfs.sort(key=lambda df: reproducible_hash(tuple(df.columns))) + # joining all count files logger.info( f"Joining count files recursively on the {config.GENE_ID_COLNAME} column" @@ -108,7 +139,7 @@ def get_counts(files: list[Path]) -> pl.DataFrame: # casting count columns to Float64 # casting gene id column to Stringcount_files # casting nans to nulls - logger.info("Cleaning mergeed dataframe") + logger.info("Cleaning merged dataframe") return merged_df.select( [pl.col(config.GENE_ID_COLNAME).cast(pl.String)] + [pl.col(column).cast(pl.Float64) for column in count_columns] diff --git a/modules/local/gprofiler/idmapping/main.nf b/modules/local/gprofiler/idmapping/main.nf index 2e411fec..b7f2b2db 100644 --- a/modules/local/gprofiler/idmapping/main.nf +++ b/modules/local/gprofiler/idmapping/main.nf @@ -1,5 +1,4 @@ process GPROFILER_IDMAPPING { - label 'process_medium' tag "${species} IDs to ${gprofiler_target_db}" @@ -7,7 +6,7 @@ process GPROFILER_IDMAPPING { errorStrategy = { if (task.exitStatus == 100 ) { log.error("Could not map gene IDs to ${gprofiler_target_db} database.") - 'finish' + 'terminate' } else if (task.exitStatus in ((130..145) + 104 + 175) && task.attempt <= 10) { // OOM & related errors; should be retried as long as memory does not fit sleep(Math.pow(2, task.attempt) * 200 as long) 'retry' @@ -38,7 +37,6 @@ process GPROFILER_IDMAPPING { script: """ - # intercepting exit code 100 gprofiler_map_ids.py \\ --gene-ids $gene_id_file \\ --species "$species" \\ diff --git a/subworkflows/local/download_public_datasets/main.nf b/subworkflows/local/download_public_datasets/main.nf index d0b1cfe5..6ea3f11f 100644 --- a/subworkflows/local/download_public_datasets/main.nf +++ b/subworkflows/local/download_public_datasets/main.nf @@ -58,7 +58,7 @@ workflow DOWNLOAD_PUBLIC_DATASETS { ch_datasets = groupFilesByDatasetId( ch_design, ch_counts ) // adding normalisation state in the meta - augmentMetadata( ch_datasets ) + ch_datasets = augmentMetadata( ch_datasets ) emit: datasets = ch_datasets diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index f8de5372..527b5fad 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -378,14 +378,16 @@ def augmentMetadata( ch_files ) { meta, file -> def norm_state = getNthPartFromEnd(file.name, 3) if ( norm_state == 'raw' ) { - meta.normalised = false + normalised = false } else if ( norm_state == 'normalised' ) { - meta.normalised = true + normalised = true } else { error("Invalid normalisation state: ${norm_state}") } - meta.platform = getNthPartFromEnd(file.name, 4) - [meta, file] + + platform = getNthPartFromEnd(file.name, 4) + new_meta = meta + [normalised: normalised, platform: platform] + [new_meta, file] } } diff --git a/tests/default.nf.test.snap b/tests/default.nf.test.snap index cc34c9cb..21d562ae 100644 --- a/tests/default.nf.test.snap +++ b/tests/default.nf.test.snap @@ -848,12 +848,12 @@ "warnings" ], [ - "all_genes_summary.csv:md5,467ef2a492f57390a815072fd27d4955", + "all_genes_summary.csv:md5,22e424fe9e669c8c38997f1f44fc43ac", "most_stable_genes_summary.csv:md5,049a1fae30bafa22cd91fa906bb33164", - "most_stable_genes_transposed_counts_filtered.csv:md5,f5e71d68baa7976a9781a2315b2fe22f", + "most_stable_genes_transposed_counts_filtered.csv:md5,7f70c275cedb0ec9ca9775b14c051105", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_genes_summary.csv:md5,467ef2a492f57390a815072fd27d4955", + "all_genes_summary.csv:md5,22e424fe9e669c8c38997f1f44fc43ac", "whole_design.csv:md5,cc24405dce8d22b93b9999a2287113ef", "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", @@ -924,7 +924,7 @@ "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-15T16:37:09.430245577" + "timestamp": "2025-12-16T14:07:29.658396871" }, "-profile test_skip_id_mapping": { "content": [ @@ -1191,9 +1191,9 @@ "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", "accessions.txt:md5,63a651d9df354aef24400cebe56dd5ec", - "geo_all_datasets.metadata.tsv:md5,465212e30807744519ad3999db01a318", + "geo_all_datasets.metadata.tsv:md5,9412aab7c5031f41910d6dce03797784", "geo_rejected_datasets.metadata.tsv:md5,0a66c9d519b4590e48b04e4c37d66416", - "geo_selected_datasets.metadata.tsv:md5,dd37fb452bd34dda97ac5b3c33516519", + "geo_selected_datasets.metadata.tsv:md5,6762daf0ab2824cf872a7de208b6e81d", "warning_reason.txt:md5,603e30b732b7a6b501b59adf9d0e8837", "id_mapping_stats.csv:md5,7c20b1e561989fcd2ce038c4a061caa5", "ratio_zeros.csv:md5,9794647ae1d7c87ec212c0c12b658d4e", @@ -1205,7 +1205,7 @@ "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-15T16:44:28.441072301" + "timestamp": "2025-12-16T13:56:28.230895486" }, "-profile test_accessions_only": { "content": [ @@ -1539,9 +1539,77 @@ }, "timestamp": "2025-12-15T16:48:56.125523436" }, - "-profile test_download_only": { + "-profile test_bigger_with_genorm": { "content": [ { + "AGGREGATE_RESULTS": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "CLEAN_GENE_IDS": { + "pandas": "2.3.3", + "polars": "1.35.2", + "python": "3.14.0" + }, + "COLLECT_GENE_IDS": { + "pandas": "2.3.3", + "python": "3.14.0", + "tqdm": "4.67.1" + }, + "COLLECT_STATISTICS": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "COMPUTE_BASE_STATISTICS": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_BASE_STATISTICS_FOR_RNASEQ": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_DATASET_STATISTICS": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "COMPUTE_GENE_TRANSCRIPT_LENGTHS": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "COMPUTE_M_MEASURE": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_STABILITY_SCORES": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_TPM": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "CROSS_JOIN": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "DASH_APP": { + "python": "3.13.8", + "dash": "3.2.0", + "dash-extensions": "2.0.4", + "dash-mantine-components": "2.3.0", + "dash-ag-grid": "32.3.2", + "polars": "1.35.0", + "pandas": "2.3.3", + "pyarrow": "22.0.0", + "scipy": "1.16.3" + }, + "DOWNLOAD_ENSEMBL_ANNOTATION": { + "bs4": "4.14.2", + "pandas": "2.3.3", + "python": "3.14.0", + "requests": "2.32.5", + "tqdm": "4.67.1" + }, "EXPRESSION_ATLAS": { "ExpressionAtlas": "1.30.0", "R": "4.3.3 (2024-02-29)", @@ -1551,65 +1619,56 @@ "pyyaml": "6.0.2", "requests": "2.32.4" }, + "EXPRESSION_RATIO": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "GET_CANDIDATE_GENES": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "GPROFILER_IDMAPPING": { + "pandas": "2.3.1", + "python": "3.13.5", + "requests": "2.32.4" + }, + "MAKE_CHUNKS": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "MERGE_ALL_COUNTS": { + "polars": "1.34.0", + "python": "3.14.0", + "tqdm": "4.67.1" + }, + "MERGE_RNASEQ_COUNTS": { + "polars": "1.34.0", + "python": "3.14.0", + "tqdm": "4.67.1" + }, + "NORMFINDER": { + "polars": "1.33.1", + "python": "3.13.7" + }, + "QUANTILE_NORMALISATION": { + "pandas": "2.2.3", + "pyarrow": "19.0.0", + "python": "3.12.8", + "scikit-learn": "1.6.1" + }, + "RATIO_STANDARD_VARIATION": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "RENAME_GENE_IDS": { + "pandas": "2.3.3", + "polars": "1.35.2", + "python": "3.14.0" + }, "Workflow": { "nf-core/stableexpression": "v1.0dev" } }, - [ - "errors", - "multiqc", - "multiqc/multiqc_data", - "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_eatlas_all_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", - "multiqc/multiqc_data/multiqc_software_versions.txt", - "multiqc/multiqc_data/multiqc_sources.txt", - "multiqc/multiqc_plots", - "multiqc/multiqc_plots/pdf", - "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", - "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", - "multiqc/multiqc_plots/png", - "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", - "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", - "multiqc/multiqc_plots/svg", - "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", - "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", - "multiqc/multiqc_report.html", - "multiqc/versions.yml", - "pipeline_info", - "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", - "public_data", - "public_data/expression_atlas", - "public_data/expression_atlas/accessions", - "public_data/expression_atlas/accessions/accessions.txt", - "public_data/expression_atlas/accessions/selected_experiments.metadata.tsv", - "public_data/expression_atlas/accessions/species_experiments.metadata.tsv", - "public_data/expression_atlas/datasets", - "public_data/expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", - "public_data/expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", - "statistics", - "warnings" - ], - [ - "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", - "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", - "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", - "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1" - ] - ], - "meta": { - "nf-test": "0.9.3", - "nextflow": "25.10.2" - }, - "timestamp": "2025-12-11T10:42:14.842517715" - }, - "-profile test_gprofiler_target_database_entrez": { - "content": [ [ "aggregated", "aggregated/all_counts_filtered.parquet", @@ -1659,7 +1718,7 @@ "idmapping/gprofiler/mapped_gene_ids.csv", "idmapping/original_gene_ids.txt", "idmapping/renamed", - "idmapping/renamed/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", + "idmapping/renamed/E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", "merged_datasets", @@ -1715,11 +1774,11 @@ "multiqc/multiqc_report.html", "multiqc/versions.yml", "normalised", - "normalised/E_MTAB_8187_rnaseq", - "normalised/E_MTAB_8187_rnaseq/quantile_normalised", - "normalised/E_MTAB_8187_rnaseq/quantile_normalised/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", - "normalised/E_MTAB_8187_rnaseq/tpm", - "normalised/E_MTAB_8187_rnaseq/tpm/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", + "normalised/E_MTAB_5072_rnaseq", + "normalised/E_MTAB_5072_rnaseq/quantile_normalised", + "normalised/E_MTAB_5072_rnaseq/quantile_normalised/E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/E_MTAB_5072_rnaseq/tpm", + "normalised/E_MTAB_5072_rnaseq/tpm/E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", "pipeline_info", "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", "public_data", @@ -1729,8 +1788,8 @@ "public_data/expression_atlas/accessions/selected_experiments.metadata.tsv", "public_data/expression_atlas/accessions/species_experiments.metadata.tsv", "public_data/expression_atlas/datasets", - "public_data/expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", - "public_data/expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", + "public_data/expression_atlas/datasets/E_MTAB_5072_rnaseq.design.csv", + "public_data/expression_atlas/datasets/E_MTAB_5072_rnaseq.rnaseq.raw.counts.csv", "statistics", "statistics/id_mapping_stats.csv", "statistics/ratio_zeros.csv", @@ -1738,13 +1797,13 @@ "warnings" ], [ - "all_genes_summary.csv:md5,32be3aeed5cd2715847ccc2fd45c09a0", - "most_stable_genes_summary.csv:md5,2e34161e2beb2f1e0921dbf6c0ce55ba", - "most_stable_genes_transposed_counts_filtered.csv:md5,5083c04020d1c504c86689e14f731ef7", + "all_genes_summary.csv:md5,d4b5d36061b8fd31ba27edc55f738a01", + "most_stable_genes_summary.csv:md5,b47891fe0414abd0a4b7c9137d53fc1d", + "most_stable_genes_transposed_counts_filtered.csv:md5,46d96c944cc9ca3dd83e16da52c528f2", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_genes_summary.csv:md5,32be3aeed5cd2715847ccc2fd45c09a0", - "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", + "all_genes_summary.csv:md5,d4b5d36061b8fd31ba27edc55f738a01", + "whole_design.csv:md5,c9bfd7bc8ca365222e03e67eb24b9a76", "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", @@ -1763,104 +1822,36 @@ "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", "style.py:md5,b9f1207f06464e43c05e6e3912f12731", "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "all_gene_ids.txt:md5,ba79f5609df755c0f75de41357319c84", - "global_gene_id_mapping.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", - "global_gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", - "gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", - "mapped_gene_ids.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", - "original_gene_ids.txt:md5,60a0406c1d56424cfc394c438de50c99", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,ed1600e42df05fc885a16a66b77324a1", - "whole_gene_id_mapping.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", - "whole_gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", - "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,b407990bb10106de943f9e93cda6323e", - "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", - "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", - "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", - "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", - "id_mapping_stats.csv:md5,6189d2a346cce55f182e00769e3fea5f", - "ratio_zeros.csv:md5,d3b518709a097d9e41a05142b524f03c", - "skewness.csv:md5,b38aabc94d60d93b979c3cef3a922299" + "all_gene_ids.txt:md5,13ae1b52833134f8ed6d982c00487927", + "global_gene_id_mapping.csv:md5,efc95a8e276be1eb0af9639f72e48145", + "global_gene_metadata.csv:md5,1d342577587bc48c1eff077a594929fa", + "gene_metadata.csv:md5,1d342577587bc48c1eff077a594929fa", + "mapped_gene_ids.csv:md5,efc95a8e276be1eb0af9639f72e48145", + "original_gene_ids.txt:md5,5b88757be20075a2458a257221703f2a", + "E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,6244da0761b4437a6de5cff49e4d2687", + "whole_gene_id_mapping.csv:md5,efc95a8e276be1eb0af9639f72e48145", + "whole_gene_metadata.csv:md5,1d342577587bc48c1eff077a594929fa", + "whole_design.csv:md5,c9bfd7bc8ca365222e03e67eb24b9a76", + "E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,4a2aa87d0bd2990c13e088f0117cc682", + "accessions.txt:md5,561a967c16b2ef6c29fb643cd4002947", + "selected_experiments.metadata.tsv:md5,385b4d7ed40829f1e764857e805d6dc4", + "species_experiments.metadata.tsv:md5,385b4d7ed40829f1e764857e805d6dc4", + "E_MTAB_5072_rnaseq.design.csv:md5,a1f33d126dde387a2d542381c44bc1f3", + "E_MTAB_5072_rnaseq.rnaseq.raw.counts.csv:md5,c41c84899a380515d759b99eeccfe43e", + "id_mapping_stats.csv:md5,6a84babdbb434ffd5975fc771ec2db44", + "ratio_zeros.csv:md5,15eb210c4ffff8a826e0c9f45d0ab4bd", + "skewness.csv:md5,6dbe4934961471c31fc6a1671577a8fc" ] ], "meta": { "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-15T16:58:27.415680085" + "timestamp": "2025-12-16T14:13:50.668342031" }, - "-profile test_full": { + "-profile test_download_only": { "content": [ { - "AGGREGATE_RESULTS": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "CLEAN_GENE_IDS": { - "pandas": "2.3.3", - "polars": "1.35.2", - "python": "3.14.0" - }, - "COLLECT_GENE_IDS": { - "pandas": "2.3.3", - "python": "3.14.0", - "tqdm": "4.67.1" - }, - "COLLECT_STATISTICS": { - "pandas": "2.3.3", - "python": "3.13.7" - }, - "COMPUTE_BASE_STATISTICS": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "COMPUTE_BASE_STATISTICS_FOR_RNASEQ": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "COMPUTE_DATASET_STATISTICS": { - "pandas": "2.3.3", - "python": "3.13.7" - }, - "COMPUTE_GENE_TRANSCRIPT_LENGTHS": { - "pandas": "2.3.3", - "python": "3.13.7" - }, - "COMPUTE_M_MEASURE": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "COMPUTE_STABILITY_SCORES": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "COMPUTE_TPM": { - "pandas": "2.3.3", - "python": "3.13.7" - }, - "CROSS_JOIN": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "DASH_APP": { - "python": "3.13.8", - "dash": "3.2.0", - "dash-extensions": "2.0.4", - "dash-mantine-components": "2.3.0", - "dash-ag-grid": "32.3.2", - "polars": "1.35.0", - "pandas": "2.3.3", - "pyarrow": "22.0.0", - "scipy": "1.16.3" - }, - "DOWNLOAD_ENSEMBL_ANNOTATION": { - "bs4": "4.14.2", - "pandas": "2.3.3", - "python": "3.14.0", - "requests": "2.32.5", - "tqdm": "4.67.1" - }, "EXPRESSION_ATLAS": { "ExpressionAtlas": "1.30.0", "R": "4.3.3 (2024-02-29)", @@ -1870,56 +1861,65 @@ "pyyaml": "6.0.2", "requests": "2.32.4" }, - "EXPRESSION_RATIO": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "GET_CANDIDATE_GENES": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "GPROFILER_IDMAPPING": { - "pandas": "2.3.1", - "python": "3.13.5", - "requests": "2.32.4" - }, - "MAKE_CHUNKS": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "MERGE_ALL_COUNTS": { - "polars": "1.34.0", - "python": "3.14.0", - "tqdm": "4.67.1" - }, - "MERGE_RNASEQ_COUNTS": { - "polars": "1.34.0", - "python": "3.14.0", - "tqdm": "4.67.1" - }, - "NORMFINDER": { - "polars": "1.33.1", - "python": "3.13.7" - }, - "QUANTILE_NORMALISATION": { - "pandas": "2.2.3", - "pyarrow": "19.0.0", - "python": "3.12.8", - "scikit-learn": "1.6.1" - }, - "RATIO_STANDARD_VARIATION": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "RENAME_GENE_IDS": { - "pandas": "2.3.3", - "polars": "1.35.2", - "python": "3.14.0" - }, "Workflow": { "nf-core/stableexpression": "v1.0dev" } }, + [ + "errors", + "multiqc", + "multiqc/multiqc_data", + "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_eatlas_all_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_eatlas_selected_experiments_metadata.txt", + "multiqc/multiqc_data/multiqc_software_versions.txt", + "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_plots", + "multiqc/multiqc_plots/pdf", + "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", + "multiqc/multiqc_plots/pdf/eatlas_selected_experiments_metadata.pdf", + "multiqc/multiqc_plots/png", + "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", + "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", + "multiqc/multiqc_plots/svg", + "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", + "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", + "multiqc/multiqc_report.html", + "multiqc/versions.yml", + "pipeline_info", + "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", + "public_data", + "public_data/expression_atlas", + "public_data/expression_atlas/accessions", + "public_data/expression_atlas/accessions/accessions.txt", + "public_data/expression_atlas/accessions/selected_experiments.metadata.tsv", + "public_data/expression_atlas/accessions/species_experiments.metadata.tsv", + "public_data/expression_atlas/datasets", + "public_data/expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", + "public_data/expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", + "statistics", + "warnings" + ], + [ + "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", + "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1" + ] + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.2" + }, + "timestamp": "2025-12-11T10:42:14.842517715" + }, + "-profile test_gprofiler_target_database_entrez": { + "content": [ [ "aggregated", "aggregated/all_counts_filtered.parquet", @@ -1969,7 +1969,7 @@ "idmapping/gprofiler/mapped_gene_ids.csv", "idmapping/original_gene_ids.txt", "idmapping/renamed", - "idmapping/renamed/E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", + "idmapping/renamed/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", "merged_datasets", @@ -2025,11 +2025,11 @@ "multiqc/multiqc_report.html", "multiqc/versions.yml", "normalised", - "normalised/E_MTAB_5072_rnaseq", - "normalised/E_MTAB_5072_rnaseq/quantile_normalised", - "normalised/E_MTAB_5072_rnaseq/quantile_normalised/E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", - "normalised/E_MTAB_5072_rnaseq/tpm", - "normalised/E_MTAB_5072_rnaseq/tpm/E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", + "normalised/E_MTAB_8187_rnaseq", + "normalised/E_MTAB_8187_rnaseq/quantile_normalised", + "normalised/E_MTAB_8187_rnaseq/quantile_normalised/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/E_MTAB_8187_rnaseq/tpm", + "normalised/E_MTAB_8187_rnaseq/tpm/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", "pipeline_info", "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", "public_data", @@ -2039,8 +2039,8 @@ "public_data/expression_atlas/accessions/selected_experiments.metadata.tsv", "public_data/expression_atlas/accessions/species_experiments.metadata.tsv", "public_data/expression_atlas/datasets", - "public_data/expression_atlas/datasets/E_MTAB_5072_rnaseq.design.csv", - "public_data/expression_atlas/datasets/E_MTAB_5072_rnaseq.rnaseq.raw.counts.csv", + "public_data/expression_atlas/datasets/E_MTAB_8187_rnaseq.design.csv", + "public_data/expression_atlas/datasets/E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv", "statistics", "statistics/id_mapping_stats.csv", "statistics/ratio_zeros.csv", @@ -2048,13 +2048,13 @@ "warnings" ], [ - "all_genes_summary.csv:md5,d4b5d36061b8fd31ba27edc55f738a01", - "most_stable_genes_summary.csv:md5,b47891fe0414abd0a4b7c9137d53fc1d", - "most_stable_genes_transposed_counts_filtered.csv:md5,46d96c944cc9ca3dd83e16da52c528f2", + "all_genes_summary.csv:md5,32be3aeed5cd2715847ccc2fd45c09a0", + "most_stable_genes_summary.csv:md5,2e34161e2beb2f1e0921dbf6c0ce55ba", + "most_stable_genes_transposed_counts_filtered.csv:md5,5083c04020d1c504c86689e14f731ef7", "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_genes_summary.csv:md5,d4b5d36061b8fd31ba27edc55f738a01", - "whole_design.csv:md5,c9bfd7bc8ca365222e03e67eb24b9a76", + "all_genes_summary.csv:md5,32be3aeed5cd2715847ccc2fd45c09a0", + "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", @@ -2073,32 +2073,32 @@ "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", "style.py:md5,b9f1207f06464e43c05e6e3912f12731", "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "all_gene_ids.txt:md5,13ae1b52833134f8ed6d982c00487927", - "global_gene_id_mapping.csv:md5,efc95a8e276be1eb0af9639f72e48145", - "global_gene_metadata.csv:md5,1d342577587bc48c1eff077a594929fa", - "gene_metadata.csv:md5,1d342577587bc48c1eff077a594929fa", - "mapped_gene_ids.csv:md5,efc95a8e276be1eb0af9639f72e48145", - "original_gene_ids.txt:md5,5b88757be20075a2458a257221703f2a", - "E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,6244da0761b4437a6de5cff49e4d2687", - "whole_gene_id_mapping.csv:md5,efc95a8e276be1eb0af9639f72e48145", - "whole_gene_metadata.csv:md5,1d342577587bc48c1eff077a594929fa", - "whole_design.csv:md5,c9bfd7bc8ca365222e03e67eb24b9a76", - "E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,4a2aa87d0bd2990c13e088f0117cc682", - "accessions.txt:md5,561a967c16b2ef6c29fb643cd4002947", - "selected_experiments.metadata.tsv:md5,385b4d7ed40829f1e764857e805d6dc4", - "species_experiments.metadata.tsv:md5,385b4d7ed40829f1e764857e805d6dc4", - "E_MTAB_5072_rnaseq.design.csv:md5,a1f33d126dde387a2d542381c44bc1f3", - "E_MTAB_5072_rnaseq.rnaseq.raw.counts.csv:md5,c41c84899a380515d759b99eeccfe43e", - "id_mapping_stats.csv:md5,6a84babdbb434ffd5975fc771ec2db44", - "ratio_zeros.csv:md5,15eb210c4ffff8a826e0c9f45d0ab4bd", - "skewness.csv:md5,6dbe4934961471c31fc6a1671577a8fc" + "all_gene_ids.txt:md5,ba79f5609df755c0f75de41357319c84", + "global_gene_id_mapping.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", + "global_gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", + "gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", + "mapped_gene_ids.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", + "original_gene_ids.txt:md5,60a0406c1d56424cfc394c438de50c99", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,ed1600e42df05fc885a16a66b77324a1", + "whole_gene_id_mapping.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", + "whole_gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", + "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,b407990bb10106de943f9e93cda6323e", + "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", + "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", + "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", + "id_mapping_stats.csv:md5,6189d2a346cce55f182e00769e3fea5f", + "ratio_zeros.csv:md5,d3b518709a097d9e41a05142b524f03c", + "skewness.csv:md5,b38aabc94d60d93b979c3cef3a922299" ] ], "meta": { "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-15T17:03:12.971932361" + "timestamp": "2025-12-15T16:58:27.415680085" }, "-profile test_dataset_custom_mapping": { "content": [ @@ -2346,6 +2346,6 @@ "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-15T16:52:41.144998246" + "timestamp": "2025-12-16T12:51:18.807822899" } } \ No newline at end of file diff --git a/tests/modules/local/gprofiler/idmapping/main.nf.test b/tests/modules/local/gprofiler/idmapping/main.nf.test index 05444be6..e754ba0f 100644 --- a/tests/modules/local/gprofiler/idmapping/main.nf.test +++ b/tests/modules/local/gprofiler/idmapping/main.nf.test @@ -8,10 +8,7 @@ nextflow_process { test("ENSG - Mapping found") { when { - params { - // define parameters here. Example: - // outdir = "tests/results" - } + process { """ input[0] = file("$projectDir/tests/test_data/idmapping/gene_ids/gene_ids.txt", checkIfExists: true) @@ -29,14 +26,11 @@ nextflow_process { ) } } - + /* test("Entrez - No mapping found") { when { - params { - // define parameters here. Example: - // outdir = "tests/results" - } + process { """ input[0] = file("$projectDir/tests/test_data/idmapping/gene_ids/gene_ids.txt", checkIfExists: true) @@ -48,9 +42,10 @@ nextflow_process { then { assertAll( - { assert process.success } + { assert !process.success } ) } } + */ } diff --git a/tests/subworkflows/local/download_public_datasets/main.nf.test.snap b/tests/subworkflows/local/download_public_datasets/main.nf.test.snap index f31464a9..3e299483 100644 --- a/tests/subworkflows/local/download_public_datasets/main.nf.test.snap +++ b/tests/subworkflows/local/download_public_datasets/main.nf.test.snap @@ -6,7 +6,9 @@ [ { "dataset": "E_MTAB_8187_rnaseq", - "design": "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf" + "design": "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", + "normalised": false, + "platform": "rnaseq" }, "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1" ] @@ -15,7 +17,9 @@ [ { "dataset": "E_MTAB_8187_rnaseq", - "design": "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf" + "design": "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", + "normalised": false, + "platform": "rnaseq" }, "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1" ] @@ -26,7 +30,7 @@ "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-15T21:46:42.719832457" + "timestamp": "2025-12-16T15:18:21.726044151" }, "Beta vulgaris - Eatlas + GEO - all accessions": { "content": [ @@ -77,6 +81,6 @@ "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-15T21:46:26.386631545" + "timestamp": "2025-12-16T15:18:08.622422246" } } \ No newline at end of file From 7814723d0f477de759bf434cadc5869772bc960e Mon Sep 17 00:00:00 2001 From: Olivier Date: Tue, 16 Dec 2025 15:22:45 +0100 Subject: [PATCH 237/258] set default random_sampling_size at 5000 to fit with homo sapiens on a normal labtop. set homo sapiens as test_full --- conf/test_full.config | 2 +- nextflow.config | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/conf/test_full.config b/conf/test_full.config index 20b51f29..d09774c0 100644 --- a/conf/test_full.config +++ b/conf/test_full.config @@ -16,6 +16,6 @@ params { config_profile_description = 'Full test dataset to check pipeline function' // Input data - species = 'arabidopsis_lyrata' + species = 'homo_sapiens' outdir = "results/test_full" } diff --git a/nextflow.config b/nextflow.config index 943039e1..51c52e21 100644 --- a/nextflow.config +++ b/nextflow.config @@ -23,7 +23,7 @@ params { // Expression atlas skip_fetch_eatlas_accessions = false - fetch_geo_accessions = false + fetch_geo_accessions = false accessions = "" excluded_accessions = "" accessions_file = null @@ -49,7 +49,7 @@ params { // random sampling random_sampling_seed = 42 - random_sampling_size = 10000 + random_sampling_size = 5000 // MultiQC options multiqc_config = null From d7c9b09ace20b86da611d253cdd4cb3d0d620fc2 Mon Sep 17 00:00:00 2001 From: Olivier Date: Thu, 18 Dec 2025 11:15:16 +0100 Subject: [PATCH 238/258] improve doc and pass linters --- docs/troubleshooting.md | 16 +++++++------ ro-crate-metadata.json | 2 +- .../main.nf | 24 ++++++++++++------- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index dcef6570..7b3fcc79 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -7,6 +7,7 @@ If you are running the pipeline on macOS with containers (`docker`, `apptainer`, ``` NOTE: Process `NFCORE_STABLEEXPRESSION:STABLEEXPRESSION:ID_MAPPING:CLEAN_GENE_IDS ()` terminated with an error exit status (139) -- Execution is retried (1) ``` + eventually leading to pipeline failure. This is likely due to the python polars library not being compatible with macOS when run inside a container. @@ -18,34 +19,35 @@ You should run the pipeline with `-profile micromamba` or `-profile conda`. For species that are not on Expression Atlas, the pipeline will not be able to find suitable datasets and will log the following message: ``` -WARN: No dataset found. Please note that for the moment only Microarray count datasets are fetched from NCBI GEO. -You can check at https://www.ncbi.nlm.nih.gov/gds if there are raw RNA-seq count datasets for this species. +ERROR: Could not find any readily usable public dataset +... ``` ->[!TIP] ->You can first try to have the pipeline fetch suitable datasets from NCBI GEO by providing the `--fetch_geo_accessions` flag. +> [!TIP] +> You can first try to have the pipeline fetch suitable datasets from NCBI GEO by providing the `--fetch_geo_accessions` flag. -In case no datasets are found, you'll have to find a way to get count datasets and to prepare them for the pipeline. +In case no datasets are found, you'll have to find a way to get count datasets and to prepare them for the pipeline. A good start is to check in the folder `/public_data/geo/datasets/` if there are `rejected` subfolders. Such subfolders contain datasets that were downloaded (together with their experimental design) but failed to pass checks. Quite often, some of them be manually reprocessed to be suitable for the pipeline. Finally, you may want to check by yourself on [NCBI GEO](https://www.ncbi.nlm.nih.gov/gds). Alternatively, some public websites contain expression datasets that may be suitable for the pipeline, such as: + - [Bgee](https://www.bgee.org/) ## Not enough memory The pipeline limits the number of downloaded datasets to a certain number in order to limit RAM usage, especially for `homo sapiens`. -However, on small computers, the limit may be too permissive and lead to RAM overhead. You can reduce the number of datasets downloaded by setting the `--random_sampling_size` to a lower value. +However, on small computers, the limit may be too permissive and lead to RAM overhead. You can reduce the number of datasets downloaded by setting the `--random_sampling_size` to a lower value. ## Why do I get only a fraction of the public datasets available on Expression Atlas or NCBI GEO? Give them back! To reduce the RAM overhead, the pipeline selects randomly a certain number of datasets, based on the number of samples they contain. To increase the number of collected datasets, you can increase the `--random_sampling_size` parameter. [!TIP] ->A seed is also set in order to make the runs reproducible. You can change the subset of chosen datasets by changing the `--random_sampling_seed`. +> A seed is also set in order to make the runs reproducible. You can change the subset of chosen datasets by changing the `--random_sampling_seed`. ## The pipeline fails because it does not find a genome for the specified species diff --git a/ro-crate-metadata.json b/ro-crate-metadata.json index ef880802..4901a6cb 100644 --- a/ro-crate-metadata.json +++ b/ro-crate-metadata.json @@ -23,7 +23,7 @@ "@type": "Dataset", "creativeWorkStatus": "InProgress", "datePublished": "2025-12-08T14:37:14+00:00", - "description": "

    \n \n \n \"nf-core/stableexpression\"\n \n

    \n\n[![Open in GitHub Codespaces](https://img.shields.io/badge/Open_In_GitHub_Codespaces-black?labelColor=grey&logo=github)](https://github.com/codespaces/new/nf-core/stableexpression)\n[![GitHub Actions CI Status](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml/badge.svg)](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml)\n[![GitHub Actions Linting Status](https://github.com/nf-core/stableexpression/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/stableexpression/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/stableexpression/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.04.0-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-3.5.1-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/3.5.1)\n[![run with apptainer](https://custom-icon-badges.demolab.com/badge/run%20with-apptainer-4545?logo=apptainer&color=teal&labelColor=000000)](https://apptainer.org/)\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/stableexpression)\n\n[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23stableexpression-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/stableexpression)[![Follow on Bluesky](https://img.shields.io/badge/bluesky-%40nf__core-1185fe?labelColor=000000&logo=bluesky)](https://bsky.app/profile/nf-co.re)[![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/stableexpression** is a bioinformatics pipeline aiming to aggregate multiple count datasets (public / provided by the user) for a specific species and find the most stable genes.\n\nIt takes as main inputs :\n * a species name (mandatory)\n * keywords for Expression Atlas / GEO search (optional)\n * a CSV input file listing your own raw / normalised count datasets (optional).\n\n**Use cases**:\n * **find the most suitable genes as RT-qPCR reference genes for a specific species (and optionally specific conditions)**\n * download all Expression Atlas and NCBI GEO datasets for a species\n\n\n\n## Basic 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\nTo search the most stable genes in a species considering all public datasets, simply run:\n\n```bash\nnextflow run nf-core/stableexpression \\\n -r dev \\\n -profile \\\n --species \\\n --outdir \n ```\n\n> [!IMPORTANT]\n > For more specific scenarios, __like fetching only specific conditions or using your own expression dataset(s)__, please refer to the [usage documentation](https://nf-co.re/stableexpression/usage).\n\n> [!NOTE]\n> See [here](https://nf-co.re/stableexpression/usage#profiles) for more information about profiles.\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/stableexpression/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/stableexpression/output).\n\n## Support us\n\nIf you like nf-core/stableexpression, please make sure you give it a star on GitHub.\n\n[![stars - stableexpression](https://img.shields.io/github/stars/nf-core/stableexpression?style=social)](https://github.com/nf-core/stableexpression)\n\n## Credits\n\nnf-core/stableexpression was originally written by Olivier Coen.\n\n\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 `#stableexpression` channel](https://nfcore.slack.com/channels/stableexpression) (you can join with [this invite](https://nf-co.re/join/slack)).\n\n## Citations\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", + "description": "

    \n \n \n \"nf-core/stableexpression\"\n \n

    \n\n[![Open in GitHub Codespaces](https://img.shields.io/badge/Open_In_GitHub_Codespaces-black?labelColor=grey&logo=github)](https://github.com/codespaces/new/nf-core/stableexpression)\n[![GitHub Actions CI Status](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml/badge.svg)](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml)\n[![GitHub Actions Linting Status](https://github.com/nf-core/stableexpression/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/stableexpression/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/stableexpression/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.04.0-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-3.5.1-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/3.5.1)\n[![run with apptainer](https://custom-icon-badges.demolab.com/badge/run%20with-apptainer-4545?logo=apptainer&color=teal&labelColor=000000)](https://apptainer.org/)\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/stableexpression)\n\n[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23stableexpression-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/stableexpression)[![Follow on Bluesky](https://img.shields.io/badge/bluesky-%40nf__core-1185fe?labelColor=000000&logo=bluesky)](https://bsky.app/profile/nf-co.re)[![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/stableexpression** is a bioinformatics pipeline aiming to aggregate multiple count datasets (public / provided by the user) for a specific species and find the most stable genes.\n\nIt takes as main inputs :\n\n- a species name (mandatory)\n- keywords for Expression Atlas / GEO search (optional)\n- a CSV input file listing your own raw / normalised count datasets (optional).\n\n**Use cases**:\n\n- **find the most suitable genes as RT-qPCR reference genes for a specific species (and optionally specific conditions)**\n- download all Expression Atlas and / or NCBI GEO datasets for a species (and optionally keywords)\n\n## Basic 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\nTo search the most stable genes in a species considering all public datasets, simply run:\n\n```bash\nnextflow run nf-core/stableexpression \\\n -profile \\\n --species \\\n --outdir \n```\n\n> [!IMPORTANT]\n> For more specific scenarios, **like fetching only specific conditions or using your own expression dataset(s)**, please refer to the [usage documentation](https://nf-co.re/stableexpression/usage).\n\n> [!NOTE]\n> See [here](https://nf-co.re/stableexpression/usage#profiles) for more information about profiles.\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/stableexpression/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/stableexpression/output).\n\n## Support us\n\nIf you like nf-core/stableexpression, please make sure you give it a star on GitHub.\n\n[![stars - stableexpression](https://img.shields.io/github/stars/nf-core/stableexpression?style=social)](https://github.com/nf-core/stableexpression)\n\n## Credits\n\nnf-core/stableexpression was originally written by Olivier Coen.\n\n\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 `#stableexpression` channel](https://nfcore.slack.com/channels/stableexpression) (you can join with [this invite](https://nf-co.re/join/slack)).\n\n## Citations\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" diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index 527b5fad..9c34f75d 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -421,17 +421,25 @@ def storeDatasetSize( ch_counts, nb_genes_key, nb_samples_key ) { */ def checkCounts(ch_counts) { - // display a warning if no datasets are found - def msg = [ - "Could not find any readily usable public dataset.", - "You can check directly on NCBI GEO if there are datasets for this species that you can prepare yourself:", - "https://www.ncbi.nlm.nih.gov/gds", - "Once you have prepared your own data, you can relaunch the pipeline and provided your prepared count datasets using the --datasets parameter. ", - "For more information, see the online documentation at https://nf-co.re/stableexpression." - ].join("\n").trim() ch_counts.count().map { n -> if( n == 0 ) { + // display a warning if no datasets are found + if ( !params.fetch_geo_accessions ) { + msg_lst = [ + "Could not find any readily usable public dataset.", + "Please set the --fetch_geo_accessions flag and run again." + ] + } else { + msg_lst = [ + "Could not find any readily usable public dataset.", + "You can check directly on NCBI GEO if there are datasets for this species that you can prepare yourself:", + "https://www.ncbi.nlm.nih.gov/gds", + "Once you have prepared your own data, you can relaunch the pipeline and provided your prepared count datasets using the --datasets parameter. ", + "For more information, see the online documentation at https://nf-co.re/stableexpression." + ] + } + def msg = msg_lst.join("\n").trim() error(msg) } } From 63678df688ea4441723a65bbf7effe8c816e1e4b Mon Sep 17 00:00:00 2001 From: Olivier Date: Thu, 18 Dec 2025 14:43:09 +0100 Subject: [PATCH 239/258] add new metromap --- README.md | 4 + .../nf-core-stableexpression_metro_map.drawio | 238 ------------------ .../nf_core_stableexpression.metromap.png | Bin 0 -> 482088 bytes 3 files changed, 4 insertions(+), 238 deletions(-) delete mode 100644 docs/images/nf-core-stableexpression_metro_map.drawio create mode 100644 docs/images/nf_core_stableexpression.metromap.png diff --git a/README.md b/README.md index 12123d78..32de7631 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,10 @@ **nf-core/stableexpression** is a bioinformatics pipeline aiming to aggregate multiple count datasets (public / provided by the user) for a specific species and find the most stable genes. +

    + +

    + It takes as main inputs : - a species name (mandatory) diff --git a/docs/images/nf-core-stableexpression_metro_map.drawio b/docs/images/nf-core-stableexpression_metro_map.drawio deleted file mode 100644 index 67fd7efe..00000000 --- a/docs/images/nf-core-stableexpression_metro_map.drawio +++ /dev/null @@ -1,238 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/images/nf_core_stableexpression.metromap.png b/docs/images/nf_core_stableexpression.metromap.png new file mode 100644 index 0000000000000000000000000000000000000000..eb1463dc520ed38946781c8d1d40233e2d83a1d4 GIT binary patch literal 482088 zcmeFaSB~r07A5$A0=gRo)CBrJfvS(Zpr`oP@}fedyyH(nG?GYBl%pt-Gw3<=ka{$Y z8bGg2qmUvZZ${c1`JS$ckjTU1wmtjov-p4a^yYv2um9(N{nx+#_21N1ZSb#u{qO(J zzy9@K{@4HUzu`*s|NdY0zy8br_rJpWEZkm~uJ8QoC%&3}`iW!KyHofRU;ByU7W@4- z+g+@Vi|y%K;Doq@`-_zm+zJgC<^Bjcd>~qHr&R1N! zmZbkF3E8z{^b=oZ1?d~}KX9k%SeqSc;TV>|KNgo7?$KH9@K_@)l{FnX$CJAYvfgpZ zH}0F9-8fkb-^niHVd|y1JKD}MyTqe!+)sS{C#H-$^pCIR~1J3YjNNYwdFGUpL~}Y*WT4u)VaMjkBj|U+u4qfitTIO(UOQBQK#%U@1vM?iDOpB zqM^K9x-fK;VlZi2D0X&j=afI3)fMh`e(3HWFIu}GaIBmSL?iGQOs{_7e(3p!fCy*cbDw(^wK?5vUx0K z^SIlgx;vuX-!2MmuT{sV$u6cbZwGwy+n>En#_`7Cf22CbYg&_^`LSUrZKJl{HSL$q z&qB@$`^9S`dAp}DE|~bZI|sHyG$MJrtliKV?iUu(=mHkQwRO1l^V^VMzq<2x8MpJb z^bd+Gj#kb|EV#dN3Xi*uQxpt#0cwM&5?@9O|D4l5zu+80CuiTZci*SOD>a(VZtH}{ z7?rN?7=q_=LA|_kk6g+pH*co|fflpob1{(=KKs7N*>`oToqGKl8lz0kBHteY>AyFe zR9~veK?$Ps&RTBj_FDa)W$kBK%6jx4Mm{O$+B@=bV$QY&`DEpRcsJ$nb%-bCJ$w>R z=JTm2WSo_AVa!GE3sSH2l)TJt^1`Fy6r?XNdHko;CQJ@2{;!$4j13-4mgLiO%N_hu zj#*}Q@+VF@oXLZzRCnj23ty09bm#JV8Q)(mZc*0C>?rw($CdftIT04AiZ4?BTao@p zYBd>Z?B`SJs8L~#+e4IKPK%20X+@3WciK6#xWIlx@rDyxYy94yP2aWf!Cr)LqqCy0 zS+~-6*RtUiwWenKPqxwpU)^i@H%3}e-tZckVd~4*(enJK6vY?dwcpJ`_3QrVoYIvL z$CN`KbN>}tr�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�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
      <$v@fI)!$e=OWx#|h! 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
      \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
      accessionnb_samples
      0E-MTAB-3578122
      1E-GEOD-5256417
      2E-GEOD-4990611
      3E-MTAB-1144212
      4E-MTAB-258212
      .........
      1278E-TABM-7558
      1279E-TABM-77816
      1280E-TABM-86523
      1281E-TABM-8930
      1282E-TABM-90356
      \n", + "

      1283 rows × 2 columns

      \n", + "
      " + ], + "text/plain": [ + " accession nb_samples\n", + "0 E-MTAB-3578 122\n", + "1 E-GEOD-52564 17\n", + "2 E-GEOD-49906 11\n", + "3 E-MTAB-11442 12\n", + "4 E-MTAB-2582 12\n", + "... ... ...\n", + "1278 E-TABM-755 8\n", + "1279 E-TABM-778 16\n", + "1280 E-TABM-865 23\n", + "1281 E-TABM-89 30\n", + "1282 E-TABM-903 56\n", + "\n", + "[1283 rows x 2 columns]" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "84325c97", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
      \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
      nb_samples
      count1283.000000
      mean20.771629
      std79.120320
      min4.000000
      25%7.000000
      50%12.000000
      75%18.000000
      max2660.000000
      \n", + "
      " + ], + "text/plain": [ + " nb_samples\n", + "count 1283.000000\n", + "mean 20.771629\n", + "std 79.120320\n", + "min 4.000000\n", + "25% 7.000000\n", + "50% 12.000000\n", + "75% 18.000000\n", + "max 2660.000000" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.describe()" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "e47bc3b4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "np.int64(26650)" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df[\"nb_samples\"].sum()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7bcb812c", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tests/default.nf.test b/tests/default.nf.test index b33b3359..4ac9c772 100644 --- a/tests/default.nf.test +++ b/tests/default.nf.test @@ -195,8 +195,8 @@ nextflow_pipeline { } - test("-profile test_dataset_custom_mapping") { - tag "test_dataset_custom_mapping" + test("-profile test_dataset_custom_mapping_and_gene_length") { + tag "test_dataset_custom_mapping_and_gene_length" when { params { @@ -206,6 +206,7 @@ nextflow_pipeline { skip_fetch_eatlas_accessions = true gene_id_mapping = "${projectDir}/tests/test_data/input_datasets/mapping.csv" gene_metadata = "${projectDir}/tests/test_data/input_datasets/metadata.csv" + gene_length = "${projectDir}/tests/test_data/input_datasets/gene_lengths.csv" outdir = "$outputDir" } } diff --git a/tests/default.nf.test.snap b/tests/default.nf.test.snap index c8b365a0..22e6aa9f 100644 --- a/tests/default.nf.test.snap +++ b/tests/default.nf.test.snap @@ -1206,6 +1206,275 @@ }, "timestamp": "2025-12-18T14:36:48.357141617" }, + "-profile test_dataset_custom_mapping_and_gene_length": { + "content": [ + { + "AGGREGATE_RESULTS": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COLLECT_STATISTICS": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "COMPUTE_BASE_STATISTICS": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_BASE_STATISTICS_FOR_MICROARRAY": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_BASE_STATISTICS_FOR_RNASEQ": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_DATASET_STATISTICS": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "COMPUTE_STABILITY_SCORES": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_TPM": { + "pandas": "2.3.3", + "python": "3.13.7" + }, + "DASH_APP": { + "python": "3.13.8", + "dash": "3.2.0", + "dash-extensions": "2.0.4", + "dash-mantine-components": "2.3.0", + "dash-ag-grid": "32.3.2", + "polars": "1.35.0", + "pandas": "2.3.3", + "pyarrow": "22.0.0", + "scipy": "1.16.3" + }, + "GET_CANDIDATE_GENES": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "MERGE_ALL_COUNTS": { + "polars": "1.34.0", + "python": "3.14.0", + "tqdm": "4.67.1" + }, + "MERGE_MICROARRAY_COUNTS": { + "polars": "1.34.0", + "python": "3.14.0", + "tqdm": "4.67.1" + }, + "MERGE_RNASEQ_COUNTS": { + "polars": "1.34.0", + "python": "3.14.0", + "tqdm": "4.67.1" + }, + "NORMFINDER": { + "polars": "1.33.1", + "python": "3.13.7" + }, + "QUANTILE_NORMALISATION": { + "pandas": "2.2.3", + "pyarrow": "19.0.0", + "python": "3.12.8", + "scikit-learn": "1.6.1" + }, + "RENAME_GENE_IDS": { + "pandas": "2.3.3", + "polars": "1.35.2", + "python": "3.14.0" + }, + "Workflow": { + "nf-core/stableexpression": "v1.0dev" + } + }, + [ + "aggregated", + "aggregated/all_counts_filtered.parquet", + "aggregated/all_genes_summary.csv", + "aggregated/most_stable_genes_summary.csv", + "aggregated/most_stable_genes_transposed_counts_filtered.csv", + "dash_app", + "dash_app/app.py", + "dash_app/assets", + "dash_app/assets/style.css", + "dash_app/data", + "dash_app/data/all_counts.parquet", + "dash_app/data/all_genes_summary.csv", + "dash_app/data/whole_design.csv", + "dash_app/environment.yml", + "dash_app/file_system_backend", + "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", + "dash_app/src", + "dash_app/src/callbacks", + "dash_app/src/callbacks/__pycache__", + "dash_app/src/callbacks/__pycache__/common.cpython-313.pyc", + "dash_app/src/callbacks/__pycache__/genes.cpython-313.pyc", + "dash_app/src/callbacks/__pycache__/samples.cpython-313.pyc", + "dash_app/src/callbacks/common.py", + "dash_app/src/callbacks/genes.py", + "dash_app/src/callbacks/samples.py", + "dash_app/src/components", + "dash_app/src/components/__pycache__", + "dash_app/src/components/__pycache__/graphs.cpython-313.pyc", + "dash_app/src/components/__pycache__/right_sidebar.cpython-313.pyc", + "dash_app/src/components/__pycache__/stores.cpython-313.pyc", + "dash_app/src/components/__pycache__/tables.cpython-313.pyc", + "dash_app/src/components/__pycache__/tooltips.cpython-313.pyc", + "dash_app/src/components/__pycache__/top.cpython-313.pyc", + "dash_app/src/components/graphs.py", + "dash_app/src/components/icons.py", + "dash_app/src/components/right_sidebar.py", + "dash_app/src/components/settings", + "dash_app/src/components/settings/__pycache__", + "dash_app/src/components/settings/__pycache__/genes.cpython-313.pyc", + "dash_app/src/components/settings/__pycache__/samples.cpython-313.pyc", + "dash_app/src/components/settings/genes.py", + "dash_app/src/components/settings/samples.py", + "dash_app/src/components/stores.py", + "dash_app/src/components/tables.py", + "dash_app/src/components/tooltips.py", + "dash_app/src/components/top.py", + "dash_app/src/utils", + "dash_app/src/utils/__pycache__", + "dash_app/src/utils/__pycache__/config.cpython-313.pyc", + "dash_app/src/utils/__pycache__/data_management.cpython-313.pyc", + "dash_app/src/utils/__pycache__/style.cpython-313.pyc", + "dash_app/src/utils/config.py", + "dash_app/src/utils/data_management.py", + "dash_app/src/utils/style.py", + "dash_app/versions.yml", + "errors", + "idmapping", + "idmapping/global_gene_id_mapping.csv", + "idmapping/global_gene_metadata.csv", + "idmapping/renamed", + "idmapping/renamed/microarray.normalised.renamed.csv", + "idmapping/renamed/rnaseq.raw.renamed.csv", + "idmapping/whole_gene_id_mapping.csv", + "idmapping/whole_gene_metadata.csv", + "merged_datasets", + "merged_datasets/whole_design.csv", + "multiqc", + "multiqc/multiqc_data", + "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_expression_distributions_most_stable_genes.txt", + "multiqc/multiqc_data/multiqc_gene_statistics.txt", + "multiqc/multiqc_data/multiqc_id_mapping_stats.txt", + "multiqc/multiqc_data/multiqc_ranked_most_stable_genes_summary.txt", + "multiqc/multiqc_data/multiqc_ratio_zeros.txt", + "multiqc/multiqc_data/multiqc_skewness.txt", + "multiqc/multiqc_data/multiqc_software_versions.txt", + "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_plots", + "multiqc/multiqc_plots/pdf", + "multiqc/multiqc_plots/pdf/expression_distributions_most_stable_genes.pdf", + "multiqc/multiqc_plots/pdf/gene_statistics.pdf", + "multiqc/multiqc_plots/pdf/id_mapping_stats-cnt.pdf", + "multiqc/multiqc_plots/pdf/id_mapping_stats-pct.pdf", + "multiqc/multiqc_plots/pdf/ranked_most_stable_genes_summary.pdf", + "multiqc/multiqc_plots/pdf/ratio_zeros.pdf", + "multiqc/multiqc_plots/pdf/skewness.pdf", + "multiqc/multiqc_plots/png", + "multiqc/multiqc_plots/png/expression_distributions_most_stable_genes.png", + "multiqc/multiqc_plots/png/gene_statistics.png", + "multiqc/multiqc_plots/png/id_mapping_stats-cnt.png", + "multiqc/multiqc_plots/png/id_mapping_stats-pct.png", + "multiqc/multiqc_plots/png/ranked_most_stable_genes_summary.png", + "multiqc/multiqc_plots/png/ratio_zeros.png", + "multiqc/multiqc_plots/png/skewness.png", + "multiqc/multiqc_plots/svg", + "multiqc/multiqc_plots/svg/expression_distributions_most_stable_genes.svg", + "multiqc/multiqc_plots/svg/gene_statistics.svg", + "multiqc/multiqc_plots/svg/id_mapping_stats-cnt.svg", + "multiqc/multiqc_plots/svg/id_mapping_stats-pct.svg", + "multiqc/multiqc_plots/svg/ranked_most_stable_genes_summary.svg", + "multiqc/multiqc_plots/svg/ratio_zeros.svg", + "multiqc/multiqc_plots/svg/skewness.svg", + "multiqc/multiqc_report.html", + "multiqc/versions.yml", + "normalised", + "normalised/microarray.normalised", + "normalised/microarray.normalised/quantile_normalised", + "normalised/microarray.normalised/quantile_normalised/microarray.normalised.renamed.quant_norm.parquet", + "normalised/rnaseq.raw", + "normalised/rnaseq.raw/quantile_normalised", + "normalised/rnaseq.raw/quantile_normalised/rnaseq.raw.renamed.tpm.quant_norm.parquet", + "normalised/rnaseq.raw/tpm", + "normalised/rnaseq.raw/tpm/rnaseq.raw.renamed.tpm.csv", + "pipeline_info", + "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", + "statistics", + "statistics/id_mapping_stats.csv", + "statistics/ratio_zeros.csv", + "statistics/skewness.csv", + "warnings" + ], + [ + "all_genes_summary.csv:md5,03b370a040cac715b58c31865e7c8557", + "most_stable_genes_summary.csv:md5,03b370a040cac715b58c31865e7c8557", + "most_stable_genes_transposed_counts_filtered.csv:md5,9ee131e180ccaa879342af5873cdcf19", + "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", + "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", + "all_genes_summary.csv:md5,03b370a040cac715b58c31865e7c8557", + "whole_design.csv:md5,70d6c2673e619ca52d2774fb3e368382", + "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", + "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", + "common.cpython-313.pyc:md5,84e191f4cc04fb26dfdbe53c3064fb49", + "genes.cpython-313.pyc:md5,5b4cea3577ad8b61b95f310095aa8aab", + "samples.cpython-313.pyc:md5,2f171003867f80ba4cbfe0cc7e04c279", + "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", + "genes.py:md5,680cb5f4e107a3b091821917d72a555c", + "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", + "graphs.cpython-313.pyc:md5,19857cc37de34c37fb5c4a6115f9ed99", + "right_sidebar.cpython-313.pyc:md5,2f35bdaf8b8106a4e5fc821fb85d484e", + "stores.cpython-313.pyc:md5,9475873426d1f241a6967069a1ac9b91", + "tables.cpython-313.pyc:md5,b65b8472259e227bae1363175a1cc59f", + "tooltips.cpython-313.pyc:md5,53d1e39e3e00c534778961ec9afc14cb", + "top.cpython-313.pyc:md5,5d2bac677772dad8e8d230f1dd0c0983", + "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", + "icons.py:md5,3e66bae5ceca3dfbd190d88db0dc0828", + "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", + "genes.cpython-313.pyc:md5,345e0b2804515e5209ac9170ce6f47e6", + "samples.cpython-313.pyc:md5,d5fe51e17f26d857ae0a622edd7d21d7", + "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", + "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", + "stores.py:md5,6e829f7950f09c7c9b10568de41baca2", + "tables.py:md5,92e8cc03fd8c4192af9201a7ff3cd979", + "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", + "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", + "config.cpython-313.pyc:md5,fbae7ce40027422c58a8b7df113643dc", + "data_management.cpython-313.pyc:md5,d13d56de0a07961ac180bf23ff6df852", + "style.cpython-313.pyc:md5,acd51533122124e35b9baf578092ccf1", + "config.py:md5,b629adc64e497622f000945ee6e6aed2", + "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", + "style.py:md5,b9f1207f06464e43c05e6e3912f12731", + "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", + "global_gene_id_mapping.csv:md5,187a86074197044846bb8565e122eb8e", + "global_gene_metadata.csv:md5,5ae2d701ca0cb6384d9e1e08a345e452", + "microarray.normalised.renamed.csv:md5,6adb74d67379a5a3d3309b10a0c4bec5", + "rnaseq.raw.renamed.csv:md5,aa22384ba73d180629add4e174c7f37d", + "whole_gene_id_mapping.csv:md5,187a86074197044846bb8565e122eb8e", + "whole_gene_metadata.csv:md5,5ae2d701ca0cb6384d9e1e08a345e452", + "whole_design.csv:md5,70d6c2673e619ca52d2774fb3e368382", + "rnaseq.raw.renamed.tpm.csv:md5,dde1d8ea5d271c4e0486c7ba0936a972", + "id_mapping_stats.csv:md5,ee400af7734b2226406fc7ba986dccfa", + "ratio_zeros.csv:md5,d206e45c16e6bd13de75ea6d20bbd30d", + "skewness.csv:md5,9917b39dfe5ee6e680fa1783f8a096c4" + ] + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.2" + }, + "timestamp": "2025-12-22T15:32:07.792328885" + }, "-profile test_accessions_only": { "content": [ { diff --git a/tests/test_data/input_datasets/gene_lengths.csv b/tests/test_data/input_datasets/gene_lengths.csv new file mode 100644 index 00000000..03ffba68 --- /dev/null +++ b/tests/test_data/input_datasets/gene_lengths.csv @@ -0,0 +1,10 @@ +gene_id,length +ENSRNA049453121,100 +ENSRNA049453138,200 +ENSRNA049454388,300 +ENSRNA049454416,400 +ENSRNA049454647,500 +ENSRNA049454661,600 +ENSRNA049454747,700 +ENSRNA049454887,800 +ENSRNA049454931,900 diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 3383a340..73d1c77a 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -116,7 +116,8 @@ workflow STABLEEXPRESSION { species, ch_counts, params.normalisation_method, - params.quantile_norm_target_distrib + params.quantile_norm_target_distrib, + params.gene_length ) // ----------------------------------------------------------------- From 4d2d9a9557c973b0ed6582200b661a021f3c7297 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Mon, 22 Dec 2025 15:47:14 +0100 Subject: [PATCH 242/258] update doc --- README.md | 15 +++++++++++---- docs/troubleshooting.md | 4 ++-- docs/usage.md | 41 +++++++++++++++++++++++++++++++++-------- 3 files changed, 46 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 32de7631..7eb5509b 100644 --- a/README.md +++ b/README.md @@ -53,11 +53,18 @@ nextflow run nf-core/stableexpression \ --outdir ``` -> [!IMPORTANT] -> For more specific scenarios, **like fetching only specific conditions or using your own expression dataset(s)**, please refer to the [usage documentation](https://nf-co.re/stableexpression/usage). +## More advanced usage -> [!NOTE] -> See [here](https://nf-co.re/stableexpression/usage#profiles) for more information about profiles. +For more specific scenarios, like: + +- **fetching only specific conditions** +- **using your own expression dataset(s)** + +please refer to the [usage documentation](https://nf-co.re/stableexpression/usage). + +## Profiles + +See [here](https://nf-co.re/stableexpression/usage#profiles) for more information about profiles. ## Pipeline output diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 7b3fcc79..49034524 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -49,9 +49,9 @@ To reduce the RAM overhead, the pipeline selects randomly a certain number of da > A seed is also set in order to make the runs reproducible. You can change the subset of chosen datasets by changing the `--random_sampling_seed`. -## The pipeline fails because it does not find a genome for the specified species +## The pipeline failed to find a genome annotation for the specified species -If you are working on a species for which there is no genome assembly available on Ensembl, you cannot (for now) perform TPM normalisation. A fallback is to use CPM normalisation by setting `--normalisation_method cpm`. It will introduce a small bias towards long genes, but this should not result in big changes. +If you know the length of the longest cDNA for each gene, you can provide gene lengths yourself with the `--gene_length` flag (see [Custom gene ID mapping / metadata / length](docs/usage.md#5-custom-gene-id-mapping-and-metadata)). In case you do not have access to gene length, TPM normalisation cannot be formed. A fallback is to use CPM normalisation by setting `--normalisation_method cpm`. It will introduce a small bias towards long genes, but this should not result in big changes. ## Java heap space diff --git a/docs/usage.md b/docs/usage.md index a08ca211..9a970f1f 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -90,7 +90,7 @@ You can of course provide your own counts datasets / experimental designs. > [!WARNING] > Microarray data must be already normalised. When mixing your own datasets with public ones in a single run, you should use the `RMA` method to be compliant with Expression Atlas and GEO datasets. -First, prepare a samplesheet listing the different count datasets you want to use. Each row represents a specific dataset and must contain: +First, prepare a CSV samplesheet listing the different count datasets you want to use. Each row represents a specific dataset and must contain: | Column | Description | | ------------ | ----------------------------------------------------------------------------------------- | @@ -170,17 +170,28 @@ nextflow run nf-core/stableexpression \ > [!TIP] > You can check if your gene IDs can be mapped using the [g:Profiler server](https://biit.cs.ut.ee/gprofiler/convert). -### 5. Custom gene ID mapping and metadata +### 5. Custom gene ID mapping / metadata / length -You can supply your own gene id mapping file and optionally gene metadata with: +You can supply your own: + +- gene id mapping file +- gene metadata file +- gene length file + +The gene ID mapping file is used to map gene IDs in count table(s) (local or downloaded) to more generic IDs that will be used as basis fore subsequent steps. + +The gene metadata file provides additional information about the genes, such as their common name and description. + +The gene length file provides the length of each gene, which is used to compute the TPM values during gene expression normalisation. ```bash nextflow run nf-core/stableexpression \ -profile \ --species \ --datasets \ - --gene_id_mapping \ - --gene_metadata \ + --gene_id_mapping \ + --gene_metadata \ + --gene_length \ --skip_fetch_eatlas_accessions \ --outdir ``` @@ -192,7 +203,7 @@ Structure of the gene id mapping file: | `original_gene_id` | Gene ID used in the provided count dataset(s) | | `gene_id` | Mapped gene ID | -It should look as follows: +Example: ```csv title=gene_id_mapping.csv original_gene_id,gene_id @@ -208,7 +219,7 @@ Structure of the gene metadata file: | `name` | Gene common name | | `description` | Gene description | -It should look as follows: +Example: ```csv title=gene_metadata.csv gene_id,name,description @@ -216,6 +227,20 @@ ENSG1234567890,Gene A,Description of gene A OTHERmappedgeneID,My OTHER Gene,Another description ``` +Structure of the gene length file: + +| Column | Description | +| --------- | -------------------------------- | +| `gene_id` | Mapped gene ID | +| `length` | Gene length (longest transcript) | + +Example: + +````csv title=gene_length.csv +gene_id,length +ENSG1234567890,1000 +OTHERmappedgeneID,2000 + ### 6. More advanced scenarios For advanced scenarios, you can see the list of available parameters in the [parameter documentation](https://nf-co.re/stableexpression/parameters). @@ -229,7 +254,7 @@ work # Directory containing the nextflow working files # Finished results in specified location (defined with --outdir) .nextflow_log # Log file from Nextflow # Other nextflow hidden files, eg. history of pipeline runs and old logs. -``` +```` For a detailed description of the output files, please consult the [nf-core stableexpression output directory structure](https://nf-co.re/stableexpression/output). From 909f984570f6ff24d648a7d41c47094c41411765 Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Tue, 23 Dec 2025 12:47:57 +0100 Subject: [PATCH 243/258] improve reproducibility of nf-tests --- conf/modules/qc.config | 5 +- modules/local/dash_app/main.nf | 5 +- tests/.nftignore | 3 +- tests/default.nf.test | 3 +- tests/default.nf.test.snap | 490 +----------------- .../expression_normalisation/main.nf.test | 41 ++ .../main.nf.test.snap | 51 ++ 7 files changed, 115 insertions(+), 483 deletions(-) diff --git a/conf/modules/qc.config b/conf/modules/qc.config index c6f57d8e..a81b42e1 100644 --- a/conf/modules/qc.config +++ b/conf/modules/qc.config @@ -11,7 +11,10 @@ process { withName: 'DASH_APP' { publishDir = [ path: { "${params.outdir}/dash_app/" }, - mode: 'copy' + mode: 'copy', + saveAs: { + filename -> ['versions.yml', 'file_system_backend'].contains(filename) ? null : filename + } ] } diff --git a/modules/local/dash_app/main.nf b/modules/local/dash_app/main.nf index 3a49ff8a..16753ff9 100644 --- a/modules/local/dash_app/main.nf +++ b/modules/local/dash_app/main.nf @@ -23,7 +23,7 @@ process DASH_APP { path all_genes_summary output: - path("*"), emit: app + path("*"), emit: app path "versions.yml", emit: versions script: @@ -55,8 +55,7 @@ process DASH_APP { # trying to launch the app # if the resulting exit code is not 124 (exit code of timeout) then there is an error - export PYTHONDONTWRITEBYTECODE=1 - timeout 10 python app.py || exit_code=\$?; [ "\$exit_code" -eq 124 ] && exit 0 || exit 100 + timeout 10 python -B app.py || exit_code=\$?; [ "\$exit_code" -eq 124 ] && exit 0 || exit 100 """ } diff --git a/tests/.nftignore b/tests/.nftignore index 53461f90..4d022cb7 100644 --- a/tests/.nftignore +++ b/tests/.nftignore @@ -2,4 +2,5 @@ pipeline_info/*.{html,json,txt,yml} multiqc/** **.parquet -public_data/geo/datasets/** +**.metadata.tsv +**.py diff --git a/tests/default.nf.test b/tests/default.nf.test index 4ac9c772..5213b474 100644 --- a/tests/default.nf.test +++ b/tests/default.nf.test @@ -225,7 +225,7 @@ nextflow_pipeline { } } - + /* test("-profile test_no_dataset_found") { tag "test_no_dataset_found" @@ -240,6 +240,7 @@ nextflow_pipeline { assert !workflow.success } } + */ test("-profile test_included_and_excluded_accessions") { tag "test_included_and_excluded_accessions" diff --git a/tests/default.nf.test.snap b/tests/default.nf.test.snap index 22e6aa9f..3242449b 100644 --- a/tests/default.nf.test.snap +++ b/tests/default.nf.test.snap @@ -115,8 +115,6 @@ "dash_app/data/all_genes_summary.csv", "dash_app/data/whole_design.csv", "dash_app/environment.yml", - "dash_app/file_system_backend", - "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", "dash_app/src", "dash_app/src/callbacks", "dash_app/src/callbacks/common.py", @@ -137,7 +135,6 @@ "dash_app/src/utils/config.py", "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", - "dash_app/versions.yml", "errors", "idmapping", "idmapping/collected_gene_ids", @@ -220,28 +217,10 @@ "all_genes_summary.csv:md5,54b96cbfb5e464ec086c7e1308701e02", "most_stable_genes_summary.csv:md5,54b96cbfb5e464ec086c7e1308701e02", "most_stable_genes_transposed_counts_filtered.csv:md5,af21e36c540965846b73245678b74f36", - "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", "all_genes_summary.csv:md5,54b96cbfb5e464ec086c7e1308701e02", "whole_design.csv:md5,f29515bc2c783e593fb9028127342593", "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", - "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", - "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", - "genes.py:md5,680cb5f4e107a3b091821917d72a555c", - "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", - "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", - "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", - "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", - "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", - "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", - "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", - "tables.py:md5,92e8cc03fd8c4192af9201a7ff3cd979", - "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", - "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.py:md5,b629adc64e497622f000945ee6e6aed2", - "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", - "style.py:md5,b9f1207f06464e43c05e6e3912f12731", - "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", "all_gene_ids.txt:md5,6b2ece983fd9da133e719914216852b0", "global_gene_id_mapping.csv:md5,78934d2ac5fe7d863f114c5703f57a06", "global_gene_metadata.csv:md5,bd76860b422e45eca7cd583212a977d2", @@ -264,7 +243,7 @@ "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-15T16:45:46.754894228" + "timestamp": "2025-12-23T12:18:28.957913038" }, "-profile test_eatlas_only_with_keywords": { "content": [ @@ -391,8 +370,6 @@ "dash_app/data/all_genes_summary.csv", "dash_app/data/whole_design.csv", "dash_app/environment.yml", - "dash_app/file_system_backend", - "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", "dash_app/src", "dash_app/src/callbacks", "dash_app/src/callbacks/common.py", @@ -413,7 +390,6 @@ "dash_app/src/utils/config.py", "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", - "dash_app/versions.yml", "errors", "idmapping", "idmapping/collected_gene_ids", @@ -507,28 +483,10 @@ "all_genes_summary.csv:md5,32be3aeed5cd2715847ccc2fd45c09a0", "most_stable_genes_summary.csv:md5,2e34161e2beb2f1e0921dbf6c0ce55ba", "most_stable_genes_transposed_counts_filtered.csv:md5,5083c04020d1c504c86689e14f731ef7", - "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", "all_genes_summary.csv:md5,32be3aeed5cd2715847ccc2fd45c09a0", "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", - "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", - "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", - "genes.py:md5,680cb5f4e107a3b091821917d72a555c", - "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", - "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", - "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", - "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", - "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", - "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", - "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", - "tables.py:md5,92e8cc03fd8c4192af9201a7ff3cd979", - "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", - "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.py:md5,b629adc64e497622f000945ee6e6aed2", - "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", - "style.py:md5,b9f1207f06464e43c05e6e3912f12731", - "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", "all_gene_ids.txt:md5,ba79f5609df755c0f75de41357319c84", "global_gene_id_mapping.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", "global_gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", @@ -541,8 +499,6 @@ "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,b407990bb10106de943f9e93cda6323e", "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", - "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", - "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", "id_mapping_stats.csv:md5,6189d2a346cce55f182e00769e3fea5f", @@ -554,7 +510,7 @@ "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-15T16:51:02.033842652" + "timestamp": "2025-12-23T12:25:51.249379198" }, "-profile test_included_and_excluded_accessions": { "content": [ @@ -681,8 +637,6 @@ "dash_app/data/all_genes_summary.csv", "dash_app/data/whole_design.csv", "dash_app/environment.yml", - "dash_app/file_system_backend", - "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", "dash_app/src", "dash_app/src/callbacks", "dash_app/src/callbacks/common.py", @@ -703,7 +657,6 @@ "dash_app/src/utils/config.py", "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", - "dash_app/versions.yml", "errors", "errors/eatlas_failure_reasons.csv", "errors/renaming_failure_reasons.tsv", @@ -851,28 +804,10 @@ "all_genes_summary.csv:md5,22e424fe9e669c8c38997f1f44fc43ac", "most_stable_genes_summary.csv:md5,049a1fae30bafa22cd91fa906bb33164", "most_stable_genes_transposed_counts_filtered.csv:md5,7f70c275cedb0ec9ca9775b14c051105", - "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", "all_genes_summary.csv:md5,22e424fe9e669c8c38997f1f44fc43ac", "whole_design.csv:md5,cc24405dce8d22b93b9999a2287113ef", "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", - "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", - "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", - "genes.py:md5,680cb5f4e107a3b091821917d72a555c", - "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", - "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", - "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", - "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", - "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", - "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", - "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", - "tables.py:md5,92e8cc03fd8c4192af9201a7ff3cd979", - "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", - "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.py:md5,b629adc64e497622f000945ee6e6aed2", - "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", - "style.py:md5,b9f1207f06464e43c05e6e3912f12731", - "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", "eatlas_failure_reasons.csv:md5,2a8cd0ed795e82647d19c484a79acde6", "renaming_failure_reasons.tsv:md5,3d83b7001eb0554a7e988f770f06d2b1", "all_gene_ids.txt:md5,e9681582a09fe58f5258977db1d9da3f", @@ -898,8 +833,6 @@ "E_MTAB_552_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,256158add9a0ee0cfcc800104dcaeeae", "E_MTAB_7711_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,8731c38eeb16bbbb6fb87f5a80665efd", "accessions.txt:md5,e38a0aaf5191ba5f94cb7a96b8d30aa7", - "selected_experiments.metadata.tsv:md5,15baa430176fcadf7bf03034fa9e95c6", - "species_experiments.metadata.tsv:md5,15baa430176fcadf7bf03034fa9e95c6", "E_GEOD_61690_rnaseq.design.csv:md5,ba807caf74e0b55f5c3fe23810a89560", "E_GEOD_61690_rnaseq.rnaseq.raw.counts.csv:md5,2e3f1a125b3d41d622e2d24447620eb3", "E_GEOD_77826_rnaseq.design.csv:md5,5aa61df754aa9c6c107b247c642d2e53", @@ -924,7 +857,7 @@ "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-16T14:07:29.658396871" + "timestamp": "2025-12-23T12:32:33.899496741" }, "-profile test_skip_id_mapping": { "content": [ @@ -983,7 +916,7 @@ "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-12T19:43:58.840498623" + "timestamp": "2025-12-23T12:26:45.717138449" }, "-profile test": { "content": [ @@ -1002,8 +935,6 @@ "dash_app/data/all_genes_summary.csv", "dash_app/data/whole_design.csv", "dash_app/environment.yml", - "dash_app/file_system_backend", - "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", "dash_app/src", "dash_app/src/callbacks", "dash_app/src/callbacks/common.py", @@ -1024,7 +955,6 @@ "dash_app/src/utils/config.py", "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", - "dash_app/versions.yml", "errors", "geo", "idmapping", @@ -1150,28 +1080,10 @@ "all_genes_summary.csv:md5,ec52af6957845539143919c0e6eec89c", "most_stable_genes_summary.csv:md5,81d590008d5582658d307bf8c101e60a", "most_stable_genes_transposed_counts_filtered.csv:md5,9b52b81241cb4f57be781e29afe52cdc", - "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", "all_genes_summary.csv:md5,ec52af6957845539143919c0e6eec89c", "whole_design.csv:md5,3c1e14c9bd7ad250326b070a0dd4d81f", "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", - "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", - "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", - "genes.py:md5,680cb5f4e107a3b091821917d72a555c", - "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", - "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", - "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", - "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", - "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", - "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", - "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", - "tables.py:md5,92e8cc03fd8c4192af9201a7ff3cd979", - "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", - "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.py:md5,b629adc64e497622f000945ee6e6aed2", - "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", - "style.py:md5,b9f1207f06464e43c05e6e3912f12731", - "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", "all_gene_ids.txt:md5,ba79f5609df755c0f75de41357319c84", "global_gene_id_mapping.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", "global_gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", @@ -1186,14 +1098,10 @@ "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,b407990bb10106de943f9e93cda6323e", "beta_vulgaris.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,2c326f3e419341f955bb757fc8bf4357", "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", - "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", - "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", "accessions.txt:md5,63a651d9df354aef24400cebe56dd5ec", - "geo_all_datasets.metadata.tsv:md5,689fdd3683d0bfaa174b648def695dc7", - "geo_rejected_datasets.metadata.tsv:md5,660763cc38454d85ad96cbda1a69391a", - "geo_selected_datasets.metadata.tsv:md5,5fb3140b07aa92b5bcbfaa00c1c8396a", + "warning_reason.txt:md5,603e30b732b7a6b501b59adf9d0e8837", "id_mapping_stats.csv:md5,7c20b1e561989fcd2ce038c4a061caa5", "ratio_zeros.csv:md5,9794647ae1d7c87ec212c0c12b658d4e", "skewness.csv:md5,7e1ecb86c9c51394a0dacfdaca05899b", @@ -1204,7 +1112,7 @@ "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-18T14:36:48.357141617" + "timestamp": "2025-12-23T12:10:41.648442347" }, "-profile test_dataset_custom_mapping_and_gene_length": { "content": [ @@ -1305,32 +1213,16 @@ "dash_app/data/all_genes_summary.csv", "dash_app/data/whole_design.csv", "dash_app/environment.yml", - "dash_app/file_system_backend", - "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", "dash_app/src", "dash_app/src/callbacks", - "dash_app/src/callbacks/__pycache__", - "dash_app/src/callbacks/__pycache__/common.cpython-313.pyc", - "dash_app/src/callbacks/__pycache__/genes.cpython-313.pyc", - "dash_app/src/callbacks/__pycache__/samples.cpython-313.pyc", "dash_app/src/callbacks/common.py", "dash_app/src/callbacks/genes.py", "dash_app/src/callbacks/samples.py", "dash_app/src/components", - "dash_app/src/components/__pycache__", - "dash_app/src/components/__pycache__/graphs.cpython-313.pyc", - "dash_app/src/components/__pycache__/right_sidebar.cpython-313.pyc", - "dash_app/src/components/__pycache__/stores.cpython-313.pyc", - "dash_app/src/components/__pycache__/tables.cpython-313.pyc", - "dash_app/src/components/__pycache__/tooltips.cpython-313.pyc", - "dash_app/src/components/__pycache__/top.cpython-313.pyc", "dash_app/src/components/graphs.py", "dash_app/src/components/icons.py", "dash_app/src/components/right_sidebar.py", "dash_app/src/components/settings", - "dash_app/src/components/settings/__pycache__", - "dash_app/src/components/settings/__pycache__/genes.cpython-313.pyc", - "dash_app/src/components/settings/__pycache__/samples.cpython-313.pyc", "dash_app/src/components/settings/genes.py", "dash_app/src/components/settings/samples.py", "dash_app/src/components/stores.py", @@ -1338,14 +1230,9 @@ "dash_app/src/components/tooltips.py", "dash_app/src/components/top.py", "dash_app/src/utils", - "dash_app/src/utils/__pycache__", - "dash_app/src/utils/__pycache__/config.cpython-313.pyc", - "dash_app/src/utils/__pycache__/data_management.cpython-313.pyc", - "dash_app/src/utils/__pycache__/style.cpython-313.pyc", "dash_app/src/utils/config.py", "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", - "dash_app/versions.yml", "errors", "idmapping", "idmapping/global_gene_id_mapping.csv", @@ -1420,42 +1307,10 @@ "all_genes_summary.csv:md5,03b370a040cac715b58c31865e7c8557", "most_stable_genes_summary.csv:md5,03b370a040cac715b58c31865e7c8557", "most_stable_genes_transposed_counts_filtered.csv:md5,9ee131e180ccaa879342af5873cdcf19", - "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", "all_genes_summary.csv:md5,03b370a040cac715b58c31865e7c8557", "whole_design.csv:md5,70d6c2673e619ca52d2774fb3e368382", "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", - "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", - "common.cpython-313.pyc:md5,84e191f4cc04fb26dfdbe53c3064fb49", - "genes.cpython-313.pyc:md5,5b4cea3577ad8b61b95f310095aa8aab", - "samples.cpython-313.pyc:md5,2f171003867f80ba4cbfe0cc7e04c279", - "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", - "genes.py:md5,680cb5f4e107a3b091821917d72a555c", - "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", - "graphs.cpython-313.pyc:md5,19857cc37de34c37fb5c4a6115f9ed99", - "right_sidebar.cpython-313.pyc:md5,2f35bdaf8b8106a4e5fc821fb85d484e", - "stores.cpython-313.pyc:md5,9475873426d1f241a6967069a1ac9b91", - "tables.cpython-313.pyc:md5,b65b8472259e227bae1363175a1cc59f", - "tooltips.cpython-313.pyc:md5,53d1e39e3e00c534778961ec9afc14cb", - "top.cpython-313.pyc:md5,5d2bac677772dad8e8d230f1dd0c0983", - "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", - "icons.py:md5,3e66bae5ceca3dfbd190d88db0dc0828", - "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", - "genes.cpython-313.pyc:md5,345e0b2804515e5209ac9170ce6f47e6", - "samples.cpython-313.pyc:md5,d5fe51e17f26d857ae0a622edd7d21d7", - "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", - "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", - "stores.py:md5,6e829f7950f09c7c9b10568de41baca2", - "tables.py:md5,92e8cc03fd8c4192af9201a7ff3cd979", - "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", - "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.cpython-313.pyc:md5,fbae7ce40027422c58a8b7df113643dc", - "data_management.cpython-313.pyc:md5,d13d56de0a07961ac180bf23ff6df852", - "style.cpython-313.pyc:md5,acd51533122124e35b9baf578092ccf1", - "config.py:md5,b629adc64e497622f000945ee6e6aed2", - "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", - "style.py:md5,b9f1207f06464e43c05e6e3912f12731", - "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", "global_gene_id_mapping.csv:md5,187a86074197044846bb8565e122eb8e", "global_gene_metadata.csv:md5,5ae2d701ca0cb6384d9e1e08a345e452", "microarray.normalised.renamed.csv:md5,6adb74d67379a5a3d3309b10a0c4bec5", @@ -1473,7 +1328,7 @@ "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-22T15:32:07.792328885" + "timestamp": "2025-12-23T12:28:01.788499112" }, "-profile test_accessions_only": { "content": [ @@ -1526,16 +1381,14 @@ "warnings" ], [ - "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", - "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", - "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2" + "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a" ] ], "meta": { "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-11T14:37:17.749448911" + "timestamp": "2025-12-23T12:18:58.308749506" }, "-profile test_one_accession_low_gene_count": { "content": [ @@ -1657,8 +1510,6 @@ "dash_app/data/all_genes_summary.csv", "dash_app/data/whole_design.csv", "dash_app/environment.yml", - "dash_app/file_system_backend", - "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", "dash_app/src", "dash_app/src/callbacks", "dash_app/src/callbacks/common.py", @@ -1679,7 +1530,6 @@ "dash_app/src/utils/config.py", "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", - "dash_app/versions.yml", "errors", "idmapping", "idmapping/collected_gene_ids", @@ -1761,28 +1611,10 @@ "all_genes_summary.csv:md5,8308c2f930f305e3db913d992a6acf36", "most_stable_genes_summary.csv:md5,bd5c71953b259d05d024f68eb4b62942", "most_stable_genes_transposed_counts_filtered.csv:md5,d6a99e3a8a422af722dea852d84bdc94", - "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", "all_genes_summary.csv:md5,8308c2f930f305e3db913d992a6acf36", "whole_design.csv:md5,d3aa542c4ad07d0051a84482fe6cd81c", "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", - "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", - "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", - "genes.py:md5,680cb5f4e107a3b091821917d72a555c", - "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", - "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", - "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", - "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", - "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", - "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", - "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", - "tables.py:md5,92e8cc03fd8c4192af9201a7ff3cd979", - "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", - "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.py:md5,b629adc64e497622f000945ee6e6aed2", - "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", - "style.py:md5,b9f1207f06464e43c05e6e3912f12731", - "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", "all_gene_ids.txt:md5,9c463e6c1754d4f4c7ea684578aa6849", "global_gene_id_mapping.csv:md5,42491ef436cce231258c0358e1af5745", "global_gene_metadata.csv:md5,b35e20500269d4e6787ef1a3468f16bc", @@ -1805,7 +1637,7 @@ "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-15T16:48:56.125523436" + "timestamp": "2025-12-23T12:22:56.224515953" }, "-profile test_bigger_with_genorm": { "content": [ @@ -1952,8 +1784,6 @@ "dash_app/data/all_genes_summary.csv", "dash_app/data/whole_design.csv", "dash_app/environment.yml", - "dash_app/file_system_backend", - "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", "dash_app/src", "dash_app/src/callbacks", "dash_app/src/callbacks/common.py", @@ -1974,7 +1804,6 @@ "dash_app/src/utils/config.py", "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", - "dash_app/versions.yml", "errors", "idmapping", "idmapping/collected_gene_ids", @@ -2068,28 +1897,10 @@ "all_genes_summary.csv:md5,d4b5d36061b8fd31ba27edc55f738a01", "most_stable_genes_summary.csv:md5,b47891fe0414abd0a4b7c9137d53fc1d", "most_stable_genes_transposed_counts_filtered.csv:md5,46d96c944cc9ca3dd83e16da52c528f2", - "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", "all_genes_summary.csv:md5,d4b5d36061b8fd31ba27edc55f738a01", "whole_design.csv:md5,c9bfd7bc8ca365222e03e67eb24b9a76", "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", - "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", - "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", - "genes.py:md5,680cb5f4e107a3b091821917d72a555c", - "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", - "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", - "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", - "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", - "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", - "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", - "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", - "tables.py:md5,92e8cc03fd8c4192af9201a7ff3cd979", - "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", - "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.py:md5,b629adc64e497622f000945ee6e6aed2", - "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", - "style.py:md5,b9f1207f06464e43c05e6e3912f12731", - "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", "all_gene_ids.txt:md5,13ae1b52833134f8ed6d982c00487927", "global_gene_id_mapping.csv:md5,efc95a8e276be1eb0af9639f72e48145", "global_gene_metadata.csv:md5,1d342577587bc48c1eff077a594929fa", @@ -2102,8 +1913,6 @@ "whole_design.csv:md5,c9bfd7bc8ca365222e03e67eb24b9a76", "E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,4a2aa87d0bd2990c13e088f0117cc682", "accessions.txt:md5,561a967c16b2ef6c29fb643cd4002947", - "selected_experiments.metadata.tsv:md5,385b4d7ed40829f1e764857e805d6dc4", - "species_experiments.metadata.tsv:md5,385b4d7ed40829f1e764857e805d6dc4", "E_MTAB_5072_rnaseq.design.csv:md5,a1f33d126dde387a2d542381c44bc1f3", "E_MTAB_5072_rnaseq.rnaseq.raw.counts.csv:md5,c41c84899a380515d759b99eeccfe43e", "id_mapping_stats.csv:md5,6a84babdbb434ffd5975fc771ec2db44", @@ -2115,7 +1924,7 @@ "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-16T14:13:50.668342031" + "timestamp": "2025-12-23T12:41:00.400997562" }, "-profile test_download_only": { "content": [ @@ -2174,8 +1983,6 @@ ], [ "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", - "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", - "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1" ] @@ -2184,7 +1991,7 @@ "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-11T10:42:14.842517715" + "timestamp": "2025-12-23T12:19:38.305186676" }, "-profile test_gprofiler_target_database_entrez": { "content": [ @@ -2203,8 +2010,6 @@ "dash_app/data/all_genes_summary.csv", "dash_app/data/whole_design.csv", "dash_app/environment.yml", - "dash_app/file_system_backend", - "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", "dash_app/src", "dash_app/src/callbacks", "dash_app/src/callbacks/common.py", @@ -2225,7 +2030,6 @@ "dash_app/src/utils/config.py", "dash_app/src/utils/data_management.py", "dash_app/src/utils/style.py", - "dash_app/versions.yml", "errors", "idmapping", "idmapping/collected_gene_ids", @@ -2319,28 +2123,10 @@ "all_genes_summary.csv:md5,32be3aeed5cd2715847ccc2fd45c09a0", "most_stable_genes_summary.csv:md5,2e34161e2beb2f1e0921dbf6c0ce55ba", "most_stable_genes_transposed_counts_filtered.csv:md5,5083c04020d1c504c86689e14f731ef7", - "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", "all_genes_summary.csv:md5,32be3aeed5cd2715847ccc2fd45c09a0", "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", - "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", - "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", - "genes.py:md5,680cb5f4e107a3b091821917d72a555c", - "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", - "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", - "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", - "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", - "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", - "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", - "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", - "tables.py:md5,92e8cc03fd8c4192af9201a7ff3cd979", - "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", - "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.py:md5,b629adc64e497622f000945ee6e6aed2", - "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", - "style.py:md5,b9f1207f06464e43c05e6e3912f12731", - "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", "all_gene_ids.txt:md5,ba79f5609df755c0f75de41357319c84", "global_gene_id_mapping.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", "global_gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", @@ -2353,8 +2139,6 @@ "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,b407990bb10106de943f9e93cda6323e", "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", - "selected_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", - "species_experiments.metadata.tsv:md5,cf220f0d0aab141abf220c856430f2f2", "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", "id_mapping_stats.csv:md5,6189d2a346cce55f182e00769e3fea5f", @@ -2366,254 +2150,6 @@ "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-15T16:58:27.415680085" - }, - "-profile test_dataset_custom_mapping": { - "content": [ - { - "AGGREGATE_RESULTS": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "COLLECT_STATISTICS": { - "pandas": "2.3.3", - "python": "3.13.7" - }, - "COMPUTE_BASE_STATISTICS": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "COMPUTE_BASE_STATISTICS_FOR_MICROARRAY": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "COMPUTE_BASE_STATISTICS_FOR_RNASEQ": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "COMPUTE_DATASET_STATISTICS": { - "pandas": "2.3.3", - "python": "3.13.7" - }, - "COMPUTE_GENE_TRANSCRIPT_LENGTHS": { - "pandas": "2.3.3", - "python": "3.13.7" - }, - "COMPUTE_STABILITY_SCORES": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "COMPUTE_TPM": { - "pandas": "2.3.3", - "python": "3.13.7" - }, - "DASH_APP": { - "python": "3.13.8", - "dash": "3.2.0", - "dash-extensions": "2.0.4", - "dash-mantine-components": "2.3.0", - "dash-ag-grid": "32.3.2", - "polars": "1.35.0", - "pandas": "2.3.3", - "pyarrow": "22.0.0", - "scipy": "1.16.3" - }, - "DOWNLOAD_ENSEMBL_ANNOTATION": { - "bs4": "4.14.2", - "pandas": "2.3.3", - "python": "3.14.0", - "requests": "2.32.5", - "tqdm": "4.67.1" - }, - "GET_CANDIDATE_GENES": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "MERGE_ALL_COUNTS": { - "polars": "1.34.0", - "python": "3.14.0", - "tqdm": "4.67.1" - }, - "MERGE_MICROARRAY_COUNTS": { - "polars": "1.34.0", - "python": "3.14.0", - "tqdm": "4.67.1" - }, - "MERGE_RNASEQ_COUNTS": { - "polars": "1.34.0", - "python": "3.14.0", - "tqdm": "4.67.1" - }, - "NORMFINDER": { - "polars": "1.33.1", - "python": "3.13.7" - }, - "QUANTILE_NORMALISATION": { - "pandas": "2.2.3", - "pyarrow": "19.0.0", - "python": "3.12.8", - "scikit-learn": "1.6.1" - }, - "RENAME_GENE_IDS": { - "pandas": "2.3.3", - "polars": "1.35.2", - "python": "3.14.0" - }, - "Workflow": { - "nf-core/stableexpression": "v1.0dev" - } - }, - [ - "aggregated", - "aggregated/all_counts_filtered.parquet", - "aggregated/all_genes_summary.csv", - "aggregated/most_stable_genes_summary.csv", - "aggregated/most_stable_genes_transposed_counts_filtered.csv", - "dash_app", - "dash_app/app.py", - "dash_app/assets", - "dash_app/assets/style.css", - "dash_app/data", - "dash_app/data/all_counts.parquet", - "dash_app/data/all_genes_summary.csv", - "dash_app/data/whole_design.csv", - "dash_app/environment.yml", - "dash_app/file_system_backend", - "dash_app/file_system_backend/2029240f6d1128be89ddc32729463129", - "dash_app/src", - "dash_app/src/callbacks", - "dash_app/src/callbacks/common.py", - "dash_app/src/callbacks/genes.py", - "dash_app/src/callbacks/samples.py", - "dash_app/src/components", - "dash_app/src/components/graphs.py", - "dash_app/src/components/icons.py", - "dash_app/src/components/right_sidebar.py", - "dash_app/src/components/settings", - "dash_app/src/components/settings/genes.py", - "dash_app/src/components/settings/samples.py", - "dash_app/src/components/stores.py", - "dash_app/src/components/tables.py", - "dash_app/src/components/tooltips.py", - "dash_app/src/components/top.py", - "dash_app/src/utils", - "dash_app/src/utils/config.py", - "dash_app/src/utils/data_management.py", - "dash_app/src/utils/style.py", - "dash_app/versions.yml", - "errors", - "idmapping", - "idmapping/global_gene_id_mapping.csv", - "idmapping/global_gene_metadata.csv", - "idmapping/renamed", - "idmapping/renamed/microarray.normalised.renamed.csv", - "idmapping/renamed/rnaseq.raw.renamed.csv", - "idmapping/whole_gene_id_mapping.csv", - "idmapping/whole_gene_metadata.csv", - "merged_datasets", - "merged_datasets/whole_design.csv", - "multiqc", - "multiqc/multiqc_data", - "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_expression_distributions_most_stable_genes.txt", - "multiqc/multiqc_data/multiqc_gene_statistics.txt", - "multiqc/multiqc_data/multiqc_id_mapping_stats.txt", - "multiqc/multiqc_data/multiqc_ranked_most_stable_genes_summary.txt", - "multiqc/multiqc_data/multiqc_ratio_zeros.txt", - "multiqc/multiqc_data/multiqc_skewness.txt", - "multiqc/multiqc_data/multiqc_software_versions.txt", - "multiqc/multiqc_data/multiqc_sources.txt", - "multiqc/multiqc_plots", - "multiqc/multiqc_plots/pdf", - "multiqc/multiqc_plots/pdf/expression_distributions_most_stable_genes.pdf", - "multiqc/multiqc_plots/pdf/gene_statistics.pdf", - "multiqc/multiqc_plots/pdf/id_mapping_stats-cnt.pdf", - "multiqc/multiqc_plots/pdf/id_mapping_stats-pct.pdf", - "multiqc/multiqc_plots/pdf/ranked_most_stable_genes_summary.pdf", - "multiqc/multiqc_plots/pdf/ratio_zeros.pdf", - "multiqc/multiqc_plots/pdf/skewness.pdf", - "multiqc/multiqc_plots/png", - "multiqc/multiqc_plots/png/expression_distributions_most_stable_genes.png", - "multiqc/multiqc_plots/png/gene_statistics.png", - "multiqc/multiqc_plots/png/id_mapping_stats-cnt.png", - "multiqc/multiqc_plots/png/id_mapping_stats-pct.png", - "multiqc/multiqc_plots/png/ranked_most_stable_genes_summary.png", - "multiqc/multiqc_plots/png/ratio_zeros.png", - "multiqc/multiqc_plots/png/skewness.png", - "multiqc/multiqc_plots/svg", - "multiqc/multiqc_plots/svg/expression_distributions_most_stable_genes.svg", - "multiqc/multiqc_plots/svg/gene_statistics.svg", - "multiqc/multiqc_plots/svg/id_mapping_stats-cnt.svg", - "multiqc/multiqc_plots/svg/id_mapping_stats-pct.svg", - "multiqc/multiqc_plots/svg/ranked_most_stable_genes_summary.svg", - "multiqc/multiqc_plots/svg/ratio_zeros.svg", - "multiqc/multiqc_plots/svg/skewness.svg", - "multiqc/multiqc_report.html", - "multiqc/versions.yml", - "normalised", - "normalised/microarray.normalised", - "normalised/microarray.normalised/quantile_normalised", - "normalised/microarray.normalised/quantile_normalised/microarray.normalised.renamed.quant_norm.parquet", - "normalised/rnaseq.raw", - "normalised/rnaseq.raw/quantile_normalised", - "normalised/rnaseq.raw/quantile_normalised/rnaseq.raw.renamed.tpm.quant_norm.parquet", - "normalised/rnaseq.raw/tpm", - "normalised/rnaseq.raw/tpm/rnaseq.raw.renamed.tpm.csv", - "pipeline_info", - "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", - "statistics", - "statistics/id_mapping_stats.csv", - "statistics/ratio_zeros.csv", - "statistics/skewness.csv", - "warnings" - ], - [ - "all_genes_summary.csv:md5,03b370a040cac715b58c31865e7c8557", - "most_stable_genes_summary.csv:md5,03b370a040cac715b58c31865e7c8557", - "most_stable_genes_transposed_counts_filtered.csv:md5,9ee131e180ccaa879342af5873cdcf19", - "app.py:md5,bb43a37542f3525925e68e41f26b3eb3", - "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_genes_summary.csv:md5,03b370a040cac715b58c31865e7c8557", - "whole_design.csv:md5,70d6c2673e619ca52d2774fb3e368382", - "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", - "2029240f6d1128be89ddc32729463129:md5,5cc6eb1406d6403afe70dd790f5a929b", - "common.py:md5,2579b1cf40bc1f19bf4d6cf9e7ee3261", - "genes.py:md5,680cb5f4e107a3b091821917d72a555c", - "samples.py:md5,ebe1620fd6a7280933c6e82f05b04820", - "graphs.py:md5,07dc2cf0dcb46104787dc5b76660cc1e", - "icons.py:md5,7873cf09c55a36ad1a51ba17b95a0627", - "right_sidebar.py:md5,2f027584cf2cf862a28af95d4b2390d8", - "genes.py:md5,b521a8e3ffa1d9c15b4699f074fa2533", - "samples.py:md5,f1b4efe28957159ed66ae6149e5337b9", - "stores.py:md5,5c3a8b547efa4dc763a84fbf8297c0f0", - "tables.py:md5,92e8cc03fd8c4192af9201a7ff3cd979", - "tooltips.py:md5,3f6e77c5bc52d3ae8b64420c50f5daa1", - "top.py:md5,55ca3f695ac2d4c605fb29994ab18f53", - "config.py:md5,b629adc64e497622f000945ee6e6aed2", - "data_management.py:md5,5319d7240ec6eb7826b0ea99fa3aa98c", - "style.py:md5,b9f1207f06464e43c05e6e3912f12731", - "versions.yml:md5,62053b1d61b243110327dfc8d0750b84", - "global_gene_id_mapping.csv:md5,187a86074197044846bb8565e122eb8e", - "global_gene_metadata.csv:md5,5ae2d701ca0cb6384d9e1e08a345e452", - "microarray.normalised.renamed.csv:md5,6adb74d67379a5a3d3309b10a0c4bec5", - "rnaseq.raw.renamed.csv:md5,aa22384ba73d180629add4e174c7f37d", - "whole_gene_id_mapping.csv:md5,187a86074197044846bb8565e122eb8e", - "whole_gene_metadata.csv:md5,5ae2d701ca0cb6384d9e1e08a345e452", - "whole_design.csv:md5,70d6c2673e619ca52d2774fb3e368382", - "rnaseq.raw.renamed.tpm.csv:md5,dde1d8ea5d271c4e0486c7ba0936a972", - "id_mapping_stats.csv:md5,ee400af7734b2226406fc7ba986dccfa", - "ratio_zeros.csv:md5,d206e45c16e6bd13de75ea6d20bbd30d", - "skewness.csv:md5,9917b39dfe5ee6e680fa1783f8a096c4" - ] - ], - "meta": { - "nf-test": "0.9.3", - "nextflow": "25.10.2" - }, - "timestamp": "2025-12-16T12:51:18.807822899" + "timestamp": "2025-12-23T12:35:29.173977298" } } \ No newline at end of file diff --git a/tests/subworkflows/local/expression_normalisation/main.nf.test b/tests/subworkflows/local/expression_normalisation/main.nf.test index 33fbe3f5..b5f5b689 100644 --- a/tests/subworkflows/local/expression_normalisation/main.nf.test +++ b/tests/subworkflows/local/expression_normalisation/main.nf.test @@ -29,6 +29,45 @@ nextflow_workflow { input[1] = ch_datasets input[2] = "tpm" input[3] = "uniform" + input[4] = null + """ + } + } + + then { + assertAll( + { assert workflow.success }, + { assert snapshot(workflow.out).match() } + ) + } + + } + + test("TPM Normalisation with gene length") { + + when { + workflow { + """ + rnaseq_raw_file = file( '$projectDir/tests/test_data/input_datasets/rnaseq.raw.csv', checkIfExists: true ) + rnaseq_raw_design_file = file( '$projectDir/tests/test_data/input_datasets/rnaseq.raw.design.csv', checkIfExists: true ) + microarray_normalised_file = file( '$projectDir/tests/test_data/input_datasets/microarray.normalised.csv', checkIfExists: true ) + microarray_normalised_design_file = file( '$projectDir/tests/test_data/input_datasets/microarray.normalised.design.csv', checkIfExists: true ) + gene_length_file = file( '$projectDir/tests/test_data/input_datasets/gene_lengths.csv', checkIfExists: true ) + ch_datasets = channel.of( + [ + [normalised: false, design: rnaseq_raw_design_file, dataset: "rnaseq_raw", platform: "rnaseq"], + rnaseq_raw_file + ], + [ + [normalised: true, design: microarray_normalised_design_file, dataset: "microarray_normalised", platform: "microarray"], + microarray_normalised_file + ] + ) + input[0] = "solanum_tuberosum" + input[1] = ch_datasets + input[2] = "tpm" + input[3] = "uniform" + input[4] = gene_length_file """ } } @@ -65,6 +104,7 @@ nextflow_workflow { input[1] = ch_datasets input[2] = "cpm" input[3] = "uniform" + input[4] = null """ } } @@ -95,6 +135,7 @@ nextflow_workflow { input[1] = ch_datasets input[2] = "tpm " input[3] = "uniform" + input[4] = null """ } } diff --git a/tests/subworkflows/local/expression_normalisation/main.nf.test.snap b/tests/subworkflows/local/expression_normalisation/main.nf.test.snap index 0ebef24f..6c8417f5 100644 --- a/tests/subworkflows/local/expression_normalisation/main.nf.test.snap +++ b/tests/subworkflows/local/expression_normalisation/main.nf.test.snap @@ -83,6 +83,57 @@ }, "timestamp": "2025-12-03T18:34:48.109732612" }, + "TPM Normalisation with gene length": { + "content": [ + { + "0": [ + [ + { + "normalised": false, + "design": "rnaseq.raw.design.csv:md5,39470b02a211aff791f9e4851b017488", + "dataset": "rnaseq_raw", + "platform": "rnaseq" + }, + "rnaseq.raw.tpm.quant_norm.parquet:md5,0a12a61db30db8c70e609270d32bc33d" + ], + [ + { + "normalised": true, + "design": "microarray.normalised.design.csv:md5,9662cc2f58d86cc552d6ff9cf094dd67", + "dataset": "microarray_normalised", + "platform": "microarray" + }, + "microarray.normalised.quant_norm.parquet:md5,b32fba01e39c2e54458f3b6d1b5fafc1" + ] + ], + "counts": [ + [ + { + "normalised": false, + "design": "rnaseq.raw.design.csv:md5,39470b02a211aff791f9e4851b017488", + "dataset": "rnaseq_raw", + "platform": "rnaseq" + }, + "rnaseq.raw.tpm.quant_norm.parquet:md5,0a12a61db30db8c70e609270d32bc33d" + ], + [ + { + "normalised": true, + "design": "microarray.normalised.design.csv:md5,9662cc2f58d86cc552d6ff9cf094dd67", + "dataset": "microarray_normalised", + "platform": "microarray" + }, + "microarray.normalised.quant_norm.parquet:md5,b32fba01e39c2e54458f3b6d1b5fafc1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.2" + }, + "timestamp": "2025-12-23T09:56:09.269591811" + }, "TPM Normalisation": { "content": [ { From ddcb9e6a330f5e4a028cd9dceb1d550211ddd42f Mon Sep 17 00:00:00 2001 From: Olivier Date: Tue, 30 Dec 2025 09:33:45 +0100 Subject: [PATCH 244/258] add pyarrow in dash_app environmen.yml --- modules/local/dash_app/app/environment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/local/dash_app/app/environment.yml b/modules/local/dash_app/app/environment.yml index 98518853..590353ad 100644 --- a/modules/local/dash_app/app/environment.yml +++ b/modules/local/dash_app/app/environment.yml @@ -6,6 +6,7 @@ channels: dependencies: - conda-forge::pandas==2.3.3 - conda-forge::polars==1.35.2 + - conda-forge::pyarrow==22.0.0 - conda-forge::scipy==1.16.3 - conda-forge::dash==3.3.0 - conda-forge::dash-mantine-components==2.4.0 From 86faacecbda28fda9e76f860870091f5144c26a4 Mon Sep 17 00:00:00 2001 From: Olivier Date: Tue, 30 Dec 2025 14:55:01 +0100 Subject: [PATCH 245/258] add subworkflow to filter out samplesthat are not valid --- bin/remove_samples_not_valid.py | 83 +++++++++++++++++++ .../local/compute_dataset_statistics/main.nf | 2 +- .../remove_samples_not_valid/environment.yml | 7 ++ .../local/remove_samples_not_valid/main.nf | 26 ++++++ subworkflows/local/filter_datasets/main.nf | 26 ++++++ .../main.nf | 4 +- workflows/stableexpression.nf | 11 ++- 7 files changed, 154 insertions(+), 5 deletions(-) create mode 100755 bin/remove_samples_not_valid.py create mode 100644 modules/local/remove_samples_not_valid/environment.yml create mode 100644 modules/local/remove_samples_not_valid/main.nf create mode 100644 subworkflows/local/filter_datasets/main.nf diff --git a/bin/remove_samples_not_valid.py b/bin/remove_samples_not_valid.py new file mode 100755 index 00000000..ac8ddbda --- /dev/null +++ b/bin/remove_samples_not_valid.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 + +# Written by Olivier Coen. Released under the MIT license. + +import argparse +import logging +import sys +from pathlib import Path + +import pandas as pd + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +OUTFILE_SUFFIX = ".filtered.csv" + +MAX_RATIO_ZEROS = 0.9 + + +##################################################### +##################################################### +# FUNCTIONS +##################################################### +##################################################### + + +def parse_args(): + parser = argparse.ArgumentParser(description="Filter out samples not valid") + parser.add_argument( + "--counts", type=Path, dest="count_file", required=True, help="Count file" + ) + return parser.parse_args() + + +def parse_counts(file: Path): + if file.suffix == ".csv": + return pd.read_csv(file, header=0, index_col=0) + else: # .tsv + return pd.read_csv(file, header=0, sep="\t", index_col=0) + + +def filter_out_columns_with_high_zero_ratio(df: pd.DataFrame, max_ratio_zeros: float): + zero_ratio = df.eq(0).mean(axis=0) + return df.loc[:, zero_ratio <= max_ratio_zeros] + + +def export_data(df: pd.DataFrame, outfile: Path): + logger.info(f"Exporting filtered counts to: {outfile}") + df.to_csv(outfile, index=True, header=True) + logger.info("Done") + + +##################################################### +##################################################### +# MAIN +##################################################### +##################################################### + + +def main(): + args = parse_args() + + # putting all counts into a single dataframe + logger.info("Loading count data...") + count_df = parse_counts(args.count_file) + logger.info( + f"Loaded count data with {len(count_df)} rows and {count_df.shape[1]} columns" + ) + + valid_count_df = filter_out_columns_with_high_zero_ratio(count_df, MAX_RATIO_ZEROS) + if valid_count_df.shape[1] == 0: + logger.error("No valid columns remaining") + sys.exit(0) + else: + logger.info( + f"Filtered out {count_df.shape[1] - valid_count_df.shape[1]} columns" + ) + outfile = args.count_file.with_suffix(OUTFILE_SUFFIX) + export_data(count_df, outfile) + + +if __name__ == "__main__": + main() diff --git a/modules/local/compute_dataset_statistics/main.nf b/modules/local/compute_dataset_statistics/main.nf index 3547515e..c5660fb9 100644 --- a/modules/local/compute_dataset_statistics/main.nf +++ b/modules/local/compute_dataset_statistics/main.nf @@ -21,7 +21,7 @@ process COMPUTE_DATASET_STATISTICS { script: def prefix = task.ext.prefix ?: "${meta.dataset}" """ - compute_dataset_statistics.py \ + compute_dataset_statistics.py \\ --counts $count_file """ diff --git a/modules/local/remove_samples_not_valid/environment.yml b/modules/local/remove_samples_not_valid/environment.yml new file mode 100644 index 00000000..104de0e2 --- /dev/null +++ b/modules/local/remove_samples_not_valid/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::pandas==2.3.3 diff --git a/modules/local/remove_samples_not_valid/main.nf b/modules/local/remove_samples_not_valid/main.nf new file mode 100644 index 00000000..020b1fd6 --- /dev/null +++ b/modules/local/remove_samples_not_valid/main.nf @@ -0,0 +1,26 @@ +process REMOVE_SAMPLES_NOT_VALID { + + label 'process_single' + + tag "${meta.dataset}" + + 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/3d/3d7126100b0eb7cb53dfb50291707ea8dda3b9738b76551ab73605d0acbe114b/data': + 'community.wave.seqera.io/library/pandas:2.3.3--5a902bf824a79745' }" + + input: + tuple val(meta), path(count_file) + + output: + tuple val(meta), path("*.filtered.csv"), optional: true, emit: counts + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions + tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions + + script: + """ + remove_samples_not_valid.py \\ + --counts $count_file + """ + +} diff --git a/subworkflows/local/filter_datasets/main.nf b/subworkflows/local/filter_datasets/main.nf new file mode 100644 index 00000000..fbe44e88 --- /dev/null +++ b/subworkflows/local/filter_datasets/main.nf @@ -0,0 +1,26 @@ +include { REMOVE_SAMPLES_NOT_VALID } from '../../../modules/local/remove_samples_not_valid' + + +/* +======================================================================================== + SUBWORKFLOW TO DOWNLOAD EXPRESSIONATLAS ACCESSIONS AND DATASETS +======================================================================================== +*/ + +workflow FILTER_DATASETS { + + take: + ch_counts + + main: + + // ----------------------------------------------------------------- + // REMOVE SAMPLES WITH TOO MANY ZEROS + // ----------------------------------------------------------------- + + REMOVE_SAMPLES_NOT_VALID ( ch_counts ) + + emit: + counts = REMOVE_SAMPLES_NOT_VALID.out.counts + +} diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index 9c34f75d..3ec4ffe4 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -449,10 +449,10 @@ def checkCounts(ch_counts) { def getWholeDatasetSize( ch_counts ) { return ch_counts .filter { meta, file -> - meta.nb_genes_after_idmapping > 0 && meta.nb_samples_after_idmapping > 0 + meta.nb_genes > 0 && meta.nb_samples > 0 } .map { meta, file -> - meta.nb_genes_after_idmapping * meta.nb_samples_after_idmapping + meta.nb_genes * meta.nb_samples } .reduce { size_1, size_2 -> size_1 + size_2 } .flatten() diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 73d1c77a..168955d4 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -7,6 +7,7 @@ include { GET_PUBLIC_ACCESSIONS } from '../subworkflows/local/get_public_accessions' include { DOWNLOAD_PUBLIC_DATASETS } from '../subworkflows/local/download_public_datasets' include { ID_MAPPING } from '../subworkflows/local/idmapping' +include { FILTER_DATASETS } from '../subworkflows/local/filter_datasets' include { EXPRESSION_NORMALISATION } from '../subworkflows/local/expression_normalisation' include { MERGE_DATA } from '../subworkflows/local/merge_data' include { BASE_STATISTICS } from '../subworkflows/local/base_statistics' @@ -106,7 +107,13 @@ workflow STABLEEXPRESSION { ch_gene_id_mapping = ID_MAPPING.out.mapping ch_gene_metadata = ID_MAPPING.out.metadata - ch_counts = storeDatasetSize( ch_counts, "nb_genes_after_idmapping", "nb_samples_after_idmapping" ) + ch_counts = storeDatasetSize( ch_counts, "nb_genes", "nb_samples" ) + + // ----------------------------------------------------------------- + // FILTER OUT SAMPLES NOT VALID + // ----------------------------------------------------------------- + + FILTER_DATASETS ( ch_counts ) // ----------------------------------------------------------------- // NORMALISATION OF RAW COUNT DATASETS (INCLUDING RNA-SEQ DATASETS) @@ -114,7 +121,7 @@ workflow STABLEEXPRESSION { EXPRESSION_NORMALISATION( species, - ch_counts, + FILTER_DATASETS.out.counts, params.normalisation_method, params.quantile_norm_target_distrib, params.gene_length From 44d03dda583d6d88ff18a32df3e3013ab8d08963 Mon Sep 17 00:00:00 2001 From: Olivier Date: Tue, 30 Dec 2025 15:03:26 +0100 Subject: [PATCH 246/258] fix issue when removing samples not valid --- assets/multiqc_config.yml | 2 +- bin/remove_samples_not_valid.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index 30b48441..164752dd 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -470,7 +470,7 @@ custom_data: sort_samples: true tt_decimals: 0 cpswitch: true # show the 'Counts / Percentages' switch - cpswitch_c_active: false # show percentages + cpswitch_c_active: true # show counts per default stacking: "relative" description: | Statistics of gene ID mapping, dataset per dataset diff --git a/bin/remove_samples_not_valid.py b/bin/remove_samples_not_valid.py index ac8ddbda..5acc0562 100755 --- a/bin/remove_samples_not_valid.py +++ b/bin/remove_samples_not_valid.py @@ -14,7 +14,7 @@ OUTFILE_SUFFIX = ".filtered.csv" -MAX_RATIO_ZEROS = 0.9 +MAX_RATIO_ZEROS = 0.75 ##################################################### @@ -76,7 +76,7 @@ def main(): f"Filtered out {count_df.shape[1] - valid_count_df.shape[1]} columns" ) outfile = args.count_file.with_suffix(OUTFILE_SUFFIX) - export_data(count_df, outfile) + export_data(valid_count_df, outfile) if __name__ == "__main__": From 047fbbc652143913c9ca78ff57ab8edd27219118 Mon Sep 17 00:00:00 2001 From: Olivier Date: Sat, 3 Jan 2026 14:52:55 +0100 Subject: [PATCH 247/258] pipeline lint --- ro-crate-metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ro-crate-metadata.json b/ro-crate-metadata.json index 4901a6cb..7532dcc9 100644 --- a/ro-crate-metadata.json +++ b/ro-crate-metadata.json @@ -23,7 +23,7 @@ "@type": "Dataset", "creativeWorkStatus": "InProgress", "datePublished": "2025-12-08T14:37:14+00:00", - "description": "

      \n \n \n \"nf-core/stableexpression\"\n \n

      \n\n[![Open in GitHub Codespaces](https://img.shields.io/badge/Open_In_GitHub_Codespaces-black?labelColor=grey&logo=github)](https://github.com/codespaces/new/nf-core/stableexpression)\n[![GitHub Actions CI Status](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml/badge.svg)](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml)\n[![GitHub Actions Linting Status](https://github.com/nf-core/stableexpression/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/stableexpression/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/stableexpression/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.04.0-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-3.5.1-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/3.5.1)\n[![run with apptainer](https://custom-icon-badges.demolab.com/badge/run%20with-apptainer-4545?logo=apptainer&color=teal&labelColor=000000)](https://apptainer.org/)\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/stableexpression)\n\n[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23stableexpression-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/stableexpression)[![Follow on Bluesky](https://img.shields.io/badge/bluesky-%40nf__core-1185fe?labelColor=000000&logo=bluesky)](https://bsky.app/profile/nf-co.re)[![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/stableexpression** is a bioinformatics pipeline aiming to aggregate multiple count datasets (public / provided by the user) for a specific species and find the most stable genes.\n\nIt takes as main inputs :\n\n- a species name (mandatory)\n- keywords for Expression Atlas / GEO search (optional)\n- a CSV input file listing your own raw / normalised count datasets (optional).\n\n**Use cases**:\n\n- **find the most suitable genes as RT-qPCR reference genes for a specific species (and optionally specific conditions)**\n- download all Expression Atlas and / or NCBI GEO datasets for a species (and optionally keywords)\n\n## Basic 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\nTo search the most stable genes in a species considering all public datasets, simply run:\n\n```bash\nnextflow run nf-core/stableexpression \\\n -profile \\\n --species \\\n --outdir \n```\n\n> [!IMPORTANT]\n> For more specific scenarios, **like fetching only specific conditions or using your own expression dataset(s)**, please refer to the [usage documentation](https://nf-co.re/stableexpression/usage).\n\n> [!NOTE]\n> See [here](https://nf-co.re/stableexpression/usage#profiles) for more information about profiles.\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/stableexpression/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/stableexpression/output).\n\n## Support us\n\nIf you like nf-core/stableexpression, please make sure you give it a star on GitHub.\n\n[![stars - stableexpression](https://img.shields.io/github/stars/nf-core/stableexpression?style=social)](https://github.com/nf-core/stableexpression)\n\n## Credits\n\nnf-core/stableexpression was originally written by Olivier Coen.\n\n\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 `#stableexpression` channel](https://nfcore.slack.com/channels/stableexpression) (you can join with [this invite](https://nf-co.re/join/slack)).\n\n## Citations\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", + "description": "

      \n \n \n \"nf-core/stableexpression\"\n \n

      \n\n[![Open in GitHub Codespaces](https://img.shields.io/badge/Open_In_GitHub_Codespaces-black?labelColor=grey&logo=github)](https://github.com/codespaces/new/nf-core/stableexpression)\n[![GitHub Actions CI Status](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml/badge.svg)](https://github.com/nf-core/stableexpression/actions/workflows/nf-test.yml)\n[![GitHub Actions Linting Status](https://github.com/nf-core/stableexpression/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/stableexpression/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/stableexpression/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.04.0-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-3.5.1-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/3.5.1)\n[![run with apptainer](https://custom-icon-badges.demolab.com/badge/run%20with-apptainer-4545?logo=apptainer&color=teal&labelColor=000000)](https://apptainer.org/)\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/stableexpression)\n\n[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23stableexpression-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/stableexpression)[![Follow on Bluesky](https://img.shields.io/badge/bluesky-%40nf__core-1185fe?labelColor=000000&logo=bluesky)](https://bsky.app/profile/nf-co.re)[![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/stableexpression** is a bioinformatics pipeline aiming to aggregate multiple count datasets (public / provided by the user) for a specific species and find the most stable genes.\n\n

      \n \n

      \n\nIt takes as main inputs :\n\n- a species name (mandatory)\n- keywords for Expression Atlas / GEO search (optional)\n- a CSV input file listing your own raw / normalised count datasets (optional).\n\n**Use cases**:\n\n- **find the most suitable genes as RT-qPCR reference genes for a specific species (and optionally specific conditions)**\n- download all Expression Atlas and / or NCBI GEO datasets for a species (and optionally keywords)\n\n## Basic 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\nTo search the most stable genes in a species considering all public datasets, simply run:\n\n```bash\nnextflow run nf-core/stableexpression \\\n -profile \\\n --species \\\n --outdir \n```\n\n## More advanced usage\n\nFor more specific scenarios, like:\n\n- **fetching only specific conditions**\n- **using your own expression dataset(s)**\n\nplease refer to the [usage documentation](https://nf-co.re/stableexpression/usage).\n\n## Profiles\n\nSee [here](https://nf-co.re/stableexpression/usage#profiles) for more information about profiles.\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/stableexpression/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/stableexpression/output).\n\n## Support us\n\nIf you like nf-core/stableexpression, please make sure you give it a star on GitHub.\n\n[![stars - stableexpression](https://img.shields.io/github/stars/nf-core/stableexpression?style=social)](https://github.com/nf-core/stableexpression)\n\n## Credits\n\nnf-core/stableexpression was originally written by Olivier Coen.\n\n\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 `#stableexpression` channel](https://nfcore.slack.com/channels/stableexpression) (you can join with [this invite](https://nf-co.re/join/slack)).\n\n## Citations\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" From 0116d68db8b024b1cc02b3c583f29a0965c03008 Mon Sep 17 00:00:00 2001 From: Olivier Date: Sat, 3 Jan 2026 14:53:47 +0100 Subject: [PATCH 248/258] add nb of merged gene IDs in multiqc id mapping stat graph --- assets/multiqc_config.yml | 11 +++++--- bin/rename_gene_ids.py | 25 +++++++++++++---- modules/local/rename_gene_ids/main.nf | 5 ++-- subworkflows/local/multiqc/main.nf | 39 +++++++++++++-------------- 4 files changed, 49 insertions(+), 31 deletions(-) diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index 164752dd..3fb3c02e 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -475,11 +475,14 @@ custom_data: description: | Statistics of gene ID mapping, dataset per dataset categories: - mapped: - name: "Mapped gene IDs" - color: "#10C995" + final: + name: "Nb final gene IDs" + color: "#2ABF96" + merged: + name: "Nb gene IDs merged with other IDs" + color: "#38B4F2" unmapped: - name: "Unmapped gene IDs" + name: "Nb unmapped gene IDs" color: "#E3224A" plot_type: "barplot" diff --git a/bin/rename_gene_ids.py b/bin/rename_gene_ids.py index ddb41fc2..cce7c86e 100755 --- a/bin/rename_gene_ids.py +++ b/bin/rename_gene_ids.py @@ -26,8 +26,9 @@ WARNING_REASON_FILE = "warning_reason.txt" FAILURE_REASON_FILE = "failure_reason.txt" -MAPPED_FILE_SUFFIX = "mapped.txt" UNMAPPED_FILE_SUFFIX = "unmapped.txt" +MERGED_FILE_SUFFIX = "merged.txt" +FINAL_FILE_SUFFIX = "final.txt" ################################################################## # FUNCTIONS @@ -104,6 +105,7 @@ def main(): # IMPORTANT: KEEPING ONLY GENES THAT HAVE BEEN CONVERTED # filtering the DataFrame to keep only the rows where the index can be mapped original_nb_genes = len(df) + rejected_df = df.filter(~pl.col(config.GENE_ID_COLNAME).is_in(mapping_dict.keys())) nb_unmapped_genes = len(rejected_df) @@ -111,17 +113,24 @@ def main(): df = df.filter(pl.col(config.GENE_ID_COLNAME).is_in(mapping_dict.keys())) nb_mapped_genes = len(df) - with open(MAPPED_FILE_SUFFIX, "w") as f: - f.write(str(nb_mapped_genes)) - with open(UNMAPPED_FILE_SUFFIX, "w") as f: f.write(str(nb_unmapped_genes)) if df.is_empty(): - msg = "NO GENES WERE MAPPED" + sample_size = min(5, nb_unmapped_genes) + example_rejected_genes = ( + rejected_df[config.GENE_ID_COLNAME].head(sample_size).to_list() + ) + msg = f"NO GENES WERE MAPPED. EXAMPLE OF GENE IDS: {example_rejected_genes}" logger.error(msg) with open(FAILURE_REASON_FILE, "w") as f: f.write(msg) + + with open(MERGED_FILE_SUFFIX, "w") as f: + f.write("0") + with open(FINAL_FILE_SUFFIX, "w") as f: + f.write("0") + sys.exit(0) if len(df) < original_nb_genes: @@ -165,6 +174,12 @@ def main(): pl.exclude(config.GENE_ID_COLNAME).mean() ) + nb_merged = nb_mapped_genes - len(df) + with open(MERGED_FILE_SUFFIX, "w") as f: + f.write(str(nb_merged)) + with open(FINAL_FILE_SUFFIX, "w") as f: + f.write(str(len(df))) + ############################################################# # WRITING OUTFILES ############################################################# diff --git a/modules/local/rename_gene_ids/main.nf b/modules/local/rename_gene_ids/main.nf index 58e09df7..e334b8a3 100644 --- a/modules/local/rename_gene_ids/main.nf +++ b/modules/local/rename_gene_ids/main.nf @@ -17,7 +17,7 @@ process RENAME_GENE_IDS { tuple val(meta), path('*.renamed.csv'), optional: true, emit: counts tuple val(meta.dataset), path("failure_reason.txt"), optional: true, topic: renaming_failure_reason tuple val(meta.dataset), path("warning_reason.txt"), optional: true, topic: renaming_warning_reason - tuple val(meta.dataset), env("NB_MAPPED"), env("NB_UNMAPPED"), topic: id_mapping_stats + tuple val(meta.dataset), env("NB_FINAL"), env("NB_MERGED"), env("NB_UNMAPPED"), topic: id_mapping_stats tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions @@ -35,8 +35,9 @@ process RENAME_GENE_IDS { --count-file "$count_file" \\ $mapping_arg - NB_MAPPED=\$(cat mapped.txt) NB_UNMAPPED=\$(cat unmapped.txt) + NB_MERGED=\$(cat merged.txt) + NB_FINAL=\$(cat final.txt) """ diff --git a/subworkflows/local/multiqc/main.nf b/subworkflows/local/multiqc/main.nf index da711a63..3c539930 100644 --- a/subworkflows/local/multiqc/main.nf +++ b/subworkflows/local/multiqc/main.nf @@ -31,11 +31,11 @@ workflow MULTIQC_WORKFLOW { ch_id_mapping_stats = channel.topic('id_mapping_stats') .collectFile( name: 'id_mapping_stats.csv', - seed: "Dataset,mapped,unmapped", + seed: "dataset,final,merged,unmapped", newLine: true, storeDir: "${outdir}/statistics/" ) { - item -> "${item[0]},${item[1]},${item[2]}" + item -> "${item[0]},${item[1]},${item[2]},${item[3]}" } ch_skewness = channel.topic('skewness') @@ -179,24 +179,23 @@ workflow MULTIQC_WORKFLOW { // MULTIQC FILES // ------------------------------------------------------------------------------------ - ch_multiqc_files - .mix( channel.topic('eatlas_all_datasets').collect() ) // single item - .mix( channel.topic('eatlas_selected_datasets').collect() ) // single item - .mix( channel.topic('geo_all_datasets').collect() ) // single item - .mix( channel.topic('geo_selected_datasets').collect() ) // single item - .mix( channel.topic('geo_rejected_datasets').collect() ) // single item - .mix( COLLECT_STATISTICS.out.csv ) - .mix( ch_id_mapping_stats ) - .mix( ch_eatlas_failure_reasons ) - .mix( ch_eatlas_warning_reasons ) - .mix( ch_geo_failure_reasons ) - .mix( ch_geo_warning_reasons ) - .mix( ch_id_cleaning_failure_reasons ) - .mix( ch_id_mapping_warning_reasons ) - .mix( ch_id_mapping_failure_reasons ) - .mix( ch_normalisation_failure_reasons ) - .mix( ch_normalisation_warning_reasons ) - .set { ch_multiqc_files } + ch_multiqc_files = ch_multiqc_files + .mix( channel.topic('eatlas_all_datasets').collect() ) // single item + .mix( channel.topic('eatlas_selected_datasets').collect() ) // single item + .mix( channel.topic('geo_all_datasets').collect() ) // single item + .mix( channel.topic('geo_selected_datasets').collect() ) // single item + .mix( channel.topic('geo_rejected_datasets').collect() ) // single item + .mix( COLLECT_STATISTICS.out.csv ) + .mix( ch_id_mapping_stats ) + .mix( ch_eatlas_failure_reasons ) + .mix( ch_eatlas_warning_reasons ) + .mix( ch_geo_failure_reasons ) + .mix( ch_geo_warning_reasons ) + .mix( ch_id_cleaning_failure_reasons ) + .mix( ch_id_mapping_warning_reasons ) + .mix( ch_id_mapping_failure_reasons ) + .mix( ch_normalisation_failure_reasons ) + .mix( ch_normalisation_warning_reasons ) // ------------------------------------------------------------------------------------ // VERSIONS From c96b9b733e555811b139db202e718105f51f1006 Mon Sep 17 00:00:00 2001 From: Olivier Date: Sat, 3 Jan 2026 19:04:25 +0100 Subject: [PATCH 249/258] big refactoring to filter out rare genes --- assets/multiqc_config.yml | 3 + bin/clean_gene_ids.py | 45 ++++---- bin/collect_gene_ids.py | 45 ++++---- bin/common.py | 52 +++++++++ bin/config.py | 1 + bin/get_genes_with_good_occurrence.py | 101 ++++++++++++++++++ bin/rename_gene_ids.py | 100 +++++++++-------- modules/local/clean_gene_ids/environment.yml | 1 - modules/local/clean_gene_ids/main.nf | 8 +- .../local/collect_gene_ids/environment.yml | 1 - modules/local/collect_gene_ids/main.nf | 11 +- .../filter_out_rare_genes/environment.yml | 7 ++ modules/local/filter_out_rare_genes/main.nf | 42 ++++++++ modules/local/rename_gene_ids/main.nf | 13 ++- nextflow.config | 1 + nextflow_schema.json | 9 ++ subworkflows/local/idmapping/main.nf | 58 +++++----- subworkflows/local/multiqc/main.nf | 4 +- tests/default.nf.test.snap | 30 +++--- workflows/stableexpression.nf | 1 + 20 files changed, 387 insertions(+), 146 deletions(-) create mode 100644 bin/common.py create mode 100755 bin/get_genes_with_good_occurrence.py create mode 100644 modules/local/filter_out_rare_genes/environment.yml create mode 100644 modules/local/filter_out_rare_genes/main.nf diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index 3fb3c02e..08225cc7 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -481,6 +481,9 @@ custom_data: merged: name: "Nb gene IDs merged with other IDs" color: "#38B4F2" + not_valid: + name: "Nb rare gene IDs removed" + color: "#38B4F2" unmapped: name: "Nb unmapped gene IDs" color: "#E3224A" diff --git a/bin/clean_gene_ids.py b/bin/clean_gene_ids.py index c5786141..ba99cba3 100755 --- a/bin/clean_gene_ids.py +++ b/bin/clean_gene_ids.py @@ -8,8 +8,8 @@ from pathlib import Path import config -import pandas as pd import polars as pl +from common import parse_count_table logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -19,7 +19,8 @@ # CONSTANTS ################################################################## -CLEANED_FILE_SUFFIX = ".cleaned.csv" +CLEANED_COUNTS_SUFFIX = ".cleaned.parquet" +CLEANED_GENE_IDS_SUFFIX = ".cleaned_gene_ids.txt" FAILURE_REASON_FILE = "failure_reason.txt" @@ -36,22 +37,6 @@ def parse_args(): return parser.parse_args() -def parse_table(file: Path): - if file.suffix == ".csv": - return pd.read_csv(file, header=0, index_col=0) - else: # .tsv - return pd.read_csv(file, header=0, sep="\t", index_col=0) - - -def parse_count_table(file: Path): - # transitting to pandas dataframe helps to avoid parsing errors - df = parse_table(file) - # whatever the name of the first col, rename it to "gene_id" - df.index.rename(config.GENE_ID_COLNAME, inplace=True) - df.index = df.index.astype(str) - return pl.from_pandas(df.reset_index()) - - def clean_ensembl_gene_id_versioning(df: pl.DataFrame): """ Clean Ensembl gene IDs by removing version numbers. @@ -111,13 +96,27 @@ def main(): sys.exit(0) ############################################################# - # WRITING OUTFILE + # WRITING RESULTS ############################################################# - # writing to output file - logger.info("Writing output file") - outfile = args.count_file.with_name(args.count_file.stem + CLEANED_FILE_SUFFIX) - df.write_csv(outfile) + logger.info("Writing cleaned IDs") + gene_ids_outfile = args.count_file.with_name( + args.count_file.stem + CLEANED_GENE_IDS_SUFFIX + ) + gene_ids = ( + df.select(config.GENE_ID_COLNAME) + .sort(config.GENE_ID_COLNAME) + .to_series() + .to_list() + ) + with open(gene_ids_outfile, "w") as fout: + fout.write("\n".join(gene_ids)) + + logger.info("Writing count file with cleaned IDs") + count_outfile = args.count_file.with_name( + args.count_file.stem + CLEANED_COUNTS_SUFFIX + ) + df.write_parquet(count_outfile) if __name__ == "__main__": diff --git a/bin/collect_gene_ids.py b/bin/collect_gene_ids.py index c5a535f7..ab51089f 100755 --- a/bin/collect_gene_ids.py +++ b/bin/collect_gene_ids.py @@ -4,15 +4,17 @@ import argparse import logging +from collections import Counter from pathlib import Path -import pandas as pd +import config from tqdm import tqdm logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -ALL_GENE_IDS_OUTFILE = "all_gene_ids.txt" +UNIQUE_GENE_IDS_OUTFILE = "unique_gene_ids.txt" +GENE_ID_OCCURRENCES_OUTFILE = "gene_id_occurrences.csv" ##################################################### @@ -25,7 +27,7 @@ def parse_args(): parser = argparse.ArgumentParser(description="Collect gene IDs from count files") parser.add_argument( - "--counts", type=str, dest="count_files", required=True, help="Count files" + "--ids", type=str, dest="gene_id_files", required=True, help="Gene ID files" ) return parser.parse_args() @@ -37,26 +39,29 @@ def parse_args(): ##################################################### -def parse_table(file: Path): - if file.suffix == ".csv": - return pd.read_csv(file, header=0, index_col=0) - else: # .tsv - return pd.read_csv(file, header=0, index_col=0, sep="\t") - - def main(): args = parse_args() - count_files = [Path(file) for file in args.count_files.split(" ")] - logger.info(f"Getting gene IDs from {len(count_files)} count files") - - all_gene_ids = set() - for count_file in tqdm(count_files): - df = parse_table(count_file) - all_gene_ids.update(list(df.index)) - # sorting IDs in order to have a consistent output - with open(ALL_GENE_IDS_OUTFILE, "w") as f: - f.write("\n".join(sorted([str(gene_id) for gene_id in all_gene_ids]))) + gene_id_files = [Path(file) for file in args.gene_id_files.split(" ")] + logger.info(f"Getting gene IDs from {len(gene_id_files)} files") + + unique_gene_ids = set() + counter = Counter() + for gene_id_file in tqdm(gene_id_files): + with open(gene_id_file, "r") as fin: + gene_ids = [line.strip() for line in fin] + unique_gene_ids.update(gene_ids) + counter.update(gene_ids) + + with open(UNIQUE_GENE_IDS_OUTFILE, "w") as fout: + fout.write("\n".join([str(gene_id) for gene_id in unique_gene_ids])) + + with open(GENE_ID_OCCURRENCES_OUTFILE, "w") as fout: + fout.write( + f"{config.ORIGINAL_GENE_ID_COLNAME},{config.GENE_ID_COUNT_COLNAME}\n" + ) + for gene_id, count in counter.items(): + fout.write(f"{gene_id},{count}\n") if __name__ == "__main__": diff --git a/bin/common.py b/bin/common.py new file mode 100644 index 00000000..61e59c62 --- /dev/null +++ b/bin/common.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 + +# Written by Olivier Coen. Released under the MIT license. + +import logging +from pathlib import Path + +import config +import polars as pl + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def parse_header(file: Path, sep: str): + with open(file, "r") as fin: + header = fin.readline().strip().split(sep) + first_row = fin.readline().strip().split(sep) + if len(header) == len(first_row): + return header + elif len(header) == len(first_row) - 1: + return [config.GENE_ID_COLNAME] + header + else: + raise ValueError( + f"Header has length: {len(header)} while first row has length: {len(first_row)}" + ) + + +def parse_table(file: Path): + # parsing header first + if file.suffix in [".csv", ".tsv"]: + # parsing header manually + sep = "," if file.suffix == ".csv" else "\t" + header = parse_header(file, sep) + return pl.read_csv( + file, separator=sep, has_header=False, skip_rows=1, new_columns=header + ) + elif file.suffix == ".parquet": + return pl.read_parquet(file) + else: + raise ValueError(f"Unsupported file format: {file.suffix}") + + +def parse_count_table(file: Path): + df = parse_table(file) + print(df) + first_col = df.columns[0] + # whatever the name of the first col, rename it to "gene_id" + return df.rename({first_col: config.GENE_ID_COLNAME}).select( + pl.col(config.GENE_ID_COLNAME).cast(pl.String()), + pl.exclude(config.GENE_ID_COLNAME).cast(pl.Float64()), + ) diff --git a/bin/config.py b/bin/config.py index ba32d51c..dbf02757 100644 --- a/bin/config.py +++ b/bin/config.py @@ -1,5 +1,6 @@ # general column names GENE_ID_COLNAME = "gene_id" +GENE_ID_COUNT_COLNAME = "count" CDNA_LENGTH_COLNAME = "length" RANK_COLNAME = "rank" diff --git a/bin/get_genes_with_good_occurrence.py b/bin/get_genes_with_good_occurrence.py new file mode 100755 index 00000000..443d7b38 --- /dev/null +++ b/bin/get_genes_with_good_occurrence.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 + +# Written by Olivier Coen. Released under the MIT license. + +import argparse +import logging +from pathlib import Path + +import config +import polars as pl +from common import parse_table + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +OUTFILE = "valid_gene_ids.txt" + +################################################################## +# FUNCTIONS +################################################################## + + +def parse_args(): + parser = argparse.ArgumentParser("Get genes with good occurrence") + parser.add_argument( + "--occurrences", + type=Path, + required=True, + dest="gene_id_occurrence_file", + help="Input file containing gene ID occurrences", + ) + parser.add_argument( + "--mappings", + type=Path, + required=True, + dest="mapping_file", + help="Mapping file containing gene IDs", + ) + parser.add_argument( + "--nb-datasets", + type=int, + required=True, + dest="nb_datasets", + help="Number of datasets", + ) + parser.add_argument( + "--min-freq-occurrence", + type=float, + required=True, + dest="min_freq_occurrence", + help="Minimum frequency of occurrences for a gene among all datasets", + ) + return parser.parse_args() + + +################################################################## +# MAIN +################################################################## + + +def main(): + args = parse_args() + + # taking lower bound of threshold + occurrence_threshold = int(args.nb_datasets * args.min_freq_occurrence) + logger.info( + f"Occurrence threshold: at least {occurrence_threshold} occurrence(s) among {args.nb_datasets} dataset(s)" + ) + + original_gene_id_occurrence_df = parse_table(args.gene_id_occurrence_file) + mapping_df = parse_table(args.mapping_file) + nb_mapped_genes = len(mapping_df) + + df = original_gene_id_occurrence_df.join( + mapping_df, + on=config.ORIGINAL_GENE_ID_COLNAME, + ) + + total_gene_id_occurrence_df = df.group_by(config.GENE_ID_COLNAME).agg( + pl.col(config.GENE_ID_COUNT_COLNAME).sum().alias("total") + ) + + df = df.join( + total_gene_id_occurrence_df, + on=config.GENE_ID_COLNAME, + ).filter(pl.col("total") >= occurrence_threshold) + + valid_gene_ids = df.select(config.GENE_ID_COLNAME).unique().to_series().to_list() + + with open(OUTFILE, "w") as f: + f.write("\n".join(valid_gene_ids)) + + nb_valid_genes = len(valid_gene_ids) + + logger.info( + f"Found {nb_valid_genes} valid gene IDs ({nb_valid_genes / nb_mapped_genes:.2%})" + ) + + +if __name__ == "__main__": + main() diff --git a/bin/rename_gene_ids.py b/bin/rename_gene_ids.py index cce7c86e..f33d8520 100755 --- a/bin/rename_gene_ids.py +++ b/bin/rename_gene_ids.py @@ -8,8 +8,8 @@ from pathlib import Path import config -import pandas as pd import polars as pl +from common import parse_count_table, parse_table logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -27,6 +27,7 @@ FAILURE_REASON_FILE = "failure_reason.txt" UNMAPPED_FILE_SUFFIX = "unmapped.txt" +NOT_VALID_FILE_SUFFIX = "not_valid.txt" MERGED_FILE_SUFFIX = "merged.txt" FINAL_FILE_SUFFIX = "final.txt" @@ -43,29 +44,18 @@ def parse_args(): parser.add_argument( "--mappings", type=Path, - required=True, dest="mapping_file", help="Mapping file containing gene IDs", ) + parser.add_argument( + "--valid-gene-ids", + type=Path, + dest="valid_gene_ids_file", + help="File containing valid gene IDs", + ) return parser.parse_args() -def parse_table(file: Path, **kwargs): - if file.suffix == ".csv": - return pd.read_csv(file, header=0, **kwargs) - else: # .tsv - return pd.read_csv(file, header=0, sep="\t", **kwargs) - - -def parse_count_table(file: Path): - # transitting to pandas dataframe helps to avoid parsing errors - df = parse_table(file, index_col=0) - # whatever the name of the first col, rename it to "gene_id" - df.index.rename(config.GENE_ID_COLNAME, inplace=True) - df.index = df.index.astype(str) - return pl.from_pandas(df.reset_index()) - - ################################################################## # MAIN ################################################################## @@ -94,9 +84,12 @@ def main(): ############################################################# mapping_df = parse_table(args.mapping_file) - mapping_dict = mapping_df.set_index(config.ORIGINAL_GENE_ID_COLNAME)[ - config.GENE_ID_COLNAME - ].to_dict() + mapping_dict = dict( + zip( + mapping_df[config.ORIGINAL_GENE_ID_COLNAME], + mapping_df[config.GENE_ID_COLNAME], + ) + ) ############################################################# # MAPPING GENE IDS IN DATAFRAME @@ -126,6 +119,8 @@ def main(): with open(FAILURE_REASON_FILE, "w") as f: f.write(msg) + with open(NOT_VALID_FILE_SUFFIX, "w") as f: + f.write("0") with open(MERGED_FILE_SUFFIX, "w") as f: f.write("0") with open(FINAL_FILE_SUFFIX, "w") as f: @@ -158,9 +153,38 @@ def main(): .alias(config.GENE_ID_COLNAME) ) - # TODO: check is there is another way to avoid duplicate gene names - # sometimes different gene names have the same Gene ID - # for now, we just get the mean of values, but this is not ideal + ############################################################# + # GETTING VALID GENE IDS + ############################################################# + + logger.info("Keeping only genes with sufficient occurrence over datasets") + nb_genes_before_validation = len(df) + + with open(args.valid_gene_ids_file, "r") as fin: + valid_gene_ids = [line.strip() for line in fin.readlines()] + + df = df.filter(pl.col(config.GENE_ID_COLNAME).is_in(valid_gene_ids)) + + nb_not_valid_genes = nb_genes_before_validation - len(df) + logger.info( + f"{nb_not_valid_genes} ({nb_not_valid_genes / nb_genes_before_validation:.2%}) genes were not valid" + ) + + with open(NOT_VALID_FILE_SUFFIX, "w") as f: + f.write(str(nb_not_valid_genes)) + + if df.is_empty(): + msg = "NO GENES LEFT AFTER REMOVING RARE GENE IDS" + logger.error(msg) + with open(FAILURE_REASON_FILE, "w") as f: + f.write(msg) + + with open(MERGED_FILE_SUFFIX, "w") as f: + f.write("0") + with open(FINAL_FILE_SUFFIX, "w") as f: + f.write("0") + + sys.exit(0) ############################################################# # GENE COUNT HANDLING @@ -169,40 +193,30 @@ def main(): # handling cases where multiple genes have the same Gene ID # since subsequent steps in the pipeline require integer values, # we need to ensure that the resulting DataFrame has integer values + + # TODO: check is there is another way to avoid duplicate gene names + # sometimes different gene names have the same Gene ID + # for now, we just get the mean of values, but this is not ideal + logger.info("Computing mean counts for genes with duplicate IDs") df = df.group_by(config.GENE_ID_COLNAME, maintain_order=True).agg( pl.exclude(config.GENE_ID_COLNAME).mean() ) + ############################################################# + # WRITING OUTFILES + ############################################################# + nb_merged = nb_mapped_genes - len(df) with open(MERGED_FILE_SUFFIX, "w") as f: f.write(str(nb_merged)) with open(FINAL_FILE_SUFFIX, "w") as f: f.write(str(len(df))) - ############################################################# - # WRITING OUTFILES - ############################################################# - # writing to output file - logger.info("Writing output file") outfile = args.count_file.with_name(args.count_file.stem + RENAMED_FILE_SUFFIX) df.write_csv(outfile) - # making dataframe for mapping (only two columns: original and new) - mapping_df = ( - pd.DataFrame(mapping_dict, index=[0]) - .T.reset_index() # transpose: setting keys as indexes instead of columns - .rename( - columns={ - "index": config.ORIGINAL_GENE_ID_COLNAME, - 0: config.GENE_ID_COLNAME, - } - ) - ) - mapping_file = args.count_file.with_name(args.count_file.stem + MAPPING_FILE_SUFFIX) - mapping_df.to_csv(mapping_file, index=False, header=True) - if __name__ == "__main__": main() diff --git a/modules/local/clean_gene_ids/environment.yml b/modules/local/clean_gene_ids/environment.yml index d44b366f..4fa78808 100644 --- a/modules/local/clean_gene_ids/environment.yml +++ b/modules/local/clean_gene_ids/environment.yml @@ -4,5 +4,4 @@ channels: - conda-forge - bioconda dependencies: - - conda-forge::pandas==2.3.3 - conda-forge::polars==1.35.2 diff --git a/modules/local/clean_gene_ids/main.nf b/modules/local/clean_gene_ids/main.nf index 6269c2c8..67eb867d 100644 --- a/modules/local/clean_gene_ids/main.nf +++ b/modules/local/clean_gene_ids/main.nf @@ -6,17 +6,17 @@ process CLEAN_GENE_IDS { 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/c9/c9b43e446f2c3b794644fd4c1c86ab09ba0afafc0c02e3fcdf45509ffc89fc4d/data': - 'community.wave.seqera.io/library/pandas_polars:29ea1468b5490a67' }" + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': + 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" input: tuple val(meta), path(count_file) output: - tuple val(meta), path('*.cleaned.csv'), optional: true, emit: counts + tuple val(meta), path('*.cleaned.parquet'), optional: true, emit: counts + path('*.cleaned_gene_ids.txt'), optional: true, emit: gene_ids tuple val(meta.dataset), path("failure_reason.txt"), optional: true, topic: id_cleaning_failure_reason tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions - tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions script: diff --git a/modules/local/collect_gene_ids/environment.yml b/modules/local/collect_gene_ids/environment.yml index fa92ab03..c4d7cc14 100644 --- a/modules/local/collect_gene_ids/environment.yml +++ b/modules/local/collect_gene_ids/environment.yml @@ -4,5 +4,4 @@ channels: - conda-forge - bioconda dependencies: - - conda-forge::pandas==2.3.3 - conda-forge::tqdm==4.67.1 diff --git a/modules/local/collect_gene_ids/main.nf b/modules/local/collect_gene_ids/main.nf index 1ce3d1a0..ffc94e68 100644 --- a/modules/local/collect_gene_ids/main.nf +++ b/modules/local/collect_gene_ids/main.nf @@ -1,26 +1,25 @@ process COLLECT_GENE_IDS { - tag "chunk ${task.index}" 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/60/604657081a64b39e17bb6ad307e545aa6aebf4133b64d6766515c9789bb2d304/data': - 'community.wave.seqera.io/library/pandas_tqdm:2ca37c1047243549' }" + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/80/80143d9f5e0bfe1364e7bf621ca8bb45f707fd48aa1ba3712158fc441d7873b0/data': + 'community.wave.seqera.io/library/tqdm:4.67.1--c1e9fac535191e31' }" input: path count_files, stageAs: "?/*" output: - path 'all_gene_ids.txt', emit: gene_ids + path 'unique_gene_ids.txt', emit: unique_gene_ids + path 'gene_id_occurrences.csv', emit: gene_id_occurrences tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions - tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions tuple val("${task.process}"), val('tqdm'), eval('python3 -c "import tqdm; print(tqdm.__version__)"'), topic: versions script: """ collect_gene_ids.py \\ - --counts "$count_files" + --ids "$count_files" """ } diff --git a/modules/local/filter_out_rare_genes/environment.yml b/modules/local/filter_out_rare_genes/environment.yml new file mode 100644 index 00000000..4fa78808 --- /dev/null +++ b/modules/local/filter_out_rare_genes/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::polars==1.35.2 diff --git a/modules/local/filter_out_rare_genes/main.nf b/modules/local/filter_out_rare_genes/main.nf new file mode 100644 index 00000000..48c279aa --- /dev/null +++ b/modules/local/filter_out_rare_genes/main.nf @@ -0,0 +1,42 @@ +process FILTER_OUT_RARE_GENES { + + 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/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': + 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" + + input: + path(gene_id_mapping_file) + path(gene_id_occurrences_file) + val nb_datasets + val(min_freq_occurrence) + + output: + path('valid_gene_ids.txt'), optional: true, emit: valid_gene_ids + tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions + tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions + + script: + def is_using_containers = workflow.containerEngine ? true : false + """ + # limiting number of threads when using conda / micromamba + if [ "${is_using_containers}" == "false" ]; then + export POLARS_MAX_THREADS=${task.cpus} + fi + + get_genes_with_good_occurrence.py \\ + --occurrences $gene_id_occurrences_file \\ + --mappings $gene_id_mapping_file \\ + --nb-datasets $nb_datasets \\ + --min-freq-occurrence $min_freq_occurrence + """ + + + stub: + """ + touch fake.validated_genes.txt + """ + +} diff --git a/modules/local/rename_gene_ids/main.nf b/modules/local/rename_gene_ids/main.nf index e334b8a3..22815b9f 100644 --- a/modules/local/rename_gene_ids/main.nf +++ b/modules/local/rename_gene_ids/main.nf @@ -6,24 +6,25 @@ process RENAME_GENE_IDS { 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/c9/c9b43e446f2c3b794644fd4c1c86ab09ba0afafc0c02e3fcdf45509ffc89fc4d/data': - 'community.wave.seqera.io/library/pandas_polars:29ea1468b5490a67' }" + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': + 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" input: tuple val(meta), path(count_file) path gene_id_mapping_file + path valid_gene_ids_file output: tuple val(meta), path('*.renamed.csv'), optional: true, emit: counts tuple val(meta.dataset), path("failure_reason.txt"), optional: true, topic: renaming_failure_reason tuple val(meta.dataset), path("warning_reason.txt"), optional: true, topic: renaming_warning_reason - tuple val(meta.dataset), env("NB_FINAL"), env("NB_MERGED"), env("NB_UNMAPPED"), topic: id_mapping_stats + tuple val(meta.dataset), env("NB_FINAL"), env("NB_MERGED"), env("NB_NOT_VALID"), env("NB_UNMAPPED"), topic: id_mapping_stats tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions - tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions script: def mapping_arg = gene_id_mapping_file ? "--mappings $gene_id_mapping_file" : "" + def valid_ids_arg = valid_gene_ids_file ? "--valid-gene-ids $valid_gene_ids_file" : "" def is_using_containers = workflow.containerEngine ? true : false """ # limiting number of threads when using conda / micromamba @@ -33,10 +34,12 @@ process RENAME_GENE_IDS { rename_gene_ids.py \\ --count-file "$count_file" \\ - $mapping_arg + $mapping_arg \\ + $valid_ids_arg NB_UNMAPPED=\$(cat unmapped.txt) NB_MERGED=\$(cat merged.txt) + NB_NOT_VALID=\$(cat not_valid.txt) NB_FINAL=\$(cat final.txt) """ diff --git a/nextflow.config b/nextflow.config index 9e92b0f3..0b8463c5 100644 --- a/nextflow.config +++ b/nextflow.config @@ -34,6 +34,7 @@ params { gene_metadata = null gene_id_mapping = null skip_id_mapping = false + min_freq_occurrence = 0.1 // statistics normalisation_method = 'tpm' diff --git a/nextflow_schema.json b/nextflow_schema.json index ae45dbbc..4cee70f9 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -167,6 +167,15 @@ "description": "Custom gene metadata file", "help_text": "Path to comma-separated file containing custom gene metadata information. Each row represents a gene and links its gene ID to its name and description. The metadata file should be a comma-separated file with 3 columns (gene_id, name and description) and a header row.", "fa_icon": "fas fa-file" + }, + "min_freq_occurrence": { + "type": "number", + "description": "Minimum frequency of occurrence", + "fa_icon": "fas fa-battery-three-quarters", + "minimum": 0, + "maximum": 1, + "default": 0.1, + "help_text": "To avoid genes that are rarely observed, a threshold is set up. Such genes may cause unnecessary overhead." } } }, diff --git a/subworkflows/local/idmapping/main.nf b/subworkflows/local/idmapping/main.nf index 3d6966c7..f242b440 100644 --- a/subworkflows/local/idmapping/main.nf +++ b/subworkflows/local/idmapping/main.nf @@ -1,6 +1,7 @@ include { CLEAN_GENE_IDS } from '../../../modules/local/clean_gene_ids' include { COLLECT_GENE_IDS } from '../../../modules/local/collect_gene_ids' include { GPROFILER_IDMAPPING } from '../../../modules/local/gprofiler/idmapping' +include { FILTER_OUT_RARE_GENES } from '../../../modules/local/filter_out_rare_genes' include { RENAME_GENE_IDS } from '../../../modules/local/rename_gene_ids' /* @@ -18,57 +19,57 @@ workflow ID_MAPPING { gprofiler_target_db custom_gene_id_mapping custom_gene_metadata + min_freq_occurrence outdir main: ch_gene_id_mapping = channel.empty() ch_gene_metadata = channel.empty() + ch_valid_gene_ids = channel.empty() if ( !skip_id_mapping ) { // ----------------------------------------------------------------- - // COLLECTING ALL GENE IDS FROM ALL DATASETS + // CLEANING GENE IDS // ----------------------------------------------------------------- - // here we cannot use directly COLLECT_GENE_IDS for runs comprising a huge number of files (eg. human) - // so that we proceed by chunks, and perform a final merging step using the Java VM - CLEAN_GENE_IDS ( ch_counts ) - ch_counts = CLEAN_GENE_IDS.out.counts - - // TRICK: - // the buffer operator creates non-deterministic chunks - // which prevents resuming the pipeline - // so we sort the list of files before buffering them - ch_chunk_counts = ch_counts - .map{ meta, file -> file } - .collect( sort: true ) // get all files and sort them - .flatten() // needed to convert the list back to individual channel items - .buffer( size: 100, remainder: true ) - - COLLECT_GENE_IDS( ch_chunk_counts ) - - ch_gene_ids = COLLECT_GENE_IDS.out.gene_ids - .splitText() - .unique() - .collectFile( - name: 'original_gene_ids.txt', - storeDir: "${outdir}/idmapping/", - sort: true - ) + ch_counts = CLEAN_GENE_IDS.out.counts + ch_cleaned_gene_ids = CLEAN_GENE_IDS.out.gene_ids + + // ----------------------------------------------------------------- + // COLLECTING ALL CLEANED GENE IDS FROM ALL DATASETS + // ----------------------------------------------------------------- + + // sorting files in order to have a consistent input and be able to retry + COLLECT_GENE_IDS( + ch_cleaned_gene_ids.toSortedList() + ) // ----------------------------------------------------------------- // MAPPING THESE GENE IDS TO THE CHOSEN TARGET DB // ----------------------------------------------------------------- GPROFILER_IDMAPPING( - ch_gene_ids, + COLLECT_GENE_IDS.out.unique_gene_ids, species, gprofiler_target_db ) ch_gene_id_mapping = GPROFILER_IDMAPPING.out.mapping ch_gene_metadata = GPROFILER_IDMAPPING.out.metadata + + // ----------------------------------------------------------------- + // FILTERING OUT GENE IDS THAT DO NOT HAVE ENOUGH OCCURRENCES + // ----------------------------------------------------------------- + + FILTER_OUT_RARE_GENES( + ch_gene_id_mapping, + COLLECT_GENE_IDS.out.gene_id_occurrences, + ch_counts.count(), + min_freq_occurrence + ) + ch_valid_gene_ids = FILTER_OUT_RARE_GENES.out.valid_gene_ids } // ----------------------------------------------------------------- @@ -119,7 +120,8 @@ workflow ID_MAPPING { RENAME_GENE_IDS( ch_counts, - ch_global_gene_id_mapping.first() + ch_global_gene_id_mapping.first(), + ch_valid_gene_ids.collect() ) ch_counts = RENAME_GENE_IDS.out.counts diff --git a/subworkflows/local/multiqc/main.nf b/subworkflows/local/multiqc/main.nf index 3c539930..d1d3eb84 100644 --- a/subworkflows/local/multiqc/main.nf +++ b/subworkflows/local/multiqc/main.nf @@ -31,11 +31,11 @@ workflow MULTIQC_WORKFLOW { ch_id_mapping_stats = channel.topic('id_mapping_stats') .collectFile( name: 'id_mapping_stats.csv', - seed: "dataset,final,merged,unmapped", + seed: "dataset,final,merged,not_valid,unmapped", newLine: true, storeDir: "${outdir}/statistics/" ) { - item -> "${item[0]},${item[1]},${item[2]},${item[3]}" + item -> "${item[0]},${item[1]},${item[2]},${item[3]},${item[4]}" } ch_skewness = channel.topic('skewness') diff --git a/tests/default.nf.test.snap b/tests/default.nf.test.snap index 3242449b..f9d58955 100644 --- a/tests/default.nf.test.snap +++ b/tests/default.nf.test.snap @@ -91,6 +91,10 @@ "python": "3.12.8", "scikit-learn": "1.6.1" }, + "REMOVE_SAMPLES_NOT_VALID": { + "pandas": "2.3.3", + "python": "3.13.7" + }, "RENAME_GENE_IDS": { "pandas": "2.3.3", "polars": "1.35.2", @@ -201,9 +205,9 @@ "normalised", "normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay", "normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay/quantile_normalised", - "normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay/quantile_normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay/quantile_normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay.cleaned.renamed.filtered.tpm.quant_norm.parquet", "normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay/tpm", - "normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay/tpm/SRP254919.salmon.merged.gene_counts.top1000cov.assay.cleaned.renamed.tpm.csv", + "normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay/tpm/SRP254919.salmon.merged.gene_counts.top1000cov.assay.cleaned.renamed.filtered.tpm.csv", "pipeline_info", "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", "statistics", @@ -220,7 +224,7 @@ "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", "all_genes_summary.csv:md5,54b96cbfb5e464ec086c7e1308701e02", "whole_design.csv:md5,f29515bc2c783e593fb9028127342593", - "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", + "environment.yml:md5,4d32e46adf0ff32a3c65a24b68483fa7", "all_gene_ids.txt:md5,6b2ece983fd9da133e719914216852b0", "global_gene_id_mapping.csv:md5,78934d2ac5fe7d863f114c5703f57a06", "global_gene_metadata.csv:md5,bd76860b422e45eca7cd583212a977d2", @@ -232,7 +236,7 @@ "whole_gene_id_mapping.csv:md5,78934d2ac5fe7d863f114c5703f57a06", "whole_gene_metadata.csv:md5,bd76860b422e45eca7cd583212a977d2", "whole_design.csv:md5,f29515bc2c783e593fb9028127342593", - "SRP254919.salmon.merged.gene_counts.top1000cov.assay.cleaned.renamed.tpm.csv:md5,5b517b410a643c4e6fbffb55dc1cd1a7", + "SRP254919.salmon.merged.gene_counts.top1000cov.assay.cleaned.renamed.filtered.tpm.csv:md5,5b517b410a643c4e6fbffb55dc1cd1a7", "id_mapping_stats.csv:md5,14cdb53685228e5e5393cf9404856b41", "ratio_zeros.csv:md5,5a667d505cbd2cc7057ee47b70536c2e", "skewness.csv:md5,582683980eadf84d32853a21f9dce230", @@ -243,7 +247,7 @@ "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-23T12:18:28.957913038" + "timestamp": "2026-01-03T11:26:35.453920775" }, "-profile test_eatlas_only_with_keywords": { "content": [ @@ -1042,14 +1046,14 @@ "normalised", "normalised/E_MTAB_8187_rnaseq", "normalised/E_MTAB_8187_rnaseq/quantile_normalised", - "normalised/E_MTAB_8187_rnaseq/quantile_normalised/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/E_MTAB_8187_rnaseq/quantile_normalised/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.quant_norm.parquet", "normalised/E_MTAB_8187_rnaseq/tpm", - "normalised/E_MTAB_8187_rnaseq/tpm/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", + "normalised/E_MTAB_8187_rnaseq/tpm/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.csv", "normalised/beta_vulgaris.rnaseq.raw.counts", "normalised/beta_vulgaris.rnaseq.raw.counts/quantile_normalised", - "normalised/beta_vulgaris.rnaseq.raw.counts/quantile_normalised/beta_vulgaris.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/beta_vulgaris.rnaseq.raw.counts/quantile_normalised/beta_vulgaris.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.quant_norm.parquet", "normalised/beta_vulgaris.rnaseq.raw.counts/tpm", - "normalised/beta_vulgaris.rnaseq.raw.counts/tpm/beta_vulgaris.rnaseq.raw.counts.cleaned.renamed.tpm.csv", + "normalised/beta_vulgaris.rnaseq.raw.counts/tpm/beta_vulgaris.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.csv", "pipeline_info", "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", "public_data", @@ -1083,7 +1087,7 @@ "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", "all_genes_summary.csv:md5,ec52af6957845539143919c0e6eec89c", "whole_design.csv:md5,3c1e14c9bd7ad250326b070a0dd4d81f", - "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", + "environment.yml:md5,4d32e46adf0ff32a3c65a24b68483fa7", "all_gene_ids.txt:md5,ba79f5609df755c0f75de41357319c84", "global_gene_id_mapping.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", "global_gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", @@ -1095,8 +1099,8 @@ "whole_gene_id_mapping.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", "whole_gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", "whole_design.csv:md5,3c1e14c9bd7ad250326b070a0dd4d81f", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,b407990bb10106de943f9e93cda6323e", - "beta_vulgaris.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,2c326f3e419341f955bb757fc8bf4357", + "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.csv:md5,b407990bb10106de943f9e93cda6323e", + "beta_vulgaris.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.csv:md5,2c326f3e419341f955bb757fc8bf4357", "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", @@ -1112,7 +1116,7 @@ "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-23T12:10:41.648442347" + "timestamp": "2026-01-03T11:24:55.793642391" }, "-profile test_dataset_custom_mapping_and_gene_length": { "content": [ diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 168955d4..b0403d80 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -101,6 +101,7 @@ workflow STABLEEXPRESSION { params.gprofiler_target_db, params.gene_id_mapping, params.gene_metadata, + params.min_freq_occurrence, params.outdir ) ch_counts = ID_MAPPING.out.counts From 4dd05e22f0ca48fb08118f442ebda8ab8332b8fe Mon Sep 17 00:00:00 2001 From: Olivier Date: Sun, 4 Jan 2026 14:44:16 +0100 Subject: [PATCH 250/258] implement filtering on rare genes; add to multiqc --- assets/multiqc_config.yml | 28 +++++++++++++- bin/get_genes_with_good_occurrence.py | 42 +++++++++++++++------ bin/gprofiler_map_ids.py | 2 +- modules/local/filter_out_rare_genes/main.nf | 12 ++++-- nextflow.config | 3 +- nextflow_schema.json | 15 ++++++-- subworkflows/local/idmapping/main.nf | 6 ++- subworkflows/local/multiqc/main.nf | 1 + workflows/stableexpression.nf | 3 +- 9 files changed, 86 insertions(+), 26 deletions(-) diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index 08225cc7..a1decec8 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -11,7 +11,7 @@ report_section_order: "nf-core-stableexpression-summary": order: -1002 -export_plots: true +export_plots: false run_modules: - custom_content @@ -483,12 +483,34 @@ custom_data: color: "#38B4F2" not_valid: name: "Nb rare gene IDs removed" - color: "#38B4F2" + color: "#F2C038" unmapped: name: "Nb unmapped gene IDs" color: "#E3224A" plot_type: "barplot" + total_gene_id_occurrence_quantiles: + section_name: "Distribution of gene ID occurrence quantiles" + parent_id: idmapping + parent_name: "ID mapping" + parent_description: "Information about the ID mapping" + file_format: "csv" + pconfig: + categories: true + #ymax: 1.1 + #ymin: -0.1 + #y_lines: + # - value: 1 + # color: "#ff0000" + # width: 2 + # dash: "dash" + # label: "Threshold" + description: | + Quantiles of the total number of occurrences of gene IDs across all datasets. Quantile values were sorted from greatest to least. + plot_type: "linegraph" + helptext: | + Gene IDs can be present or absent in the datasets. For each gene ID, the total number of occurrences across all datasets was calculated and quantile values were computed from these totals. + eatlas_selected_experiments_metadata: section_name: "Selected" parent_id: eatlas @@ -659,6 +681,8 @@ sp: max_filesize: 50000000 # 50MB id_mapping_stats: fn: "*id_mapping_stats.csv" + total_gene_id_occurrence_quantiles: + fn: "*total_gene_id_occurrence_quantiles.csv" skewness: fn: "*skewness.transposed.csv" ratio_zeros: diff --git a/bin/get_genes_with_good_occurrence.py b/bin/get_genes_with_good_occurrence.py index 443d7b38..d7f17d9a 100755 --- a/bin/get_genes_with_good_occurrence.py +++ b/bin/get_genes_with_good_occurrence.py @@ -13,7 +13,8 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -OUTFILE = "valid_gene_ids.txt" +VALID_GENE_IDS_OUTFILE = "valid_gene_ids.txt" +TOTAL_OCCURRENCES_OUTFILE = "total_gene_id_occurrence_quantiles.csv" ################################################################## # FUNCTIONS @@ -44,10 +45,17 @@ def parse_args(): help="Number of datasets", ) parser.add_argument( - "--min-freq-occurrence", + "--min-occurrence-frequency", type=float, required=True, - dest="min_freq_occurrence", + dest="min_occurrence_frequency", + help="Minimum frequency of occurrences for a gene among all datasets", + ) + parser.add_argument( + "--min-occurrence-quantile", + type=float, + required=True, + dest="min_occurrence_quantile", help="Minimum frequency of occurrences for a gene among all datasets", ) return parser.parse_args() @@ -61,12 +69,6 @@ def parse_args(): def main(): args = parse_args() - # taking lower bound of threshold - occurrence_threshold = int(args.nb_datasets * args.min_freq_occurrence) - logger.info( - f"Occurrence threshold: at least {occurrence_threshold} occurrence(s) among {args.nb_datasets} dataset(s)" - ) - original_gene_id_occurrence_df = parse_table(args.gene_id_occurrence_file) mapping_df = parse_table(args.mapping_file) nb_mapped_genes = len(mapping_df) @@ -77,17 +79,33 @@ def main(): ) total_gene_id_occurrence_df = df.group_by(config.GENE_ID_COLNAME).agg( - pl.col(config.GENE_ID_COUNT_COLNAME).sum().alias("total") + pl.col(config.GENE_ID_COUNT_COLNAME).sum().alias("total_occurrences") ) df = df.join( total_gene_id_occurrence_df, on=config.GENE_ID_COLNAME, - ).filter(pl.col("total") >= occurrence_threshold) + ).with_columns( + ( + pl.col("total_occurrences").rank(method="max") + / pl.col("total_occurrences").count() + ).alias("total_occurrences_quantile") + ) + + # writing total occurrences in a csv before filtering + df.select([config.GENE_ID_COLNAME, "total_occurrences_quantile"]).sort( + "total_occurrences_quantile", descending=True + ).write_csv(TOTAL_OCCURRENCES_OUTFILE) + + # filtering genes + min_total_occurrence = args.nb_datasets * args.min_occurrence_frequency + df = df.filter( + pl.col("total_occurrences_quantile") >= args.min_occurrence_quantile + ).filter(pl.col("total_occurrences") >= min_total_occurrence) valid_gene_ids = df.select(config.GENE_ID_COLNAME).unique().to_series().to_list() - with open(OUTFILE, "w") as f: + with open(VALID_GENE_IDS_OUTFILE, "w") as f: f.write("\n".join(valid_gene_ids)) nb_valid_genes = len(valid_gene_ids) diff --git a/bin/gprofiler_map_ids.py b/bin/gprofiler_map_ids.py index e24d63af..a41fae92 100755 --- a/bin/gprofiler_map_ids.py +++ b/bin/gprofiler_map_ids.py @@ -63,7 +63,7 @@ def main(): args = parse_args() with open(args.gene_id_file, "r") as fin: - gene_ids = [line.strip() for line in fin] + gene_ids = list(set([line.strip() for line in fin])) logger.info(f"Converting {len(gene_ids)} IDs for species {args.species} ") diff --git a/modules/local/filter_out_rare_genes/main.nf b/modules/local/filter_out_rare_genes/main.nf index 48c279aa..ea2842f6 100644 --- a/modules/local/filter_out_rare_genes/main.nf +++ b/modules/local/filter_out_rare_genes/main.nf @@ -10,11 +10,13 @@ process FILTER_OUT_RARE_GENES { input: path(gene_id_mapping_file) path(gene_id_occurrences_file) - val nb_datasets - val(min_freq_occurrence) + val(nb_datasets) + val(min_occurrence_frequency) + val(min_occurrence_quantile) output: - path('valid_gene_ids.txt'), optional: true, emit: valid_gene_ids + path('valid_gene_ids.txt'), emit: valid_gene_ids + path('total_gene_id_occurrence_quantiles.csv'), topic: total_gene_id_occurrence_quantiles tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions @@ -30,7 +32,9 @@ process FILTER_OUT_RARE_GENES { --occurrences $gene_id_occurrences_file \\ --mappings $gene_id_mapping_file \\ --nb-datasets $nb_datasets \\ - --min-freq-occurrence $min_freq_occurrence + --min-occurrence-frequency $min_occurrence_frequency \\ + --min-occurrence-quantile $min_occurrence_quantile + """ diff --git a/nextflow.config b/nextflow.config index 0b8463c5..ad3dbca8 100644 --- a/nextflow.config +++ b/nextflow.config @@ -34,7 +34,8 @@ params { gene_metadata = null gene_id_mapping = null skip_id_mapping = false - min_freq_occurrence = 0.1 + min_occurrence_freq = 0.1 + min_occurrence_quantile = 0.1 // statistics normalisation_method = 'tpm' diff --git a/nextflow_schema.json b/nextflow_schema.json index 4cee70f9..3a926237 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -168,14 +168,23 @@ "help_text": "Path to comma-separated file containing custom gene metadata information. Each row represents a gene and links its gene ID to its name and description. The metadata file should be a comma-separated file with 3 columns (gene_id, name and description) and a header row.", "fa_icon": "fas fa-file" }, - "min_freq_occurrence": { + "min_occurrence_quantile": { "type": "number", - "description": "Minimum frequency of occurrence", + "description": "Minimum quantile for the frequency of occurrence", "fa_icon": "fas fa-battery-three-quarters", "minimum": 0, "maximum": 1, "default": 0.1, - "help_text": "To avoid genes that are rarely observed, a threshold is set up. Such genes may cause unnecessary overhead." + "help_text": "To avoid genes that are rarely observed, genes less represented than the specified quantile will be filtered out. For example, value of 0.2 means that the 20% less represented will be filtered out. This filter is applied before using the absolute filter `--min_occurrence_freq`." + }, + "min_occurrence_freq": { + "type": "number", + "description": "Minimum frequency of occurrence among all datasets", + "fa_icon": "fas fa-battery-three-quarters", + "minimum": 0, + "maximum": 1, + "default": 0.1, + "help_text": "To avoid genes that are rarely observed, genes showing a frequency of occurrence below this threshold will be filtered out." } } }, diff --git a/subworkflows/local/idmapping/main.nf b/subworkflows/local/idmapping/main.nf index f242b440..c6802db8 100644 --- a/subworkflows/local/idmapping/main.nf +++ b/subworkflows/local/idmapping/main.nf @@ -19,7 +19,8 @@ workflow ID_MAPPING { gprofiler_target_db custom_gene_id_mapping custom_gene_metadata - min_freq_occurrence + min_occurrence_freq + min_occurrence_quantile outdir main: @@ -67,7 +68,8 @@ workflow ID_MAPPING { ch_gene_id_mapping, COLLECT_GENE_IDS.out.gene_id_occurrences, ch_counts.count(), - min_freq_occurrence + min_occurrence_freq, + min_occurrence_quantile ) ch_valid_gene_ids = FILTER_OUT_RARE_GENES.out.valid_gene_ids } diff --git a/subworkflows/local/multiqc/main.nf b/subworkflows/local/multiqc/main.nf index d1d3eb84..c23d46ce 100644 --- a/subworkflows/local/multiqc/main.nf +++ b/subworkflows/local/multiqc/main.nf @@ -187,6 +187,7 @@ workflow MULTIQC_WORKFLOW { .mix( channel.topic('geo_rejected_datasets').collect() ) // single item .mix( COLLECT_STATISTICS.out.csv ) .mix( ch_id_mapping_stats ) + .mix( channel.topic('total_gene_id_occurrence_quantiles').collect() ) // single item .mix( ch_eatlas_failure_reasons ) .mix( ch_eatlas_warning_reasons ) .mix( ch_geo_failure_reasons ) diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index b0403d80..4bf9f262 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -101,7 +101,8 @@ workflow STABLEEXPRESSION { params.gprofiler_target_db, params.gene_id_mapping, params.gene_metadata, - params.min_freq_occurrence, + params.min_occurrence_freq, + params.min_occurrence_quantile, params.outdir ) ch_counts = ID_MAPPING.out.counts From f8b10f30037a9a4c8d048ad86030e780657cc919 Mon Sep 17 00:00:00 2001 From: Olivier Date: Sun, 4 Jan 2026 18:01:45 +0100 Subject: [PATCH 251/258] fix issue when cleaning ENSG ids --- bin/clean_gene_ids.py | 3 ++- bin/common.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/clean_gene_ids.py b/bin/clean_gene_ids.py index ba99cba3..7c5c802e 100755 --- a/bin/clean_gene_ids.py +++ b/bin/clean_gene_ids.py @@ -44,7 +44,7 @@ def clean_ensembl_gene_id_versioning(df: pl.DataFrame): """ return df.with_columns( pl.when(pl.col(config.GENE_ID_COLNAME).str.starts_with("ENSG")) - .then(pl.col(config.GENE_ID_COLNAME).str.extract(r"^(ENSG\d+)", 1)) + .then(pl.col(config.GENE_ID_COLNAME).str.extract(r"^(ENSG[a-zA-Z0-9]+)", 1)) .otherwise(pl.col(config.GENE_ID_COLNAME)) .alias(config.GENE_ID_COLNAME) ) @@ -109,6 +109,7 @@ def main(): .to_series() .to_list() ) + with open(gene_ids_outfile, "w") as fout: fout.write("\n".join(gene_ids)) diff --git a/bin/common.py b/bin/common.py index 61e59c62..118f6117 100644 --- a/bin/common.py +++ b/bin/common.py @@ -43,7 +43,6 @@ def parse_table(file: Path): def parse_count_table(file: Path): df = parse_table(file) - print(df) first_col = df.columns[0] # whatever the name of the first col, rename it to "gene_id" return df.rename({first_col: config.GENE_ID_COLNAME}).select( From fae0fdb6d560e0dba8a964908999569949897cc6 Mon Sep 17 00:00:00 2001 From: Olivier Date: Sun, 4 Jan 2026 21:02:51 +0100 Subject: [PATCH 252/258] increase default value of min_occurrence_quantile to 0.2 --- bin/get_genes_with_good_occurrence.py | 44 ++++++++++++++++++--------- nextflow.config | 2 +- nextflow_schema.json | 2 +- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/bin/get_genes_with_good_occurrence.py b/bin/get_genes_with_good_occurrence.py index d7f17d9a..5235b84c 100755 --- a/bin/get_genes_with_good_occurrence.py +++ b/bin/get_genes_with_good_occurrence.py @@ -82,14 +82,28 @@ def main(): pl.col(config.GENE_ID_COUNT_COLNAME).sum().alias("total_occurrences") ) - df = df.join( - total_gene_id_occurrence_df, - on=config.GENE_ID_COLNAME, - ).with_columns( - ( - pl.col("total_occurrences").rank(method="max") - / pl.col("total_occurrences").count() - ).alias("total_occurrences_quantile") + df = ( + df.join( + total_gene_id_occurrence_df, + on=config.GENE_ID_COLNAME, + ) + .with_columns( + total_occurrences_quantile=( + pl.col("total_occurrences").rank(method="max") + / pl.col("total_occurrences").count() + ), + total_occurrences_frequency=( + pl.col("total_occurrences") / args.nb_datasets + ), + ) + .select( + [ + config.GENE_ID_COLNAME, + "total_occurrences_frequency", + "total_occurrences_quantile", + ] + ) + .unique() ) # writing total occurrences in a csv before filtering @@ -98,12 +112,14 @@ def main(): ).write_csv(TOTAL_OCCURRENCES_OUTFILE) # filtering genes - min_total_occurrence = args.nb_datasets * args.min_occurrence_frequency - df = df.filter( - pl.col("total_occurrences_quantile") >= args.min_occurrence_quantile - ).filter(pl.col("total_occurrences") >= min_total_occurrence) - - valid_gene_ids = df.select(config.GENE_ID_COLNAME).unique().to_series().to_list() + valid_gene_ids = ( + df.filter(pl.col("total_occurrences_quantile") >= args.min_occurrence_quantile) + .filter(pl.col("total_occurrences_frequency") >= args.min_occurrence_frequency) + .select(config.GENE_ID_COLNAME) + .unique() + .to_series() + .to_list() + ) with open(VALID_GENE_IDS_OUTFILE, "w") as f: f.write("\n".join(valid_gene_ids)) diff --git a/nextflow.config b/nextflow.config index ad3dbca8..ec2fd0da 100644 --- a/nextflow.config +++ b/nextflow.config @@ -35,7 +35,7 @@ params { gene_id_mapping = null skip_id_mapping = false min_occurrence_freq = 0.1 - min_occurrence_quantile = 0.1 + min_occurrence_quantile = 0.2 // statistics normalisation_method = 'tpm' diff --git a/nextflow_schema.json b/nextflow_schema.json index 3a926237..f63eced3 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -174,7 +174,7 @@ "fa_icon": "fas fa-battery-three-quarters", "minimum": 0, "maximum": 1, - "default": 0.1, + "default": 0.2, "help_text": "To avoid genes that are rarely observed, genes less represented than the specified quantile will be filtered out. For example, value of 0.2 means that the 20% less represented will be filtered out. This filter is applied before using the absolute filter `--min_occurrence_freq`." }, "min_occurrence_freq": { From 50a0c1c33d60c7f665a7b54650b6ca9fbf65dcd5 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 5 Jan 2026 13:33:18 +0100 Subject: [PATCH 253/258] replace pandas by polars in remove_samples_not_valid.py --- bin/remove_samples_not_valid.py | 29 +++++++++---------- bin/rename_gene_ids.py | 8 ++--- .../remove_samples_not_valid/environment.yml | 2 +- .../local/remove_samples_not_valid/main.nf | 8 ++--- modules/local/rename_gene_ids/environment.yml | 1 - modules/local/rename_gene_ids/main.nf | 2 +- 6 files changed, 23 insertions(+), 27 deletions(-) diff --git a/bin/remove_samples_not_valid.py b/bin/remove_samples_not_valid.py index 5acc0562..25adc84d 100755 --- a/bin/remove_samples_not_valid.py +++ b/bin/remove_samples_not_valid.py @@ -7,12 +7,14 @@ import sys from pathlib import Path -import pandas as pd +import config +import polars as pl +from common import parse_count_table logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -OUTFILE_SUFFIX = ".filtered.csv" +OUTFILE_SUFFIX = ".filtered.parquet" MAX_RATIO_ZEROS = 0.75 @@ -32,21 +34,17 @@ def parse_args(): return parser.parse_args() -def parse_counts(file: Path): - if file.suffix == ".csv": - return pd.read_csv(file, header=0, index_col=0) - else: # .tsv - return pd.read_csv(file, header=0, sep="\t", index_col=0) +def filter_out_columns_with_high_zero_ratio(df: pl.DataFrame, max_ratio_zeros: float): + zero_ratio_df = df.select(pl.exclude(config.GENE_ID_COLNAME).eq(pl.lit(0)).mean()) + valid_zero_ratio_samples = [ + col for col in zero_ratio_df.columns if zero_ratio_df[col][0] <= max_ratio_zeros + ] + return df.select(pl.col(config.GENE_ID_COLNAME), pl.col(valid_zero_ratio_samples)) -def filter_out_columns_with_high_zero_ratio(df: pd.DataFrame, max_ratio_zeros: float): - zero_ratio = df.eq(0).mean(axis=0) - return df.loc[:, zero_ratio <= max_ratio_zeros] - - -def export_data(df: pd.DataFrame, outfile: Path): +def export_data(df: pl.DataFrame, outfile: Path): logger.info(f"Exporting filtered counts to: {outfile}") - df.to_csv(outfile, index=True, header=True) + df.write_parquet(outfile) logger.info("Done") @@ -62,12 +60,13 @@ def main(): # putting all counts into a single dataframe logger.info("Loading count data...") - count_df = parse_counts(args.count_file) + count_df = parse_count_table(args.count_file) logger.info( f"Loaded count data with {len(count_df)} rows and {count_df.shape[1]} columns" ) valid_count_df = filter_out_columns_with_high_zero_ratio(count_df, MAX_RATIO_ZEROS) + if valid_count_df.shape[1] == 0: logger.error("No valid columns remaining") sys.exit(0) diff --git a/bin/rename_gene_ids.py b/bin/rename_gene_ids.py index f33d8520..1f0ec4e8 100755 --- a/bin/rename_gene_ids.py +++ b/bin/rename_gene_ids.py @@ -19,9 +19,7 @@ # CONSTANTS ################################################################## -RENAMED_FILE_SUFFIX = ".renamed.csv" -METADATA_FILE_SUFFIX = ".metadata.csv" -MAPPING_FILE_SUFFIX = ".mapping.csv" +RENAMED_FILE_SUFFIX = ".renamed.parquet" WARNING_REASON_FILE = "warning_reason.txt" FAILURE_REASON_FILE = "failure_reason.txt" @@ -214,8 +212,8 @@ def main(): f.write(str(len(df))) logger.info("Writing output file") - outfile = args.count_file.with_name(args.count_file.stem + RENAMED_FILE_SUFFIX) - df.write_csv(outfile) + outfilename = args.count_file.with_suffix(RENAMED_FILE_SUFFIX).name + df.write_parquet(outfilename) if __name__ == "__main__": diff --git a/modules/local/remove_samples_not_valid/environment.yml b/modules/local/remove_samples_not_valid/environment.yml index 104de0e2..4fa78808 100644 --- a/modules/local/remove_samples_not_valid/environment.yml +++ b/modules/local/remove_samples_not_valid/environment.yml @@ -4,4 +4,4 @@ channels: - conda-forge - bioconda dependencies: - - conda-forge::pandas==2.3.3 + - conda-forge::polars==1.35.2 diff --git a/modules/local/remove_samples_not_valid/main.nf b/modules/local/remove_samples_not_valid/main.nf index 020b1fd6..e61528df 100644 --- a/modules/local/remove_samples_not_valid/main.nf +++ b/modules/local/remove_samples_not_valid/main.nf @@ -6,16 +6,16 @@ process REMOVE_SAMPLES_NOT_VALID { 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/3d/3d7126100b0eb7cb53dfb50291707ea8dda3b9738b76551ab73605d0acbe114b/data': - 'community.wave.seqera.io/library/pandas:2.3.3--5a902bf824a79745' }" + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': + 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" input: tuple val(meta), path(count_file) output: - tuple val(meta), path("*.filtered.csv"), optional: true, emit: counts + tuple val(meta), path("*.filtered.parquet"), optional: true, emit: counts tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions - tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions + tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions script: """ diff --git a/modules/local/rename_gene_ids/environment.yml b/modules/local/rename_gene_ids/environment.yml index d44b366f..4fa78808 100644 --- a/modules/local/rename_gene_ids/environment.yml +++ b/modules/local/rename_gene_ids/environment.yml @@ -4,5 +4,4 @@ channels: - conda-forge - bioconda dependencies: - - conda-forge::pandas==2.3.3 - conda-forge::polars==1.35.2 diff --git a/modules/local/rename_gene_ids/main.nf b/modules/local/rename_gene_ids/main.nf index 22815b9f..c6985f15 100644 --- a/modules/local/rename_gene_ids/main.nf +++ b/modules/local/rename_gene_ids/main.nf @@ -15,7 +15,7 @@ process RENAME_GENE_IDS { path valid_gene_ids_file output: - tuple val(meta), path('*.renamed.csv'), optional: true, emit: counts + tuple val(meta), path('*.renamed.parquet'), optional: true, emit: counts tuple val(meta.dataset), path("failure_reason.txt"), optional: true, topic: renaming_failure_reason tuple val(meta.dataset), path("warning_reason.txt"), optional: true, topic: renaming_warning_reason tuple val(meta.dataset), env("NB_FINAL"), env("NB_MERGED"), env("NB_NOT_VALID"), env("NB_UNMAPPED"), topic: id_mapping_stats From 2106da31424a5cb7afe0c5908a8f65f2c8d32b28 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 5 Jan 2026 16:36:12 +0100 Subject: [PATCH 254/258] add log2 normalisation when computing tpm and cpm --- bin/common.py | 10 ++ bin/compute_cpm.py | 43 +++--- bin/compute_tpm.py | 131 ++++++++++-------- .../normalisation/compute_tpm/environment.yml | 2 +- .../local/normalisation/compute_tpm/main.nf | 6 +- 5 files changed, 108 insertions(+), 84 deletions(-) diff --git a/bin/common.py b/bin/common.py index 118f6117..d8e50e0f 100644 --- a/bin/common.py +++ b/bin/common.py @@ -49,3 +49,13 @@ def parse_count_table(file: Path): pl.col(config.GENE_ID_COLNAME).cast(pl.String()), pl.exclude(config.GENE_ID_COLNAME).cast(pl.Float64()), ) + + +def compute_log2(df: pl.DataFrame) -> pl.DataFrame: + """ + Compute log2 values. + """ + return df.select( + pl.col(config.GENE_ID_COLNAME), + (pl.exclude(config.GENE_ID_COLNAME) + 1).log(base=2), + ) diff --git a/bin/compute_cpm.py b/bin/compute_cpm.py index 2b97237b..27df84d0 100755 --- a/bin/compute_cpm.py +++ b/bin/compute_cpm.py @@ -8,13 +8,14 @@ from pathlib import Path import config -import pandas as pd +import polars as pl +from common import compute_log2, parse_count_table logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -CPM_NORM_SUFFIX = ".cpm.csv" +OUTFILE_SUFFIX = ".cpm.parquet" WARNING_REASON_FILE = "warning_reason.txt" FAILURE_REASON_FILE = "failure_reason.txt" @@ -35,14 +36,7 @@ def parse_args(): return parser.parse_args() -def parse_counts(file: Path): - if file.suffix == ".csv": - return pd.read_csv(file, header=0, index_col=0) - else: # .tsv - return pd.read_csv(file, header=0, sep="\t", index_col=0) - - -def calculate_cpm(counts_df: pd.DataFrame): +def calculate_cpm(df: pl.DataFrame) -> pl.DataFrame: """ Calculate CPM (Counts Per Million) from raw count data. @@ -57,20 +51,14 @@ def calculate_cpm(counts_df: pd.DataFrame): DataFrame with CPM values """ # Calculate total counts per sample (column sums) - total_counts = counts_df.sum(axis=0) + sums = df.select(pl.exclude(config.GENE_ID_COLNAME).sum()) # Calculate CPM: (count / total_counts) * 1,000,000 - cpm_df = (counts_df / total_counts) * 1e6 - - return cpm_df - - -def export_normalised_data(count_df: pd.DataFrame, count_file: Path): - """Export gene expression data to CSV.""" - # replace .csv / .tsv by .tpm.csv - outfilename = ".".join(count_file.name.split(".")[:-1]) + CPM_NORM_SUFFIX - logger.info(f"Exporting CPM normalised counts to: {outfilename}") - count_df.to_csv(outfilename, index=True, header=True) + count_columns = df.select(pl.exclude(config.GENE_ID_COLNAME)).columns + return df.select( + [pl.col(config.GENE_ID_COLNAME)] + + [(pl.col(col) / sums[col][0] * 1e6).alias(col) for col in count_columns] + ) ##################################################### @@ -86,14 +74,17 @@ def main(): logger.info("Parsing data") try: - count_df = parse_counts(args.count_file) - count_df.index.name = config.GENE_ID_COLNAME + count_df = parse_count_table(args.count_file) logger.info(f"Normalising {args.count_file.name}") - count_df = calculate_cpm(count_df) - export_normalised_data(count_df, args.count_file) + logger.info("Computing log2 values") + count_df = compute_log2(count_df) + + outfilename = args.count_file.with_suffix(OUTFILE_SUFFIX).name + logger.info(f"Exporting TPM normalised counts to: {outfilename}") + count_df.write_parquet(outfilename) except Exception as e: logger.error(f"Error occurred while normalising data: {e}") diff --git a/bin/compute_tpm.py b/bin/compute_tpm.py index 7139e714..c3283f9b 100755 --- a/bin/compute_tpm.py +++ b/bin/compute_tpm.py @@ -8,13 +8,14 @@ from pathlib import Path import config -import pandas as pd +import polars as pl +from common import compute_log2, parse_count_table, parse_table logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -TPM_NORM_SUFFIX = ".tpm.csv" +OUTFILE_SUFFIX = ".tpm.parquet" WARNING_REASON_FILE = "warning_reason.txt" FAILURE_REASON_FILE = "failure_reason.txt" @@ -42,70 +43,90 @@ def parse_args(): return parser.parse_args() -def parse_counts(file: Path): - if file.suffix == ".csv": - return pd.read_csv(file, header=0, index_col=0) - else: # .tsv - return pd.read_csv(file, header=0, sep="\t", index_col=0) +def try_cast_to_int(df: pl.DataFrame) -> pl.DataFrame: + """Try casting columns to integers.""" + count_columns = df.select(pl.exclude(config.GENE_ID_COLNAME)).columns + # try casting to handle integer values that are float-formated like 1.0 + for col in count_columns: + is_all_integers = df.select(pl.col(col).round().eq(pl.col(col)).all()).item() + if is_all_integers: + df = df.with_columns(pl.col(col).cast(pl.Int64())) + return df -def reorder_gene_lengths( - count_df: pd.DataFrame, cdna_length_df: pd.DataFrame -) -> pd.Series: - # merge with gene length and extracts it afterwards - # so that the genes are in the same order in df and in cdna_length_series - gene_id_df = pd.DataFrame({config.GENE_ID_COLNAME: count_df.index}) - gene_id_df = pd.merge( - gene_id_df, cdna_length_df, how="left", on=config.GENE_ID_COLNAME +def is_raw_counts(df: pl.DataFrame) -> bool: + """Check if the data are raw counts (integers).""" + count_columns = df.select(pl.exclude(config.GENE_ID_COLNAME)).columns + return all( + dtype + in ( + pl.Int8(), + pl.Int16(), + pl.Int32(), + pl.Int64(), + pl.UInt8(), + pl.UInt16(), + pl.UInt32(), + pl.UInt64(), + ) + for dtype in df.select(count_columns).schema.values() ) - cdna_length_series = gene_id_df[config.CDNA_LENGTH_COLNAME].astype(float) - cdna_length_series.index = gene_id_df[config.GENE_ID_COLNAME] - return cdna_length_series -def is_raw_counts(df: pd.DataFrame): - """Check if the data are raw counts (integers).""" - return all(df.dtypes.apply(lambda x: pd.api.types.is_integer_dtype(x))) +def is_tpm(df: pl.DataFrame) -> bool: + """Check if the data are TPM (sum to 1e6 per sample).""" + sample_sums_df = df.select(pl.exclude(config.GENE_ID_COLNAME).sum()) + # a small error is possible, and we assume that if the sum is close to 1e6, it is TPM + # setting the tolerance to 100 + is_tpm_col_df = sample_sums_df.select((pl.all() - 1e6).abs() < 1e2) + return is_tpm_col_df.select( + pl.any_horizontal(pl.all()) + ).item() # Allow for floating-point precision -def is_tpm(df: pd.DataFrame): - """Check if the data are TPM (sum to 1e6 per sample).""" - sample_sums = df.sum(axis=0) - return all((sample_sums - 1e6).abs() < 1e-2) # Allow for floating-point precision +def compute_rpkm(df: pl.DataFrame, cdna_length_df: pl.DataFrame) -> pl.DataFrame: + """ + Process raw counts to RPKM. + """ + logger.info("Computing RPKM.") + df = df.join(cdna_length_df, on=config.GENE_ID_COLNAME) + return df.select( + pl.col(config.GENE_ID_COLNAME), + pl.exclude([config.GENE_ID_COLNAME, config.CDNA_LENGTH_COLNAME]).truediv( + pl.col(config.CDNA_LENGTH_COLNAME) + ), + ) -def is_fpkm_or_rpkm(df: pd.DataFrame): - """Check if the data are FPKM or RPKM (not raw, not TPM).""" - return not is_raw_counts(df) and not is_tpm(df) +def compute_tpm_from_rpkm(rpkm_df: pl.DataFrame) -> pl.DataFrame: + """ + Process RPKM to TPM. + """ + logger.info("Computing TPM from RPKM.") + sums = rpkm_df.select(pl.exclude(config.GENE_ID_COLNAME).sum()) + # Divide each column by its sum and multiply by 1e6 + count_columns = rpkm_df.select(pl.exclude(config.GENE_ID_COLNAME)).columns + return rpkm_df.select( + [pl.col(config.GENE_ID_COLNAME)] + + [(pl.col(col) / sums[col][0] * 1e6).alias(col) for col in count_columns], + ) -def compute_tpm(df: pd.DataFrame, cdna_length_series: pd.Series): +def compute_tpm(df: pl.DataFrame, cdna_length_df: pl.DataFrame) -> pl.DataFrame: """ Process raw counts, FPKM, or RPKM to TPM. """ if is_raw_counts(df): logger.info("Raw counts detected → computing TPM directly.") - rpk = df.div(cdna_length_series, axis=0) # read per kilobase - tpm = rpk.div(rpk.sum(axis=0), axis=1) * 1e6 - return tpm - elif is_fpkm_or_rpkm(df): - # Convert FPKM/RPKM to TPM - logger.info("FPKM/RPKM detected → computing TPM.") - tpm = df.div(df.sum(axis=0), axis=1) * 1e6 - return tpm + rpkm_df = compute_rpkm(df, cdna_length_df) + return compute_tpm_from_rpkm(rpkm_df) elif is_tpm(df): logger.info("Data are already TPM. No conversion needed.") return df else: - raise ValueError("Could not determine data type.") - - -def export_normalised_data(count_df: pd.DataFrame, count_file: Path): - """Export gene expression data to Parquet.""" - # replace .csv / .tsv by .tpm.csv - outfilename = ".".join(count_file.name.split(".")[:-1]) + TPM_NORM_SUFFIX - logger.info(f"Exporting TPM normalised counts to: {outfilename}") - count_df.to_csv(outfilename, index=True, header=True) + # Convert FPKM/RPKM to TPM + logger.info("Assuming FPKM/RPKM normalisation.") + return compute_tpm_from_rpkm(df) ##################################################### @@ -120,19 +141,21 @@ def main(): try: logger.info("Parsing data") - count_df = parse_counts(args.count_file) - count_df.index.name = config.GENE_ID_COLNAME - - cdna_length_df = pd.read_csv(args.gene_lengths_file, header=0) + count_df = parse_count_table(args.count_file) + cdna_length_df = parse_table(args.gene_lengths_file) - logger.info("Reordering gene lengths") - cdna_length_series = reorder_gene_lengths(count_df, cdna_length_df) + logger.info("Converting data types") + count_df = try_cast_to_int(count_df) logger.info(f"Normalising {args.count_file.name}") + count_df = compute_tpm(count_df, cdna_length_df) - count_df = compute_tpm(count_df, cdna_length_series) + logger.info("Computing log2 values") + count_df = compute_log2(count_df) - export_normalised_data(count_df, args.count_file) + outfilename = args.count_file.with_suffix(OUTFILE_SUFFIX).name + logger.info(f"Exporting TPM normalised counts to: {outfilename}") + count_df.write_parquet(outfilename) except Exception as e: logger.error(f"Error occurred while normalising data: {e}") diff --git a/modules/local/normalisation/compute_tpm/environment.yml b/modules/local/normalisation/compute_tpm/environment.yml index 104de0e2..4fa78808 100644 --- a/modules/local/normalisation/compute_tpm/environment.yml +++ b/modules/local/normalisation/compute_tpm/environment.yml @@ -4,4 +4,4 @@ channels: - conda-forge - bioconda dependencies: - - conda-forge::pandas==2.3.3 + - conda-forge::polars==1.35.2 diff --git a/modules/local/normalisation/compute_tpm/main.nf b/modules/local/normalisation/compute_tpm/main.nf index 561ee580..83265fee 100644 --- a/modules/local/normalisation/compute_tpm/main.nf +++ b/modules/local/normalisation/compute_tpm/main.nf @@ -6,15 +6,15 @@ process NORMALISATION_COMPUTE_TPM { 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/3d/3d7126100b0eb7cb53dfb50291707ea8dda3b9738b76551ab73605d0acbe114b/data': - 'community.wave.seqera.io/library/pandas:2.3.3--5a902bf824a79745' }" + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': + 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" input: tuple val(meta), path(count_file) path gene_lengths_file output: - tuple val(meta), path('*.tpm.csv'), optional: true, emit: counts + tuple val(meta), path('*.tpm.parquet'), optional: true, emit: counts tuple val(meta.dataset), path("failure_reason.txt"), optional: true, topic: normalisation_failure_reason tuple val(meta.dataset), path("warning_reason.txt"), optional: true, topic: normalisation_warning_reason tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions From 6e618d7c8ca624528f0473be1cb08fb16706b04c Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 5 Jan 2026 17:30:04 +0100 Subject: [PATCH 255/258] replace pandas by polars in quantile normalisation --- bin/common.py | 6 +++ bin/quantile_normalise.py | 44 +++++++------------ .../quantile_normalisation/environment.yml | 5 +-- modules/local/quantile_normalisation/main.nf | 7 ++- 4 files changed, 28 insertions(+), 34 deletions(-) diff --git a/bin/common.py b/bin/common.py index d8e50e0f..8c875fd5 100644 --- a/bin/common.py +++ b/bin/common.py @@ -59,3 +59,9 @@ def compute_log2(df: pl.DataFrame) -> pl.DataFrame: pl.col(config.GENE_ID_COLNAME), (pl.exclude(config.GENE_ID_COLNAME) + 1).log(base=2), ) + + +def export_parquet(df: pl.DataFrame, count_file: Path, suffix: str): + outfilename = count_file.with_suffix(suffix).name + logger.info(f"Exporting processed counts to: {outfilename}") + df.write_parquet(outfilename) diff --git a/bin/quantile_normalise.py b/bin/quantile_normalise.py index 3403cd75..57a6dba6 100755 --- a/bin/quantile_normalise.py +++ b/bin/quantile_normalise.py @@ -7,13 +7,14 @@ from pathlib import Path import config -import pandas as pd -from sklearn.preprocessing import QuantileTransformer +import polars as pl +from common import export_parquet, parse_count_table +from sklearn.preprocessing import quantile_transform logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -QUANT_NORM_SUFFIX = ".quant_norm.parquet" +OUTFILE_SUFFIX = ".quant_norm.parquet" N_QUANTILES = 1000 @@ -45,28 +46,19 @@ def parse_args(): return parser.parse_args() -def quantile_normalise(data: pd.DataFrame, target_distribution: str): +def quantile_normalise(df: pl.DataFrame, target_distribution: str): """ - Quantile normalize a data matrix based on a target distribution. + Quantile normalize a dataframe; column by column, based on a target distribution. """ - transformer = QuantileTransformer( + kwargs = dict( n_quantiles=N_QUANTILES, output_distribution=target_distribution, subsample=None ) - - normalised_data = pd.DataFrame(index=data.index, columns=data.columns) - for col in data.columns: - normalised_data[col] = transformer.fit_transform(data[col].to_frame()) - - return normalised_data - - -def export_count_data(count_df: pd.DataFrame, count_file: Path): - """Export gene expression data to CSV files.""" - outfilename = count_file.name.replace(".csv", QUANT_NORM_SUFFIX) - logger.info(f"Exporting quantile normalised counts to: {outfilename}") - count_df.reset_index(inplace=True) - count_df[config.GENE_ID_COLNAME] = count_df[config.GENE_ID_COLNAME].astype(str) - count_df.to_parquet(outfilename) + return df.select( + pl.exclude(config.GENE_ID_COLNAME).map_batches( + lambda x: quantile_transform(x.to_frame(), **kwargs).flatten(), + return_dtype=pl.Float64, + ) + ) ##################################################### @@ -80,15 +72,13 @@ def main(): args = parse_args() count_file = args.count_file - logger.info(f"Quantile normalising {count_file.name}") - # count_df = pd.read_parquet(count_file) - # count_df.set_index(config.GENE_ID_COLNAME, inplace=True) - count_df = pd.read_csv(count_file, index_col=0) - count_df.index.name = config.GENE_ID_COLNAME + logger.info(f"Parsing {count_file.name}") + count_df = parse_count_table(count_file) + logger.info(f"Quantile normalising {count_file.name}") quantile_normalized_counts = quantile_normalise(count_df, args.target_distribution) - export_count_data(quantile_normalized_counts, count_file) + export_parquet(quantile_normalized_counts, count_file, OUTFILE_SUFFIX) if __name__ == "__main__": diff --git a/modules/local/quantile_normalisation/environment.yml b/modules/local/quantile_normalisation/environment.yml index 6c7a8ca7..d9e1398e 100644 --- a/modules/local/quantile_normalisation/environment.yml +++ b/modules/local/quantile_normalisation/environment.yml @@ -4,6 +4,5 @@ channels: - conda-forge - bioconda dependencies: - - conda-forge::pandas==2.3.3 - - conda-forge::scikit-learn==1.7.2 - - conda-forge::pyarrow==22.0.0 + - conda-forge::polars==1.36.1 + - conda-forge::scikit-learn==1.8.0 diff --git a/modules/local/quantile_normalisation/main.nf b/modules/local/quantile_normalisation/main.nf index adb9134a..c1d7e94c 100644 --- a/modules/local/quantile_normalisation/main.nf +++ b/modules/local/quantile_normalisation/main.nf @@ -6,8 +6,8 @@ process QUANTILE_NORMALISATION { 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/2d/2df931a4ea181fe1ea9527abe0fd4aff9453d6ea56d56aee7c4ac5dceed611e3/data': - 'community.wave.seqera.io/library/pandas_pyarrow_python_scikit-learn:6f85e3c4d1706e81' }" + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/eb/eb8feda3812519f6f6f085e1d058f534b0aedba570c1443c4479d79975e81906/data': + 'community.wave.seqera.io/library/polars_scikit-learn:a30d22b117dad962' }" input: tuple val(meta), path(count_file) @@ -16,9 +16,8 @@ process QUANTILE_NORMALISATION { output: tuple val(meta), path('*.quant_norm.parquet'), emit: counts tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions - tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions + tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions tuple val("${task.process}"), val('scikit-learn'), eval('python3 -c "import sklearn; print(sklearn.__version__)"'), topic: versions - tuple val("${task.process}"), val('pyarrow'), eval('python3 -c "import pyarrow; print(pyarrow.__version__)"'), topic: versions script: """ From 98d707ab4a5b99f796f0935c68edf5034a898744 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 5 Jan 2026 17:53:22 +0100 Subject: [PATCH 256/258] change pandas to polars in compute_dataset_statistics.py --- bin/compute_dataset_statistics.py | 38 ++++++++++--------- .../environment.yml | 2 +- .../local/compute_dataset_statistics/main.nf | 6 +-- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/bin/compute_dataset_statistics.py b/bin/compute_dataset_statistics.py index be8217dc..985099f7 100755 --- a/bin/compute_dataset_statistics.py +++ b/bin/compute_dataset_statistics.py @@ -6,15 +6,14 @@ import logging from pathlib import Path -import pandas as pd +import config +import polars as pl +from common import parse_count_table logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -COL_TO_OUTFILE = {"skewness": "skewness.txt", "ratio_zeros": "ratio_zeros.txt"} - - -# ALLOWED_TARGET_DISTRIBUTIONS = ["normal", "uniform"] +KEY_TO_OUTFILE = {"skewness": "skewness.txt", "ratio_zeros": "ratio_zeros.txt"} ##################################################### @@ -34,22 +33,25 @@ def parse_args(): return parser.parse_args() -def compute_dataset_statistics(count_df: pd.DataFrame) -> pd.DataFrame: - skewness = count_df.skew() - ratio_zeros = (count_df == 0).sum() / len(count_df) - return pd.DataFrame({"skewness": skewness, "ratio_zeros": ratio_zeros}).T +def compute_dataset_statistics(df: pl.DataFrame) -> dict: + # sample count skewness + skewness = df.select(pl.exclude(config.GENE_ID_COLNAME).skew()).row(0) + # sample count ratio of zeros + ratio_zeros = df.select( + pl.exclude(config.GENE_ID_COLNAME).eq(pl.lit(0)).sum() / len(df) + ).row(0) + return dict(skewness=list(skewness), ratio_zeros=list(ratio_zeros)) -def export_count_data(dataset_stats_df: pd.DataFrame): +def export_count_data(stats: dict): """ Export dataset statistics to CSV files. Write each statistic to a separate file, on a single row """ - for col, outfile_name in COL_TO_OUTFILE.items(): - logger.info(f"Exporting dataset statistics {col} to: {outfile_name}") - pd.DataFrame(dataset_stats_df.loc[col]).T.to_csv( - outfile_name, index=False, header=False, float_format="%.4f" - ) + for key, outfile_name in KEY_TO_OUTFILE.items(): + logger.info(f"Exporting dataset statistics {key} to: {outfile_name}") + with open(outfile_name, "w") as outfile: + outfile.write(",".join([str(val) for val in stats[key]])) ##################################################### @@ -64,11 +66,11 @@ def main(): count_file = args.count_file logger.info(f"Computing dataset statistics for {count_file.name}") - count_df = pd.read_csv(count_file, index_col=0, header=0) + count_df = parse_count_table(count_file) - dataset_stats_df = compute_dataset_statistics(count_df) + stat_dict = compute_dataset_statistics(count_df) - export_count_data(dataset_stats_df) + export_count_data(stat_dict) if __name__ == "__main__": diff --git a/modules/local/compute_dataset_statistics/environment.yml b/modules/local/compute_dataset_statistics/environment.yml index 104de0e2..4fa78808 100644 --- a/modules/local/compute_dataset_statistics/environment.yml +++ b/modules/local/compute_dataset_statistics/environment.yml @@ -4,4 +4,4 @@ channels: - conda-forge - bioconda dependencies: - - conda-forge::pandas==2.3.3 + - conda-forge::polars==1.35.2 diff --git a/modules/local/compute_dataset_statistics/main.nf b/modules/local/compute_dataset_statistics/main.nf index c5660fb9..6280d8dc 100644 --- a/modules/local/compute_dataset_statistics/main.nf +++ b/modules/local/compute_dataset_statistics/main.nf @@ -6,8 +6,8 @@ process COMPUTE_DATASET_STATISTICS { 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/3d/3d7126100b0eb7cb53dfb50291707ea8dda3b9738b76551ab73605d0acbe114b/data': - 'community.wave.seqera.io/library/pandas:2.3.3--5a902bf824a79745' }" + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': + 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" input: tuple val(meta), path(count_file) @@ -16,7 +16,7 @@ process COMPUTE_DATASET_STATISTICS { tuple val(meta.dataset), path("skewness.txt"), topic: skewness tuple val(meta.dataset), path("ratio_zeros.txt"), topic: ratio_zeros tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions - tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions + tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions script: def prefix = task.ext.prefix ?: "${meta.dataset}" From 5122121b6e25c9195a05d74c78e132a9fdf83ea1 Mon Sep 17 00:00:00 2001 From: Olivier Date: Tue, 6 Jan 2026 00:20:28 +0100 Subject: [PATCH 257/258] big refactoring in order to better scale --- bin/aggregate_results.py | 17 ++----- bin/compute_cpm.py | 10 ++-- bin/compute_tpm.py | 6 +-- ...ood_occurrence.py => detect_rare_genes.py} | 0 ...gene_ids.py => filter_and_rename_genes.py} | 7 +-- bin/quantile_normalise.py | 2 +- conf/base.config | 4 +- conf/local.config | 17 ------- conf/modules/aggregation.config | 8 ++++ modules/local/aggregate_results/main.nf | 10 ++-- modules/local/compute_base_statistics/main.nf | 10 ++-- .../environment.yml | 0 .../main.nf | 4 +- .../environment.yml | 0 .../main.nf | 4 +- modules/local/gprofiler/idmapping/main.nf | 2 +- modules/local/merge_counts/main.nf | 10 ++-- .../normalisation/compute_cpm/environment.yml | 2 +- .../local/normalisation/compute_cpm/main.nf | 8 ++-- .../local/normalisation/compute_tpm/main.nf | 2 +- nextflow.config | 1 - nextflow_schema.json | 2 +- subworkflows/local/base_statistics/main.nf | 29 ++++------- .../local/get_public_accessions/main.nf | 4 +- subworkflows/local/idmapping/main.nf | 12 ++--- subworkflows/local/merge_data/main.nf | 48 +++++++++++-------- workflows/stableexpression.nf | 14 ++---- 27 files changed, 101 insertions(+), 132 deletions(-) rename bin/{get_genes_with_good_occurrence.py => detect_rare_genes.py} (100%) rename bin/{rename_gene_ids.py => filter_and_rename_genes.py} (96%) delete mode 100644 conf/local.config rename modules/local/{filter_out_rare_genes => detect_rare_genes}/environment.yml (100%) rename modules/local/{filter_out_rare_genes => detect_rare_genes}/main.nf (96%) rename modules/local/{rename_gene_ids => filter_and_rename_genes}/environment.yml (100%) rename modules/local/{rename_gene_ids => filter_and_rename_genes}/main.nf (96%) diff --git a/bin/aggregate_results.py b/bin/aggregate_results.py index f3ee5e63..9fccdc9a 100755 --- a/bin/aggregate_results.py +++ b/bin/aggregate_results.py @@ -48,16 +48,11 @@ def parse_args(): help="File containing statistics for all genes and stability scores by candidate genes", ) parser.add_argument( - "--rnaseq", + "--platform-stats", type=Path, - dest="rnaseq_dataset_stat_file", - help="File containing base statistics for all genes and for all RNAseq datasets", - ) - parser.add_argument( - "--microarray", - type=Path, - dest="microarray_dataset_stat_file", - help="File containing base statistics for all genes and for all Microarray datasets", + dest="platform_stat_files", + nargs="+", + help="File containing base statistics for all genes and for all datasets for a specific platform", ) parser.add_argument( "--metadata", @@ -269,9 +264,7 @@ def main(): all_genes_stat_summary_df = parse_stat_file(args.stat_file) platform_datasets_stat_dfs = [ - parse_stat_file(file) - for file in [args.rnaseq_dataset_stat_file, args.microarray_dataset_stat_file] - if file is not None + parse_stat_file(file) for file in args.platform_stat_files if file is not None ] metadata_df = get_metadata(metadata_files) diff --git a/bin/compute_cpm.py b/bin/compute_cpm.py index 27df84d0..b2548896 100755 --- a/bin/compute_cpm.py +++ b/bin/compute_cpm.py @@ -9,7 +9,7 @@ import config import polars as pl -from common import compute_log2, parse_count_table +from common import compute_log2, export_parquet, parse_count_table logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -42,12 +42,12 @@ def calculate_cpm(df: pl.DataFrame) -> pl.DataFrame: Parameters: ----------- - counts_df : pandas.DataFrame + counts_df : polars.DataFrame DataFrame with genes as rows and samples as columns Returns: -------- - cpm_df : pandas.DataFrame + cpm_df : polars.DataFrame DataFrame with CPM values """ # Calculate total counts per sample (column sums) @@ -82,9 +82,7 @@ def main(): logger.info("Computing log2 values") count_df = compute_log2(count_df) - outfilename = args.count_file.with_suffix(OUTFILE_SUFFIX).name - logger.info(f"Exporting TPM normalised counts to: {outfilename}") - count_df.write_parquet(outfilename) + export_parquet(count_df, args.count_file, OUTFILE_SUFFIX) except Exception as e: logger.error(f"Error occurred while normalising data: {e}") diff --git a/bin/compute_tpm.py b/bin/compute_tpm.py index c3283f9b..77e936b3 100755 --- a/bin/compute_tpm.py +++ b/bin/compute_tpm.py @@ -9,7 +9,7 @@ import config import polars as pl -from common import compute_log2, parse_count_table, parse_table +from common import compute_log2, export_parquet, parse_count_table, parse_table logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -153,9 +153,7 @@ def main(): logger.info("Computing log2 values") count_df = compute_log2(count_df) - outfilename = args.count_file.with_suffix(OUTFILE_SUFFIX).name - logger.info(f"Exporting TPM normalised counts to: {outfilename}") - count_df.write_parquet(outfilename) + export_parquet(count_df, args.count_file, OUTFILE_SUFFIX) except Exception as e: logger.error(f"Error occurred while normalising data: {e}") diff --git a/bin/get_genes_with_good_occurrence.py b/bin/detect_rare_genes.py similarity index 100% rename from bin/get_genes_with_good_occurrence.py rename to bin/detect_rare_genes.py diff --git a/bin/rename_gene_ids.py b/bin/filter_and_rename_genes.py similarity index 96% rename from bin/rename_gene_ids.py rename to bin/filter_and_rename_genes.py index 1f0ec4e8..106f8013 100755 --- a/bin/rename_gene_ids.py +++ b/bin/filter_and_rename_genes.py @@ -194,11 +194,12 @@ def main(): # TODO: check is there is another way to avoid duplicate gene names # sometimes different gene names have the same Gene ID - # for now, we just get the mean of values, but this is not ideal + # for now, we just get the max of values, but this is not ideal + # we do not take the mean because if counts are integers, we want to keep them as integers - logger.info("Computing mean counts for genes with duplicate IDs") + logger.info("Computing max counts for genes with duplicate IDs") df = df.group_by(config.GENE_ID_COLNAME, maintain_order=True).agg( - pl.exclude(config.GENE_ID_COLNAME).mean() + pl.exclude(config.GENE_ID_COLNAME).max() ) ############################################################# diff --git a/bin/quantile_normalise.py b/bin/quantile_normalise.py index 57a6dba6..241cd536 100755 --- a/bin/quantile_normalise.py +++ b/bin/quantile_normalise.py @@ -53,7 +53,7 @@ def quantile_normalise(df: pl.DataFrame, target_distribution: str): kwargs = dict( n_quantiles=N_QUANTILES, output_distribution=target_distribution, subsample=None ) - return df.select( + return df.with_columns( pl.exclude(config.GENE_ID_COLNAME).map_batches( lambda x: quantile_transform(x.to_frame(), **kwargs).flatten(), return_dtype=pl.Float64, diff --git a/conf/base.config b/conf/base.config index eeb8957a..614bbb35 100644 --- a/conf/base.config +++ b/conf/base.config @@ -9,7 +9,7 @@ */ executor { - cpus = 12 + cpus = 8 memory = 24.GB } @@ -64,7 +64,7 @@ process { time = { 4.h * task.attempt } } withLabel:process_high { - cpus = { 4 } + cpus = { 4 } memory = { 8.GB + 4.GB * task.attempt } time = { 8.h * task.attempt } } diff --git a/conf/local.config b/conf/local.config deleted file mode 100644 index 7eaf470d..00000000 --- a/conf/local.config +++ /dev/null @@ -1,17 +0,0 @@ -profiles { - local { - - process { - resourceLimits = [ - cpus: 16, - memory: '25.GB', - time: '4.h' - ] - } - - executor { - cpus = 8 - memory = 25.GB - } - } -} diff --git a/conf/modules/aggregation.config b/conf/modules/aggregation.config index 8b9dfa83..46a07491 100644 --- a/conf/modules/aggregation.config +++ b/conf/modules/aggregation.config @@ -7,4 +7,12 @@ process { ] } + withName: MERGE_PLATFORM_COUNTS { + maxForks = 1 + } + + withName: COMPUTE_PLATFORM_STATISTICS { + maxForks = 1 + } + } diff --git a/modules/local/aggregate_results/main.nf b/modules/local/aggregate_results/main.nf index 1402f24b..b0d54c42 100644 --- a/modules/local/aggregate_results/main.nf +++ b/modules/local/aggregate_results/main.nf @@ -10,8 +10,7 @@ process AGGREGATE_RESULTS { input: path count_file path stat_file - path rnaseq_dataset_stat_file - path microarray_dataset_stat_file + path platform_stat_files, stageAs: "?/*" path metadata_files path mapping_files @@ -26,8 +25,6 @@ process AGGREGATE_RESULTS { script: def mapping_files_arg = mapping_files ? "--mappings " + "$mapping_files" : "" def metadata_files_arg = metadata_files ? "--metadata " + "$metadata_files" : "" - def rnaseq_dataset_stat_file_arg = rnaseq_dataset_stat_file ? "--rnaseq $rnaseq_dataset_stat_file" : "" - def microarray_dataset_stat_file_arg = microarray_dataset_stat_file ? "--microarray $microarray_dataset_stat_file" : "" def is_using_containers = workflow.containerEngine ? true : false """ # limiting number of threads when using conda / micromamba @@ -38,10 +35,9 @@ process AGGREGATE_RESULTS { aggregate_results.py \\ --counts $count_file \\ --stats $stat_file \\ + --platform-stats $platform_stat_files \\ $mapping_files_arg \\ - $metadata_files_arg \\ - $rnaseq_dataset_stat_file_arg \\ - $microarray_dataset_stat_file_arg + $metadata_files_arg """ } diff --git a/modules/local/compute_base_statistics/main.nf b/modules/local/compute_base_statistics/main.nf index 7bf75279..b10c8da4 100644 --- a/modules/local/compute_base_statistics/main.nf +++ b/modules/local/compute_base_statistics/main.nf @@ -1,8 +1,9 @@ process COMPUTE_BASE_STATISTICS { + tag "${meta.platform}" label 'process_high' - memory { def calc = (dataset_size / 50000).toInteger() + memory { def calc = (meta.dataset_size / 50000).toInteger() def result = Math.max(1, calc) // Ensure at least 1 MB def multiplicator = 1 + 0.2 * task.attempt // increase memory usage with each attempt by 20% return 1.MB * result * multiplicator @@ -14,8 +15,7 @@ process COMPUTE_BASE_STATISTICS { 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" input: - path count_file - val platform + tuple val(meta), path(count_file) output: path '*stats_all_genes.csv', emit: stats @@ -24,8 +24,8 @@ process COMPUTE_BASE_STATISTICS { script: def args = task.ext.args ?: '' - if ( platform != [] ) { - args += " --platform $platform" + if ( meta.platform != "all" ) { + args += " --platform $meta.platform" } def is_using_containers = workflow.containerEngine ? true : false """ diff --git a/modules/local/filter_out_rare_genes/environment.yml b/modules/local/detect_rare_genes/environment.yml similarity index 100% rename from modules/local/filter_out_rare_genes/environment.yml rename to modules/local/detect_rare_genes/environment.yml diff --git a/modules/local/filter_out_rare_genes/main.nf b/modules/local/detect_rare_genes/main.nf similarity index 96% rename from modules/local/filter_out_rare_genes/main.nf rename to modules/local/detect_rare_genes/main.nf index ea2842f6..2f45c375 100644 --- a/modules/local/filter_out_rare_genes/main.nf +++ b/modules/local/detect_rare_genes/main.nf @@ -1,4 +1,4 @@ -process FILTER_OUT_RARE_GENES { +process DETECT_RARE_GENES { label 'process_low' @@ -28,7 +28,7 @@ process FILTER_OUT_RARE_GENES { export POLARS_MAX_THREADS=${task.cpus} fi - get_genes_with_good_occurrence.py \\ + detect_rare_genes.py \\ --occurrences $gene_id_occurrences_file \\ --mappings $gene_id_mapping_file \\ --nb-datasets $nb_datasets \\ diff --git a/modules/local/rename_gene_ids/environment.yml b/modules/local/filter_and_rename_genes/environment.yml similarity index 100% rename from modules/local/rename_gene_ids/environment.yml rename to modules/local/filter_and_rename_genes/environment.yml diff --git a/modules/local/rename_gene_ids/main.nf b/modules/local/filter_and_rename_genes/main.nf similarity index 96% rename from modules/local/rename_gene_ids/main.nf rename to modules/local/filter_and_rename_genes/main.nf index c6985f15..684c2f04 100644 --- a/modules/local/rename_gene_ids/main.nf +++ b/modules/local/filter_and_rename_genes/main.nf @@ -1,4 +1,4 @@ -process RENAME_GENE_IDS { +process FILTER_AND_RENAME_GENES { label 'process_low' @@ -32,7 +32,7 @@ process RENAME_GENE_IDS { export POLARS_MAX_THREADS=${task.cpus} fi - rename_gene_ids.py \\ + filter_and_rename_genes.py \\ --count-file "$count_file" \\ $mapping_arg \\ $valid_ids_arg diff --git a/modules/local/gprofiler/idmapping/main.nf b/modules/local/gprofiler/idmapping/main.nf index b7f2b2db..64106af7 100644 --- a/modules/local/gprofiler/idmapping/main.nf +++ b/modules/local/gprofiler/idmapping/main.nf @@ -3,7 +3,7 @@ process GPROFILER_IDMAPPING { tag "${species} IDs to ${gprofiler_target_db}" - errorStrategy = { + errorStrategy { if (task.exitStatus == 100 ) { log.error("Could not map gene IDs to ${gprofiler_target_db} database.") 'terminate' diff --git a/modules/local/merge_counts/main.nf b/modules/local/merge_counts/main.nf index a897a563..9302a67d 100644 --- a/modules/local/merge_counts/main.nf +++ b/modules/local/merge_counts/main.nf @@ -1,8 +1,11 @@ process MERGE_COUNTS { + tag "${meta.platform}" label "process_high" - memory { def calc = (dataset_size / 50000).toInteger() + maxForks 1 + + memory { def calc = (meta.dataset_size / 50000).toInteger() def result = Math.max(1, calc) // Ensure at least 1 MB def multiplicator = 1 + 0.2 * task.attempt // increase memory usage with each attempt by 20% return 1.MB * result * multiplicator @@ -14,11 +17,10 @@ process MERGE_COUNTS { 'community.wave.seqera.io/library/polars_tqdm:54b124dde91d1bf3' }" input: - path count_files, stageAs: "?/*" - val dataset_size + tuple val(meta), path(count_files, stageAs: "?/*") output: - path 'all_counts.parquet', emit: counts + tuple val(meta), path('all_counts.parquet'), emit: counts tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions tuple val("${task.process}"), val('tqdm'), eval('python3 -c "import tqdm; print(tqdm.__version__)"'), topic: versions diff --git a/modules/local/normalisation/compute_cpm/environment.yml b/modules/local/normalisation/compute_cpm/environment.yml index 104de0e2..4fa78808 100644 --- a/modules/local/normalisation/compute_cpm/environment.yml +++ b/modules/local/normalisation/compute_cpm/environment.yml @@ -4,4 +4,4 @@ channels: - conda-forge - bioconda dependencies: - - conda-forge::pandas==2.3.3 + - conda-forge::polars==1.35.2 diff --git a/modules/local/normalisation/compute_cpm/main.nf b/modules/local/normalisation/compute_cpm/main.nf index 06fc5a0c..5045c4f4 100644 --- a/modules/local/normalisation/compute_cpm/main.nf +++ b/modules/local/normalisation/compute_cpm/main.nf @@ -6,18 +6,18 @@ process NORMALISATION_COMPUTE_CPM { 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/3d/3d7126100b0eb7cb53dfb50291707ea8dda3b9738b76551ab73605d0acbe114b/data': - 'community.wave.seqera.io/library/pandas:2.3.3--5a902bf824a79745' }" + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': + 'community.wave.seqera.io/library/polars_python:cab787b788e5eba7' }" input: tuple val(meta), path(count_file) output: - tuple val(meta), path('*.cpm.csv'), optional: true, emit: counts + tuple val(meta), path('*.cpm.parquet'), optional: true, emit: counts tuple val(meta.dataset), path("failure_reason.txt"), optional: true, topic: normalisation_failure_reason tuple val(meta.dataset), path("warning_reason.txt"), optional: true, topic: normalisation_warning_reason tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions - tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions + tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions script: """ diff --git a/modules/local/normalisation/compute_tpm/main.nf b/modules/local/normalisation/compute_tpm/main.nf index 83265fee..c5b57203 100644 --- a/modules/local/normalisation/compute_tpm/main.nf +++ b/modules/local/normalisation/compute_tpm/main.nf @@ -18,7 +18,7 @@ process NORMALISATION_COMPUTE_TPM { tuple val(meta.dataset), path("failure_reason.txt"), optional: true, topic: normalisation_failure_reason tuple val(meta.dataset), path("warning_reason.txt"), optional: true, topic: normalisation_warning_reason tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions - tuple val("${task.process}"), val('pandas'), eval('python3 -c "import pandas; print(pandas.__version__)"'), topic: versions + tuple val("${task.process}"), val('polars'), eval('python3 -c "import polars; print(polars.__version__)"'), topic: versions script: """ diff --git a/nextflow.config b/nextflow.config index ec2fd0da..8fe61c85 100644 --- a/nextflow.config +++ b/nextflow.config @@ -216,7 +216,6 @@ profiles { test { includeConfig 'conf/test.config' } test_full { includeConfig 'conf/test_full.config' } test_dataset_eatlas { includeConfig 'conf/test_dataset_eatlas.config' } - local { includeConfig 'conf/local.config' } } // Load nf-core custom profiles from different institutions diff --git a/nextflow_schema.json b/nextflow_schema.json index f63eced3..23901fa9 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -208,7 +208,7 @@ "exists": true, "schema": "assets/schema_gene_length.json", "mimetype": "text/csv", - "pattern": "^\\S+\\.(csv|dat)$", + "pattern": "^\\S+\\.(csv|tsv|dat)$", "description": "Gene length file", "help_text": "Path to comma-separated file containing gene lengths. Each row represents a gene and gives the length of its longest transcript. The file should be a comma-separated file with 2 columns (gene_id and length) and a header row.", "fa_icon": "fas fa-file" diff --git a/subworkflows/local/base_statistics/main.nf b/subworkflows/local/base_statistics/main.nf index 80e0caf1..8ac6ff15 100644 --- a/subworkflows/local/base_statistics/main.nf +++ b/subworkflows/local/base_statistics/main.nf @@ -1,6 +1,5 @@ -include { COMPUTE_BASE_STATISTICS } from '../../../modules/local/compute_base_statistics' -include { COMPUTE_BASE_STATISTICS as COMPUTE_BASE_STATISTICS_FOR_RNASEQ } from '../../../modules/local/compute_base_statistics' -include { COMPUTE_BASE_STATISTICS as COMPUTE_BASE_STATISTICS_FOR_MICROARRAY } from '../../../modules/local/compute_base_statistics' +include { COMPUTE_BASE_STATISTICS as COMPUTE_GLOBAL_STATISTICS } from '../../../modules/local/compute_base_statistics' +include { COMPUTE_BASE_STATISTICS as COMPUTE_PLATFORM_STATISTICS } from '../../../modules/local/compute_base_statistics' /* ======================================================================================== @@ -11,9 +10,8 @@ include { COMPUTE_BASE_STATISTICS as COMPUTE_BASE_STATISTICS_FOR_MICROARRAY workflow BASE_STATISTICS { take: - ch_all_counts - ch_rnaseq_counts - ch_microarray_counts + ch_all_counts // [ [ platform: platform, dataset_size: size], file ] + ch_platform_counts // [ [ platform: platform, dataset_size: size], file ] main: @@ -21,28 +19,17 @@ workflow BASE_STATISTICS { // PLATFORM-SPECIFIC STATISTICS // ----------------------------------------------------------------- - COMPUTE_BASE_STATISTICS_FOR_RNASEQ( - ch_rnaseq_counts.collect(), // single item - "rnaseq" - ) + COMPUTE_PLATFORM_STATISTICS( ch_platform_counts ) - COMPUTE_BASE_STATISTICS_FOR_MICROARRAY( - ch_microarray_counts.collect(), // single item - "microarray" - ) // ----------------------------------------------------------------- // ALL DATA // ----------------------------------------------------------------- - COMPUTE_BASE_STATISTICS ( - ch_all_counts.collect(), // single item - [] - ) + COMPUTE_GLOBAL_STATISTICS( ch_all_counts ) emit: - stats = COMPUTE_BASE_STATISTICS.out.stats - rnaseq_stats = COMPUTE_BASE_STATISTICS_FOR_RNASEQ.out.stats - microarray_stats = COMPUTE_BASE_STATISTICS_FOR_MICROARRAY.out.stats + stats = COMPUTE_GLOBAL_STATISTICS.out.stats + platform_stats = COMPUTE_PLATFORM_STATISTICS.out.stats } diff --git a/subworkflows/local/get_public_accessions/main.nf b/subworkflows/local/get_public_accessions/main.nf index 0959cf83..debb29e0 100644 --- a/subworkflows/local/get_public_accessions/main.nf +++ b/subworkflows/local/get_public_accessions/main.nf @@ -74,8 +74,8 @@ workflow GET_PUBLIC_ACCESSIONS { // trick to avoid fetching accessions from GEO when the sampling quota is already exceeded ch_species = channel.of( species ) .combine( ch_sampling_quota ) - .filter { species, quota -> quota == "ok" } - .map { species, quota -> species } + .filter { species_name, quota -> quota == "ok" } + .map { species_name, quota -> species_name } // getting GEO accessions given a species name and keywords // keywords can be an empty string diff --git a/subworkflows/local/idmapping/main.nf b/subworkflows/local/idmapping/main.nf index c6802db8..cce21a54 100644 --- a/subworkflows/local/idmapping/main.nf +++ b/subworkflows/local/idmapping/main.nf @@ -1,8 +1,8 @@ include { CLEAN_GENE_IDS } from '../../../modules/local/clean_gene_ids' include { COLLECT_GENE_IDS } from '../../../modules/local/collect_gene_ids' include { GPROFILER_IDMAPPING } from '../../../modules/local/gprofiler/idmapping' -include { FILTER_OUT_RARE_GENES } from '../../../modules/local/filter_out_rare_genes' -include { RENAME_GENE_IDS } from '../../../modules/local/rename_gene_ids' +include { DETECT_RARE_GENES } from '../../../modules/local/detect_rare_genes' +include { FILTER_AND_RENAME_GENES } from '../../../modules/local/filter_and_rename_genes' /* ======================================================================================== @@ -64,14 +64,14 @@ workflow ID_MAPPING { // FILTERING OUT GENE IDS THAT DO NOT HAVE ENOUGH OCCURRENCES // ----------------------------------------------------------------- - FILTER_OUT_RARE_GENES( + DETECT_RARE_GENES( ch_gene_id_mapping, COLLECT_GENE_IDS.out.gene_id_occurrences, ch_counts.count(), min_occurrence_freq, min_occurrence_quantile ) - ch_valid_gene_ids = FILTER_OUT_RARE_GENES.out.valid_gene_ids + ch_valid_gene_ids = DETECT_RARE_GENES.out.valid_gene_ids } // ----------------------------------------------------------------- @@ -120,12 +120,12 @@ workflow ID_MAPPING { if ( !skip_id_mapping || custom_gene_id_mapping ) { - RENAME_GENE_IDS( + FILTER_AND_RENAME_GENES( ch_counts, ch_global_gene_id_mapping.first(), ch_valid_gene_ids.collect() ) - ch_counts = RENAME_GENE_IDS.out.counts + ch_counts = FILTER_AND_RENAME_GENES.out.counts } diff --git a/subworkflows/local/merge_data/main.nf b/subworkflows/local/merge_data/main.nf index 44742b74..ca1d2501 100644 --- a/subworkflows/local/merge_data/main.nf +++ b/subworkflows/local/merge_data/main.nf @@ -1,6 +1,5 @@ +include { MERGE_COUNTS as MERGE_PLATFORM_COUNTS } from '../../../modules/local/merge_counts' include { MERGE_COUNTS as MERGE_ALL_COUNTS } from '../../../modules/local/merge_counts' -include { MERGE_COUNTS as MERGE_RNASEQ_COUNTS } from '../../../modules/local/merge_counts' -include { MERGE_COUNTS as MERGE_MICROARRAY_COUNTS } from '../../../modules/local/merge_counts' include { getWholeDatasetSize } from '../../../subworkflows/local/utils_nfcore_stableexpression_pipeline' @@ -28,36 +27,46 @@ workflow MERGE_DATA { ch_normalised_rnaseq_counts = ch_normalised_counts.filter { meta, file -> meta.platform == "rnaseq" } ch_whole_rnaseq_size = getWholeDatasetSize ( ch_normalised_rnaseq_counts ) - MERGE_RNASEQ_COUNTS ( - ch_normalised_rnaseq_counts.map { meta, file -> file }.collect( sort: true ), - ch_whole_rnaseq_size.collect() // single item - ) - // MICROARRAY ch_normalised_microarray_counts = ch_normalised_counts.filter { meta, file -> meta.platform == "microarray" } ch_whole_microarray_size = getWholeDatasetSize ( ch_normalised_microarray_counts ) - MERGE_MICROARRAY_COUNTS ( - ch_normalised_microarray_counts.map { meta, file -> file }.collect( sort: true ), - ch_whole_microarray_size.collect() // single item + ch_collected_rnaseq_counts = ch_normalised_rnaseq_counts + .map { meta, file -> file } + .collect( sort: true ) + .map { files -> [ files ] } + .combine( ch_whole_rnaseq_size ) + .map { files, size -> [ [ platform: "rnaseq", dataset_size: size ], files ] } + + ch_collected_microarray_counts = ch_normalised_microarray_counts + .map { meta, file -> file } + .collect( sort: true ) + .map { files -> [ files ] } + .combine( ch_whole_microarray_size ) + .map { files, size -> [ [ platform: "microarray", dataset_size: size ], files ] } + + MERGE_PLATFORM_COUNTS ( + ch_collected_rnaseq_counts.concat( ch_collected_microarray_counts ) ) + ch_platform_counts = MERGE_PLATFORM_COUNTS.out.counts + // ----------------------------------------------------------------- // MERGE ALL COUNTS // ----------------------------------------------------------------- - ch_merged_rnaseq_counts = MERGE_RNASEQ_COUNTS.out.counts - ch_merged_microarray_counts = MERGE_MICROARRAY_COUNTS.out.counts - ch_platform_counts = ch_merged_rnaseq_counts.mix ( ch_merged_microarray_counts ) - ch_whole_size = ch_whole_rnaseq_size .mix(ch_whole_microarray_size) .reduce { rnaseq_size, microarray_size -> rnaseq_size + microarray_size } - MERGE_ALL_COUNTS( - ch_platform_counts.collect( sort: true ), - ch_whole_size.collect() // single item - ) + ch_collected_merged_counts = ch_platform_counts + .map { meta, file -> file } + .collect( sort: true ) + .map { files -> [ files ] } + .combine( ch_whole_size ) + .map { files, size -> [ [ platform: "all", dataset_size: size ], files ] } + + MERGE_ALL_COUNTS( ch_collected_merged_counts ) // ----------------------------------------------------------------- // MERGE ALL DESIGNS IN A SINGLE TABLE @@ -126,8 +135,7 @@ workflow MERGE_DATA { emit: all_counts = MERGE_ALL_COUNTS.out.counts - rnaseq_counts = ch_merged_rnaseq_counts - microarray_counts = ch_merged_microarray_counts + platform_counts = ch_platform_counts whole_design = ch_whole_design whole_gene_id_mapping = ch_whole_gene_id_mapping whole_gene_metadata = ch_whole_gene_metadata diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 4bf9f262..262cf2b7 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -109,8 +109,6 @@ workflow STABLEEXPRESSION { ch_gene_id_mapping = ID_MAPPING.out.mapping ch_gene_metadata = ID_MAPPING.out.metadata - ch_counts = storeDatasetSize( ch_counts, "nb_genes", "nb_samples" ) - // ----------------------------------------------------------------- // FILTER OUT SAMPLES NOT VALID // ----------------------------------------------------------------- @@ -155,8 +153,7 @@ workflow STABLEEXPRESSION { BASE_STATISTICS ( ch_all_counts, - MERGE_DATA.out.rnaseq_counts, - MERGE_DATA.out.microarray_counts + MERGE_DATA.out.platform_counts ) ch_all_datasets_stats = BASE_STATISTICS.out.stats @@ -166,7 +163,7 @@ workflow STABLEEXPRESSION { // ----------------------------------------------------------------- STABILITY_SCORING ( - ch_all_counts, + ch_all_counts.map{ meta, file -> file }, ch_whole_design, ch_all_datasets_stats, params.candidate_selection_descriptor, @@ -183,10 +180,9 @@ workflow STABLEEXPRESSION { // ----------------------------------------------------------------- AGGREGATE_RESULTS ( - ch_all_counts.collect(), + ch_all_counts.map{ meta, file -> file }.collect(), ch_stats_all_genes_with_scores.collect(), - BASE_STATISTICS.out.rnaseq_stats.ifEmpty( [] ), - BASE_STATISTICS.out.microarray_stats.ifEmpty( [] ), + BASE_STATISTICS.out.platform_stats.collect(), MERGE_DATA.out.whole_gene_metadata.collect(), MERGE_DATA.out.whole_gene_id_mapping.collect() ) @@ -200,7 +196,7 @@ workflow STABLEEXPRESSION { // ----------------------------------------------------------------- DASH_APP( - ch_all_counts.collect(), + ch_all_counts.map{ meta, file -> file }.collect(), ch_whole_design.collect(), ch_all_genes_summary.collect() ) From 50472fea3281cf8aba9ad170e692f88d0d65d2ea Mon Sep 17 00:00:00 2001 From: Olivier Date: Wed, 7 Jan 2026 19:10:40 +0100 Subject: [PATCH 258/258] pass nf-tests --- assets/multiqc_config.yml | 2 +- bin/collect_gene_ids.py | 4 +- bin/gprofiler_map_ids.py | 6 +- .../local/aggregate_results/environment.yml | 1 + modules/local/clean_gene_ids/environment.yml | 1 + .../local/collect_gene_ids/environment.yml | 1 + modules/local/collect_gene_ids/main.nf | 4 +- .../local/collect_statistics/environment.yml | 1 + .../compute_base_statistics/environment.yml | 1 + modules/local/compute_base_statistics/main.nf | 6 - .../environment.yml | 1 + .../environment.yml | 1 + .../compute_stability_scores/environment.yml | 1 + modules/local/dash_app/app/environment.yml | 1 + .../local/detect_rare_genes/environment.yml | 1 + .../environment.yml | 1 + .../download_ncbi_annotation/environment.yml | 1 + .../getaccessions/environment.yml | 1 + .../filter_and_rename_genes/environment.yml | 1 + .../genorm/compute_m_measure/environment.yml | 1 + .../local/genorm/cross_join/environment.yml | 1 + .../genorm/expression_ratio/environment.yml | 1 + .../local/genorm/make_chunks/environment.yml | 1 + .../ratio_standard_variation/environment.yml | 1 + .../local/geo/getaccessions/environment.yml | 1 + .../local/get_candidate_genes/environment.yml | 1 + .../local/gprofiler/idmapping/environment.yml | 1 + modules/local/merge_counts/environment.yml | 1 + modules/local/merge_counts/main.nf | 8 - .../normalisation/compute_cpm/environment.yml | 1 + .../normalisation/compute_tpm/environment.yml | 1 + modules/local/normfinder/environment.yml | 1 + .../normalise_microarray/environment.yml | 0 .../quantile_normalisation/environment.yml | 1 + .../remove_samples_not_valid/environment.yml | 1 + subworkflows/local/merge_data/main.nf | 24 +- .../main.nf | 47 +- tests/default.nf.test.snap | 827 ++++++------------ .../local/aggregate_results/main.nf.test | 17 +- .../compute_base_statistics/main.nf.test | 12 +- .../main.nf.test.snap | 30 +- .../main.nf.test | 36 +- .../filter_and_rename_genes/main.nf.test.snap | 170 ++++ .../gprofiler/idmapping/main.nf.test.snap | 6 +- tests/modules/local/merge_counts/main.nf.test | 22 +- .../local/merge_counts/main.nf.test.snap | 60 +- .../compute_cpm/main.nf.test.snap | 72 +- .../compute_tpm/main.nf.test.snap | 88 +- .../quantile_normalisation/main.nf.test.snap | 66 +- .../local/rename_gene_ids/main.nf.test.snap | 124 --- .../main.nf.test.snap | 48 +- .../idmapping/mapped/no_valid_gene_id.txt | 0 .../idmapping/mapped/valid_gene_ids.txt | 2 + .../idmapping/tsv/valid_gene_ids.txt | 2 + workflows/stableexpression.nf | 3 - 55 files changed, 791 insertions(+), 923 deletions(-) rename modules/local/{ => old}/normalise_microarray/environment.yml (100%) rename tests/modules/local/{rename_gene_ids => filter_and_rename_genes}/main.nf.test (53%) create mode 100644 tests/modules/local/filter_and_rename_genes/main.nf.test.snap delete mode 100644 tests/modules/local/rename_gene_ids/main.nf.test.snap create mode 100644 tests/test_data/idmapping/mapped/no_valid_gene_id.txt create mode 100644 tests/test_data/idmapping/mapped/valid_gene_ids.txt create mode 100644 tests/test_data/idmapping/tsv/valid_gene_ids.txt diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index a1decec8..9aed3942 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -11,7 +11,7 @@ report_section_order: "nf-core-stableexpression-summary": order: -1002 -export_plots: false +export_plots: true run_modules: - custom_content diff --git a/bin/collect_gene_ids.py b/bin/collect_gene_ids.py index ab51089f..d4531444 100755 --- a/bin/collect_gene_ids.py +++ b/bin/collect_gene_ids.py @@ -54,13 +54,13 @@ def main(): counter.update(gene_ids) with open(UNIQUE_GENE_IDS_OUTFILE, "w") as fout: - fout.write("\n".join([str(gene_id) for gene_id in unique_gene_ids])) + fout.write("\n".join([str(gene_id) for gene_id in sorted(unique_gene_ids)])) with open(GENE_ID_OCCURRENCES_OUTFILE, "w") as fout: fout.write( f"{config.ORIGINAL_GENE_ID_COLNAME},{config.GENE_ID_COUNT_COLNAME}\n" ) - for gene_id, count in counter.items(): + for gene_id, count in sorted(counter.items()): fout.write(f"{gene_id},{count}\n") diff --git a/bin/gprofiler_map_ids.py b/bin/gprofiler_map_ids.py index a41fae92..4a559bde 100755 --- a/bin/gprofiler_map_ids.py +++ b/bin/gprofiler_map_ids.py @@ -101,6 +101,7 @@ def main(): 0: config.GENE_ID_COLNAME, } ) + .sort_values(by=config.ORIGINAL_GENE_ID_COLNAME) ) mapping_df.to_csv(MAPPED_GENE_IDS_OUTFILE, index=False, header=True) @@ -111,9 +112,10 @@ def main(): gene_metadata_df = pd.concat(gene_metadata_dfs, ignore_index=True) # dropping duplicates and keeping the first occurence gene_metadata_df.drop_duplicates( - inplace=True, subset=[config.GENE_ID_COLNAME], keep="first" + subset=[config.GENE_ID_COLNAME], keep="first" + ).sort_values(by=config.GENE_ID_COLNAME).to_csv( + METADATA_OUTFILE, index=False, header=True ) - gene_metadata_df.to_csv(METADATA_OUTFILE, index=False, header=True) if __name__ == "__main__": diff --git a/modules/local/aggregate_results/environment.yml b/modules/local/aggregate_results/environment.yml index 4fa78808..f0eaf3dd 100644 --- a/modules/local/aggregate_results/environment.yml +++ b/modules/local/aggregate_results/environment.yml @@ -4,4 +4,5 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.12.8 - conda-forge::polars==1.35.2 diff --git a/modules/local/clean_gene_ids/environment.yml b/modules/local/clean_gene_ids/environment.yml index 4fa78808..f0eaf3dd 100644 --- a/modules/local/clean_gene_ids/environment.yml +++ b/modules/local/clean_gene_ids/environment.yml @@ -4,4 +4,5 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.12.8 - conda-forge::polars==1.35.2 diff --git a/modules/local/collect_gene_ids/environment.yml b/modules/local/collect_gene_ids/environment.yml index c4d7cc14..75afc696 100644 --- a/modules/local/collect_gene_ids/environment.yml +++ b/modules/local/collect_gene_ids/environment.yml @@ -4,4 +4,5 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.14.2 - conda-forge::tqdm==4.67.1 diff --git a/modules/local/collect_gene_ids/main.nf b/modules/local/collect_gene_ids/main.nf index ffc94e68..f7b0bdfe 100644 --- a/modules/local/collect_gene_ids/main.nf +++ b/modules/local/collect_gene_ids/main.nf @@ -4,8 +4,8 @@ process COLLECT_GENE_IDS { 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/80/80143d9f5e0bfe1364e7bf621ca8bb45f707fd48aa1ba3712158fc441d7873b0/data': - 'community.wave.seqera.io/library/tqdm:4.67.1--c1e9fac535191e31' }" + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/70/70c17cde84896904c0620d614cba74ff029f1255db64e66416e63c91b7c959a2/data': + 'community.wave.seqera.io/library/python_tqdm:4e039400f75bdad0' }" input: path count_files, stageAs: "?/*" diff --git a/modules/local/collect_statistics/environment.yml b/modules/local/collect_statistics/environment.yml index 104de0e2..5d27c0af 100644 --- a/modules/local/collect_statistics/environment.yml +++ b/modules/local/collect_statistics/environment.yml @@ -4,4 +4,5 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.13.7 - conda-forge::pandas==2.3.3 diff --git a/modules/local/compute_base_statistics/environment.yml b/modules/local/compute_base_statistics/environment.yml index 4fa78808..f0eaf3dd 100644 --- a/modules/local/compute_base_statistics/environment.yml +++ b/modules/local/compute_base_statistics/environment.yml @@ -4,4 +4,5 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.12.8 - conda-forge::polars==1.35.2 diff --git a/modules/local/compute_base_statistics/main.nf b/modules/local/compute_base_statistics/main.nf index b10c8da4..89c66855 100644 --- a/modules/local/compute_base_statistics/main.nf +++ b/modules/local/compute_base_statistics/main.nf @@ -3,12 +3,6 @@ process COMPUTE_BASE_STATISTICS { tag "${meta.platform}" label 'process_high' - memory { def calc = (meta.dataset_size / 50000).toInteger() - def result = Math.max(1, calc) // Ensure at least 1 MB - def multiplicator = 1 + 0.2 * task.attempt // increase memory usage with each attempt by 20% - return 1.MB * result * multiplicator - } - 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/0f/0f8a5d02e7b31980c887253a9f118da0ef91ead1c7b158caf855199e5c5d5473/data': diff --git a/modules/local/compute_dataset_statistics/environment.yml b/modules/local/compute_dataset_statistics/environment.yml index 4fa78808..f0eaf3dd 100644 --- a/modules/local/compute_dataset_statistics/environment.yml +++ b/modules/local/compute_dataset_statistics/environment.yml @@ -4,4 +4,5 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.12.8 - conda-forge::polars==1.35.2 diff --git a/modules/local/compute_gene_transcript_lengths/environment.yml b/modules/local/compute_gene_transcript_lengths/environment.yml index 104de0e2..5d27c0af 100644 --- a/modules/local/compute_gene_transcript_lengths/environment.yml +++ b/modules/local/compute_gene_transcript_lengths/environment.yml @@ -4,4 +4,5 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.13.7 - conda-forge::pandas==2.3.3 diff --git a/modules/local/compute_stability_scores/environment.yml b/modules/local/compute_stability_scores/environment.yml index 4fa78808..f0eaf3dd 100644 --- a/modules/local/compute_stability_scores/environment.yml +++ b/modules/local/compute_stability_scores/environment.yml @@ -4,4 +4,5 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.12.8 - conda-forge::polars==1.35.2 diff --git a/modules/local/dash_app/app/environment.yml b/modules/local/dash_app/app/environment.yml index 590353ad..36fb5a36 100644 --- a/modules/local/dash_app/app/environment.yml +++ b/modules/local/dash_app/app/environment.yml @@ -4,6 +4,7 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.13.8 - conda-forge::pandas==2.3.3 - conda-forge::polars==1.35.2 - conda-forge::pyarrow==22.0.0 diff --git a/modules/local/detect_rare_genes/environment.yml b/modules/local/detect_rare_genes/environment.yml index 4fa78808..f0eaf3dd 100644 --- a/modules/local/detect_rare_genes/environment.yml +++ b/modules/local/detect_rare_genes/environment.yml @@ -4,4 +4,5 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.12.8 - conda-forge::polars==1.35.2 diff --git a/modules/local/download_ensembl_annotation/environment.yml b/modules/local/download_ensembl_annotation/environment.yml index 7aa02d40..95caa5f9 100644 --- a/modules/local/download_ensembl_annotation/environment.yml +++ b/modules/local/download_ensembl_annotation/environment.yml @@ -4,6 +4,7 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.14.0 - conda-forge::pandas==2.3.3 - conda-forge::requests==2.32.5 - conda-forge::tqdm==4.67.1 diff --git a/modules/local/download_ncbi_annotation/environment.yml b/modules/local/download_ncbi_annotation/environment.yml index 087abd4b..f644045d 100644 --- a/modules/local/download_ncbi_annotation/environment.yml +++ b/modules/local/download_ncbi_annotation/environment.yml @@ -4,5 +4,6 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.13.5 - conda-forge::requests==2.32.5 - conda-forge::tenacity==9.1.2 diff --git a/modules/local/expressionatlas/getaccessions/environment.yml b/modules/local/expressionatlas/getaccessions/environment.yml index 6abe20f0..207b56fc 100644 --- a/modules/local/expressionatlas/getaccessions/environment.yml +++ b/modules/local/expressionatlas/getaccessions/environment.yml @@ -4,6 +4,7 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.13.5 - conda-forge::pandas==2.3.3 - conda-forge::requests==2.32.5 - conda-forge::tenacity==9.1.2 diff --git a/modules/local/filter_and_rename_genes/environment.yml b/modules/local/filter_and_rename_genes/environment.yml index 4fa78808..f0eaf3dd 100644 --- a/modules/local/filter_and_rename_genes/environment.yml +++ b/modules/local/filter_and_rename_genes/environment.yml @@ -4,4 +4,5 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.12.8 - conda-forge::polars==1.35.2 diff --git a/modules/local/genorm/compute_m_measure/environment.yml b/modules/local/genorm/compute_m_measure/environment.yml index 4fa78808..f0eaf3dd 100644 --- a/modules/local/genorm/compute_m_measure/environment.yml +++ b/modules/local/genorm/compute_m_measure/environment.yml @@ -4,4 +4,5 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.12.8 - conda-forge::polars==1.35.2 diff --git a/modules/local/genorm/cross_join/environment.yml b/modules/local/genorm/cross_join/environment.yml index 4fa78808..f0eaf3dd 100644 --- a/modules/local/genorm/cross_join/environment.yml +++ b/modules/local/genorm/cross_join/environment.yml @@ -4,4 +4,5 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.12.8 - conda-forge::polars==1.35.2 diff --git a/modules/local/genorm/expression_ratio/environment.yml b/modules/local/genorm/expression_ratio/environment.yml index 4fa78808..f0eaf3dd 100644 --- a/modules/local/genorm/expression_ratio/environment.yml +++ b/modules/local/genorm/expression_ratio/environment.yml @@ -4,4 +4,5 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.12.8 - conda-forge::polars==1.35.2 diff --git a/modules/local/genorm/make_chunks/environment.yml b/modules/local/genorm/make_chunks/environment.yml index 4fa78808..f0eaf3dd 100644 --- a/modules/local/genorm/make_chunks/environment.yml +++ b/modules/local/genorm/make_chunks/environment.yml @@ -4,4 +4,5 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.12.8 - conda-forge::polars==1.35.2 diff --git a/modules/local/genorm/ratio_standard_variation/environment.yml b/modules/local/genorm/ratio_standard_variation/environment.yml index 4fa78808..f0eaf3dd 100644 --- a/modules/local/genorm/ratio_standard_variation/environment.yml +++ b/modules/local/genorm/ratio_standard_variation/environment.yml @@ -4,4 +4,5 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.12.8 - conda-forge::polars==1.35.2 diff --git a/modules/local/geo/getaccessions/environment.yml b/modules/local/geo/getaccessions/environment.yml index d2475405..91b4f94e 100644 --- a/modules/local/geo/getaccessions/environment.yml +++ b/modules/local/geo/getaccessions/environment.yml @@ -4,6 +4,7 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.13.7 - conda-forge::pandas==2.3.3 - conda-forge::requests==2.32.5 - conda-forge::tenacity==9.1.2 diff --git a/modules/local/get_candidate_genes/environment.yml b/modules/local/get_candidate_genes/environment.yml index 4fa78808..f0eaf3dd 100644 --- a/modules/local/get_candidate_genes/environment.yml +++ b/modules/local/get_candidate_genes/environment.yml @@ -4,4 +4,5 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.12.8 - conda-forge::polars==1.35.2 diff --git a/modules/local/gprofiler/idmapping/environment.yml b/modules/local/gprofiler/idmapping/environment.yml index e8068c38..02344ca8 100644 --- a/modules/local/gprofiler/idmapping/environment.yml +++ b/modules/local/gprofiler/idmapping/environment.yml @@ -4,6 +4,7 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.13.5 - conda-forge::pandas==2.3.3 - conda-forge::requests==2.32.5 - conda-forge::tenacity==9.1.2 diff --git a/modules/local/merge_counts/environment.yml b/modules/local/merge_counts/environment.yml index c55b56d4..fc0aa746 100644 --- a/modules/local/merge_counts/environment.yml +++ b/modules/local/merge_counts/environment.yml @@ -4,5 +4,6 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.14.0 - conda-forge::polars==1.35.2 - conda-forge::tqdm==4.67.1 diff --git a/modules/local/merge_counts/main.nf b/modules/local/merge_counts/main.nf index 9302a67d..94858c45 100644 --- a/modules/local/merge_counts/main.nf +++ b/modules/local/merge_counts/main.nf @@ -3,14 +3,6 @@ process MERGE_COUNTS { tag "${meta.platform}" label "process_high" - maxForks 1 - - memory { def calc = (meta.dataset_size / 50000).toInteger() - def result = Math.max(1, calc) // Ensure at least 1 MB - def multiplicator = 1 + 0.2 * task.attempt // increase memory usage with each attempt by 20% - return 1.MB * result * multiplicator - } - 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/90/90617e987f709570820b8e7752baf9004ba85917111425d4b44b429b27b201ca/data': diff --git a/modules/local/normalisation/compute_cpm/environment.yml b/modules/local/normalisation/compute_cpm/environment.yml index 4fa78808..f0eaf3dd 100644 --- a/modules/local/normalisation/compute_cpm/environment.yml +++ b/modules/local/normalisation/compute_cpm/environment.yml @@ -4,4 +4,5 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.12.8 - conda-forge::polars==1.35.2 diff --git a/modules/local/normalisation/compute_tpm/environment.yml b/modules/local/normalisation/compute_tpm/environment.yml index 4fa78808..f0eaf3dd 100644 --- a/modules/local/normalisation/compute_tpm/environment.yml +++ b/modules/local/normalisation/compute_tpm/environment.yml @@ -4,4 +4,5 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.12.8 - conda-forge::polars==1.35.2 diff --git a/modules/local/normfinder/environment.yml b/modules/local/normfinder/environment.yml index 220cc1ee..8e9b3ad3 100644 --- a/modules/local/normfinder/environment.yml +++ b/modules/local/normfinder/environment.yml @@ -4,6 +4,7 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.13.7 - conda-forge::polars==1.35.2 - conda-forge::tqdm==4.67.1 - conda-forge::numpy==2.3.5 diff --git a/modules/local/normalise_microarray/environment.yml b/modules/local/old/normalise_microarray/environment.yml similarity index 100% rename from modules/local/normalise_microarray/environment.yml rename to modules/local/old/normalise_microarray/environment.yml diff --git a/modules/local/quantile_normalisation/environment.yml b/modules/local/quantile_normalisation/environment.yml index d9e1398e..69fedff0 100644 --- a/modules/local/quantile_normalisation/environment.yml +++ b/modules/local/quantile_normalisation/environment.yml @@ -4,5 +4,6 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.14.2 - conda-forge::polars==1.36.1 - conda-forge::scikit-learn==1.8.0 diff --git a/modules/local/remove_samples_not_valid/environment.yml b/modules/local/remove_samples_not_valid/environment.yml index 4fa78808..f0eaf3dd 100644 --- a/modules/local/remove_samples_not_valid/environment.yml +++ b/modules/local/remove_samples_not_valid/environment.yml @@ -4,4 +4,5 @@ channels: - conda-forge - bioconda dependencies: + - conda-forge::python=3.12.8 - conda-forge::polars==1.35.2 diff --git a/subworkflows/local/merge_data/main.nf b/subworkflows/local/merge_data/main.nf index ca1d2501..5941757e 100644 --- a/subworkflows/local/merge_data/main.nf +++ b/subworkflows/local/merge_data/main.nf @@ -1,8 +1,6 @@ include { MERGE_COUNTS as MERGE_PLATFORM_COUNTS } from '../../../modules/local/merge_counts' include { MERGE_COUNTS as MERGE_ALL_COUNTS } from '../../../modules/local/merge_counts' -include { getWholeDatasetSize } from '../../../subworkflows/local/utils_nfcore_stableexpression_pipeline' - /* ======================================================================================== SUBWORKFLOW TO DOWNLOAD EXPRESSIONATLAS ACCESSIONS AND DATASETS @@ -23,27 +21,19 @@ workflow MERGE_DATA { // MERGE COUNTS FOR EACH PLATFORM SEPARATELY // ----------------------------------------------------------------- - // RNASEQ - ch_normalised_rnaseq_counts = ch_normalised_counts.filter { meta, file -> meta.platform == "rnaseq" } - ch_whole_rnaseq_size = getWholeDatasetSize ( ch_normalised_rnaseq_counts ) - // MICROARRAY + ch_normalised_rnaseq_counts = ch_normalised_counts.filter { meta, file -> meta.platform == "rnaseq" } ch_normalised_microarray_counts = ch_normalised_counts.filter { meta, file -> meta.platform == "microarray" } - ch_whole_microarray_size = getWholeDatasetSize ( ch_normalised_microarray_counts ) ch_collected_rnaseq_counts = ch_normalised_rnaseq_counts .map { meta, file -> file } .collect( sort: true ) - .map { files -> [ files ] } - .combine( ch_whole_rnaseq_size ) - .map { files, size -> [ [ platform: "rnaseq", dataset_size: size ], files ] } + .map { files -> [ [ platform: "rnaseq" ], files ] } ch_collected_microarray_counts = ch_normalised_microarray_counts .map { meta, file -> file } .collect( sort: true ) - .map { files -> [ files ] } - .combine( ch_whole_microarray_size ) - .map { files, size -> [ [ platform: "microarray", dataset_size: size ], files ] } + .map { files -> [ [ platform: "microarray" ], files ] } MERGE_PLATFORM_COUNTS ( ch_collected_rnaseq_counts.concat( ch_collected_microarray_counts ) @@ -55,16 +45,10 @@ workflow MERGE_DATA { // MERGE ALL COUNTS // ----------------------------------------------------------------- - ch_whole_size = ch_whole_rnaseq_size - .mix(ch_whole_microarray_size) - .reduce { rnaseq_size, microarray_size -> rnaseq_size + microarray_size } - ch_collected_merged_counts = ch_platform_counts .map { meta, file -> file } .collect( sort: true ) - .map { files -> [ files ] } - .combine( ch_whole_size ) - .map { files, size -> [ [ platform: "all", dataset_size: size ], files ] } + .map { files -> [ [ platform: "all" ], files ] } MERGE_ALL_COUNTS( ch_collected_merged_counts ) diff --git a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf index 3ec4ffe4..9e238a2e 100644 --- a/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_stableexpression_pipeline/main.nf @@ -377,43 +377,22 @@ def augmentMetadata( ch_files ) { .map { meta, file -> def norm_state = getNthPartFromEnd(file.name, 3) - if ( norm_state == 'raw' ) { - normalised = false - } else if ( norm_state == 'normalised' ) { + def normalised = false + if ( norm_state == 'normalised' ) { normalised = true + } else if ( norm_state == 'raw' ) { + normalised = false } else { error("Invalid normalisation state: ${norm_state}") } - platform = getNthPartFromEnd(file.name, 4) - new_meta = meta + [normalised: normalised, platform: platform] + def platform = getNthPartFromEnd(file.name, 4) + def new_meta = meta + [normalised: normalised, platform: platform] [new_meta, file] } } -/* -======================================================================================== - FUNCTIONS FOR CALCULATING SIZE OF DATA -======================================================================================== -*/ - -def storeDatasetSize( ch_counts, nb_genes_key, nb_samples_key ) { - // adding nb genes and nb samples in the meta map under keys provided as parameters - return ch_counts - .map { meta, count_file -> - def header = count_file.withReader { reader -> reader.readLine() } - def columns = header.contains(',') ? header.split(',') : - header.contains('\t') ? header.split('\t') : - [header] - def content = count_file.splitCsv( header: false, skip: 1 ) - meta[nb_genes_key] = content.size() - meta[nb_samples_key] = columns.size() - 1 // removing index column - [ meta, count_file ] - } -} - - /* ======================================================================================== FUNCTIONS FOR CHECKING NB OF DATASETS @@ -425,6 +404,7 @@ def checkCounts(ch_counts) { ch_counts.count().map { n -> if( n == 0 ) { // display a warning if no datasets are found + def msg_lst = [] if ( !params.fetch_geo_accessions ) { msg_lst = [ "Could not find any readily usable public dataset.", @@ -444,16 +424,3 @@ def checkCounts(ch_counts) { } } } - - -def getWholeDatasetSize( ch_counts ) { - return ch_counts - .filter { meta, file -> - meta.nb_genes > 0 && meta.nb_samples > 0 - } - .map { meta, file -> - meta.nb_genes * meta.nb_samples - } - .reduce { size_1, size_2 -> size_1 + size_2 } - .flatten() -} diff --git a/tests/default.nf.test.snap b/tests/default.nf.test.snap index f9d58955..12c6970b 100644 --- a/tests/default.nf.test.snap +++ b/tests/default.nf.test.snap @@ -7,42 +7,40 @@ "python": "3.12.8" }, "CLEAN_GENE_IDS": { - "pandas": "2.3.3", - "polars": "1.35.2", - "python": "3.14.0" + "polars": "1.17.1", + "python": "3.12.8" }, "COLLECT_GENE_IDS": { - "pandas": "2.3.3", - "python": "3.14.0", + "python": "3.14.2", "tqdm": "4.67.1" }, "COLLECT_STATISTICS": { "pandas": "2.3.3", "python": "3.13.7" }, - "COMPUTE_BASE_STATISTICS": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "COMPUTE_BASE_STATISTICS_FOR_RNASEQ": { + "COMPUTE_DATASET_STATISTICS": { "polars": "1.17.1", "python": "3.12.8" }, - "COMPUTE_DATASET_STATISTICS": { - "pandas": "2.3.3", - "python": "3.13.7" - }, "COMPUTE_GENE_TRANSCRIPT_LENGTHS": { "pandas": "2.3.3", "python": "3.13.7" }, + "COMPUTE_GLOBAL_STATISTICS": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_PLATFORM_STATISTICS": { + "polars": "1.17.1", + "python": "3.12.8" + }, "COMPUTE_STABILITY_SCORES": { "polars": "1.17.1", "python": "3.12.8" }, "COMPUTE_TPM": { - "pandas": "2.3.3", - "python": "3.13.7" + "polars": "1.17.1", + "python": "3.12.8" }, "DASH_APP": { "python": "3.13.8", @@ -55,6 +53,10 @@ "pyarrow": "22.0.0", "scipy": "1.16.3" }, + "DETECT_RARE_GENES": { + "polars": "1.17.1", + "python": "3.12.8" + }, "DOWNLOAD_ENSEMBL_ANNOTATION": { "bs4": "4.14.2", "pandas": "2.3.3", @@ -62,6 +64,10 @@ "requests": "2.32.5", "tqdm": "4.67.1" }, + "FILTER_AND_RENAME_GENES": { + "polars": "1.17.1", + "python": "3.12.8" + }, "GET_CANDIDATE_GENES": { "polars": "1.17.1", "python": "3.12.8" @@ -76,7 +82,7 @@ "python": "3.14.0", "tqdm": "4.67.1" }, - "MERGE_RNASEQ_COUNTS": { + "MERGE_PLATFORM_COUNTS": { "polars": "1.34.0", "python": "3.14.0", "tqdm": "4.67.1" @@ -86,19 +92,13 @@ "python": "3.13.7" }, "QUANTILE_NORMALISATION": { - "pandas": "2.2.3", - "pyarrow": "19.0.0", - "python": "3.12.8", - "scikit-learn": "1.6.1" + "polars": "1.36.1", + "python": "3.14.2", + "scikit-learn": "1.8.0" }, "REMOVE_SAMPLES_NOT_VALID": { - "pandas": "2.3.3", - "python": "3.13.7" - }, - "RENAME_GENE_IDS": { - "pandas": "2.3.3", - "polars": "1.35.2", - "python": "3.14.0" + "polars": "1.17.1", + "python": "3.12.8" }, "Workflow": { "nf-core/stableexpression": "v1.0dev" @@ -142,16 +142,13 @@ "errors", "idmapping", "idmapping/collected_gene_ids", - "idmapping/collected_gene_ids/all_gene_ids.txt", + "idmapping/collected_gene_ids/gene_id_occurrences.csv", + "idmapping/collected_gene_ids/unique_gene_ids.txt", "idmapping/global_gene_id_mapping.csv", "idmapping/global_gene_metadata.csv", "idmapping/gprofiler", "idmapping/gprofiler/gene_metadata.csv", "idmapping/gprofiler/mapped_gene_ids.csv", - "idmapping/original_gene_ids.txt", - "idmapping/renamed", - "idmapping/renamed/SRP254919.salmon.merged.gene_counts.top1000cov.assay.cleaned.renamed.csv", - "idmapping/renamed/warning_reason.txt", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", "merged_datasets", @@ -172,6 +169,7 @@ "multiqc/multiqc_data/multiqc_skewness.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_data/multiqc_total_gene_id_occurrence_quantiles.txt", "multiqc/multiqc_plots", "multiqc/multiqc_plots/pdf", "multiqc/multiqc_plots/pdf/expression_distributions_most_stable_genes.pdf", @@ -182,6 +180,7 @@ "multiqc/multiqc_plots/pdf/ratio_zeros.pdf", "multiqc/multiqc_plots/pdf/renaming_warning_reasons.pdf", "multiqc/multiqc_plots/pdf/skewness.pdf", + "multiqc/multiqc_plots/pdf/total_gene_id_occurrence_quantiles.pdf", "multiqc/multiqc_plots/png", "multiqc/multiqc_plots/png/expression_distributions_most_stable_genes.png", "multiqc/multiqc_plots/png/gene_statistics.png", @@ -191,6 +190,7 @@ "multiqc/multiqc_plots/png/ratio_zeros.png", "multiqc/multiqc_plots/png/renaming_warning_reasons.png", "multiqc/multiqc_plots/png/skewness.png", + "multiqc/multiqc_plots/png/total_gene_id_occurrence_quantiles.png", "multiqc/multiqc_plots/svg", "multiqc/multiqc_plots/svg/expression_distributions_most_stable_genes.svg", "multiqc/multiqc_plots/svg/gene_statistics.svg", @@ -200,6 +200,7 @@ "multiqc/multiqc_plots/svg/ratio_zeros.svg", "multiqc/multiqc_plots/svg/renaming_warning_reasons.svg", "multiqc/multiqc_plots/svg/skewness.svg", + "multiqc/multiqc_plots/svg/total_gene_id_occurrence_quantiles.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", "normalised", @@ -207,7 +208,7 @@ "normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay/quantile_normalised", "normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay/quantile_normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay.cleaned.renamed.filtered.tpm.quant_norm.parquet", "normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay/tpm", - "normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay/tpm/SRP254919.salmon.merged.gene_counts.top1000cov.assay.cleaned.renamed.filtered.tpm.csv", + "normalised/SRP254919.salmon.merged.gene_counts.top1000cov.assay/tpm/SRP254919.salmon.merged.gene_counts.top1000cov.assay.cleaned.renamed.filtered.tpm.parquet", "pipeline_info", "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", "statistics", @@ -218,28 +219,25 @@ "warnings/renaming_warning_reasons.tsv" ], [ - "all_genes_summary.csv:md5,54b96cbfb5e464ec086c7e1308701e02", - "most_stable_genes_summary.csv:md5,54b96cbfb5e464ec086c7e1308701e02", - "most_stable_genes_transposed_counts_filtered.csv:md5,af21e36c540965846b73245678b74f36", + "all_genes_summary.csv:md5,09c1c8807fbb69ae7baf5c4a7772c8d2", + "most_stable_genes_summary.csv:md5,09c1c8807fbb69ae7baf5c4a7772c8d2", + "most_stable_genes_transposed_counts_filtered.csv:md5,68fa221be589ee1a5970ae1461a743ac", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_genes_summary.csv:md5,54b96cbfb5e464ec086c7e1308701e02", + "all_genes_summary.csv:md5,09c1c8807fbb69ae7baf5c4a7772c8d2", "whole_design.csv:md5,f29515bc2c783e593fb9028127342593", - "environment.yml:md5,4d32e46adf0ff32a3c65a24b68483fa7", - "all_gene_ids.txt:md5,6b2ece983fd9da133e719914216852b0", + "environment.yml:md5,f9b192ef98a67f2084ad2fed6da01bc1", + "gene_id_occurrences.csv:md5,72c8f7ffe8413be06419c10ae66c35e5", + "unique_gene_ids.txt:md5,6b2ece983fd9da133e719914216852b0", "global_gene_id_mapping.csv:md5,78934d2ac5fe7d863f114c5703f57a06", "global_gene_metadata.csv:md5,bd76860b422e45eca7cd583212a977d2", "gene_metadata.csv:md5,bd76860b422e45eca7cd583212a977d2", "mapped_gene_ids.csv:md5,78934d2ac5fe7d863f114c5703f57a06", - "original_gene_ids.txt:md5,0c6b71845bdf783b426294fa7993da94", - "SRP254919.salmon.merged.gene_counts.top1000cov.assay.cleaned.renamed.csv:md5,c855d1848a74842378c812556ddd2e1f", - "warning_reason.txt:md5,b13d82afc1a3752e78dd796fb1c53d52", "whole_gene_id_mapping.csv:md5,78934d2ac5fe7d863f114c5703f57a06", "whole_gene_metadata.csv:md5,bd76860b422e45eca7cd583212a977d2", "whole_design.csv:md5,f29515bc2c783e593fb9028127342593", - "SRP254919.salmon.merged.gene_counts.top1000cov.assay.cleaned.renamed.filtered.tpm.csv:md5,5b517b410a643c4e6fbffb55dc1cd1a7", - "id_mapping_stats.csv:md5,14cdb53685228e5e5393cf9404856b41", - "ratio_zeros.csv:md5,5a667d505cbd2cc7057ee47b70536c2e", - "skewness.csv:md5,582683980eadf84d32853a21f9dce230", + "id_mapping_stats.csv:md5,b47d6ebd34e3fb11a40665b0a38db3da", + "ratio_zeros.csv:md5,2272ebcf58ac8bb283d238f87d508b96", + "skewness.csv:md5,2ef6f5a2aa5834110fda06e705adcbf8", "renaming_warning_reasons.tsv:md5,0a11a59b5b547a39ab7a0e4dac622173" ] ], @@ -247,7 +245,7 @@ "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2026-01-03T11:26:35.453920775" + "timestamp": "2026-01-13T13:10:38.553975658" }, "-profile test_eatlas_only_with_keywords": { "content": [ @@ -257,42 +255,40 @@ "python": "3.12.8" }, "CLEAN_GENE_IDS": { - "pandas": "2.3.3", - "polars": "1.35.2", - "python": "3.14.0" + "polars": "1.17.1", + "python": "3.12.8" }, "COLLECT_GENE_IDS": { - "pandas": "2.3.3", - "python": "3.14.0", + "python": "3.14.2", "tqdm": "4.67.1" }, "COLLECT_STATISTICS": { "pandas": "2.3.3", "python": "3.13.7" }, - "COMPUTE_BASE_STATISTICS": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "COMPUTE_BASE_STATISTICS_FOR_RNASEQ": { + "COMPUTE_DATASET_STATISTICS": { "polars": "1.17.1", "python": "3.12.8" }, - "COMPUTE_DATASET_STATISTICS": { - "pandas": "2.3.3", - "python": "3.13.7" - }, "COMPUTE_GENE_TRANSCRIPT_LENGTHS": { "pandas": "2.3.3", "python": "3.13.7" }, + "COMPUTE_GLOBAL_STATISTICS": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_PLATFORM_STATISTICS": { + "polars": "1.17.1", + "python": "3.12.8" + }, "COMPUTE_STABILITY_SCORES": { "polars": "1.17.1", "python": "3.12.8" }, "COMPUTE_TPM": { - "pandas": "2.3.3", - "python": "3.13.7" + "polars": "1.17.1", + "python": "3.12.8" }, "DASH_APP": { "python": "3.13.8", @@ -305,6 +301,10 @@ "pyarrow": "22.0.0", "scipy": "1.16.3" }, + "DETECT_RARE_GENES": { + "polars": "1.17.1", + "python": "3.12.8" + }, "DOWNLOAD_ENSEMBL_ANNOTATION": { "bs4": "4.14.2", "pandas": "2.3.3", @@ -321,6 +321,10 @@ "pyyaml": "6.0.2", "requests": "2.32.4" }, + "FILTER_AND_RENAME_GENES": { + "polars": "1.17.1", + "python": "3.12.8" + }, "GET_CANDIDATE_GENES": { "polars": "1.17.1", "python": "3.12.8" @@ -335,7 +339,7 @@ "python": "3.14.0", "tqdm": "4.67.1" }, - "MERGE_RNASEQ_COUNTS": { + "MERGE_PLATFORM_COUNTS": { "polars": "1.34.0", "python": "3.14.0", "tqdm": "4.67.1" @@ -345,15 +349,13 @@ "python": "3.13.7" }, "QUANTILE_NORMALISATION": { - "pandas": "2.2.3", - "pyarrow": "19.0.0", - "python": "3.12.8", - "scikit-learn": "1.6.1" + "polars": "1.36.1", + "python": "3.14.2", + "scikit-learn": "1.8.0" }, - "RENAME_GENE_IDS": { - "pandas": "2.3.3", - "polars": "1.35.2", - "python": "3.14.0" + "REMOVE_SAMPLES_NOT_VALID": { + "polars": "1.17.1", + "python": "3.12.8" }, "Workflow": { "nf-core/stableexpression": "v1.0dev" @@ -397,15 +399,13 @@ "errors", "idmapping", "idmapping/collected_gene_ids", - "idmapping/collected_gene_ids/all_gene_ids.txt", + "idmapping/collected_gene_ids/gene_id_occurrences.csv", + "idmapping/collected_gene_ids/unique_gene_ids.txt", "idmapping/global_gene_id_mapping.csv", "idmapping/global_gene_metadata.csv", "idmapping/gprofiler", "idmapping/gprofiler/gene_metadata.csv", "idmapping/gprofiler/mapped_gene_ids.csv", - "idmapping/original_gene_ids.txt", - "idmapping/renamed", - "idmapping/renamed/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", "merged_datasets", @@ -427,6 +427,7 @@ "multiqc/multiqc_data/multiqc_skewness.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_data/multiqc_total_gene_id_occurrence_quantiles.txt", "multiqc/multiqc_plots", "multiqc/multiqc_plots/pdf", "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", @@ -438,6 +439,7 @@ "multiqc/multiqc_plots/pdf/ranked_most_stable_genes_summary.pdf", "multiqc/multiqc_plots/pdf/ratio_zeros.pdf", "multiqc/multiqc_plots/pdf/skewness.pdf", + "multiqc/multiqc_plots/pdf/total_gene_id_occurrence_quantiles.pdf", "multiqc/multiqc_plots/png", "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", @@ -448,6 +450,7 @@ "multiqc/multiqc_plots/png/ranked_most_stable_genes_summary.png", "multiqc/multiqc_plots/png/ratio_zeros.png", "multiqc/multiqc_plots/png/skewness.png", + "multiqc/multiqc_plots/png/total_gene_id_occurrence_quantiles.png", "multiqc/multiqc_plots/svg", "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", @@ -458,14 +461,15 @@ "multiqc/multiqc_plots/svg/ranked_most_stable_genes_summary.svg", "multiqc/multiqc_plots/svg/ratio_zeros.svg", "multiqc/multiqc_plots/svg/skewness.svg", + "multiqc/multiqc_plots/svg/total_gene_id_occurrence_quantiles.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", "normalised", "normalised/E_MTAB_8187_rnaseq", "normalised/E_MTAB_8187_rnaseq/quantile_normalised", - "normalised/E_MTAB_8187_rnaseq/quantile_normalised/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/E_MTAB_8187_rnaseq/quantile_normalised/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.quant_norm.parquet", "normalised/E_MTAB_8187_rnaseq/tpm", - "normalised/E_MTAB_8187_rnaseq/tpm/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", + "normalised/E_MTAB_8187_rnaseq/tpm/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.parquet", "pipeline_info", "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", "public_data", @@ -484,37 +488,35 @@ "warnings" ], [ - "all_genes_summary.csv:md5,32be3aeed5cd2715847ccc2fd45c09a0", - "most_stable_genes_summary.csv:md5,2e34161e2beb2f1e0921dbf6c0ce55ba", - "most_stable_genes_transposed_counts_filtered.csv:md5,5083c04020d1c504c86689e14f731ef7", + "all_genes_summary.csv:md5,829fd6221705fc1588a11bfdb1a37210", + "most_stable_genes_summary.csv:md5,73cbb4d6462e01ed7778f076726613fd", + "most_stable_genes_transposed_counts_filtered.csv:md5,4f50fafaa96ffd7a7b0d98d9c8d6beb4", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_genes_summary.csv:md5,32be3aeed5cd2715847ccc2fd45c09a0", + "all_genes_summary.csv:md5,829fd6221705fc1588a11bfdb1a37210", "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", - "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", - "all_gene_ids.txt:md5,ba79f5609df755c0f75de41357319c84", + "environment.yml:md5,f9b192ef98a67f2084ad2fed6da01bc1", + "gene_id_occurrences.csv:md5,b3ee7b1c575f83d247c5bce88382fb2b", + "unique_gene_ids.txt:md5,ba79f5609df755c0f75de41357319c84", "global_gene_id_mapping.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", "global_gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", "gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", "mapped_gene_ids.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", - "original_gene_ids.txt:md5,60a0406c1d56424cfc394c438de50c99", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,ed1600e42df05fc885a16a66b77324a1", "whole_gene_id_mapping.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", "whole_gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,b407990bb10106de943f9e93cda6323e", "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", - "id_mapping_stats.csv:md5,6189d2a346cce55f182e00769e3fea5f", - "ratio_zeros.csv:md5,d3b518709a097d9e41a05142b524f03c", - "skewness.csv:md5,b38aabc94d60d93b979c3cef3a922299" + "id_mapping_stats.csv:md5,17ccaa8e70c67c7d0de4ec3c630c2e5b", + "ratio_zeros.csv:md5,32889cf6de2af6413c42b8810a99a2df", + "skewness.csv:md5,178449bbd2361aa1e804e3f18e092ef1" ] ], "meta": { "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-23T12:25:51.249379198" + "timestamp": "2026-01-13T13:14:45.841673917" }, "-profile test_included_and_excluded_accessions": { "content": [ @@ -524,42 +526,40 @@ "python": "3.12.8" }, "CLEAN_GENE_IDS": { - "pandas": "2.3.3", - "polars": "1.35.2", - "python": "3.14.0" + "polars": "1.17.1", + "python": "3.12.8" }, "COLLECT_GENE_IDS": { - "pandas": "2.3.3", - "python": "3.14.0", + "python": "3.14.2", "tqdm": "4.67.1" }, "COLLECT_STATISTICS": { "pandas": "2.3.3", "python": "3.13.7" }, - "COMPUTE_BASE_STATISTICS": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "COMPUTE_BASE_STATISTICS_FOR_RNASEQ": { + "COMPUTE_DATASET_STATISTICS": { "polars": "1.17.1", "python": "3.12.8" }, - "COMPUTE_DATASET_STATISTICS": { - "pandas": "2.3.3", - "python": "3.13.7" - }, "COMPUTE_GENE_TRANSCRIPT_LENGTHS": { "pandas": "2.3.3", "python": "3.13.7" }, + "COMPUTE_GLOBAL_STATISTICS": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_PLATFORM_STATISTICS": { + "polars": "1.17.1", + "python": "3.12.8" + }, "COMPUTE_STABILITY_SCORES": { "polars": "1.17.1", "python": "3.12.8" }, "COMPUTE_TPM": { - "pandas": "2.3.3", - "python": "3.13.7" + "polars": "1.17.1", + "python": "3.12.8" }, "DASH_APP": { "python": "3.13.8", @@ -572,6 +572,10 @@ "pyarrow": "22.0.0", "scipy": "1.16.3" }, + "DETECT_RARE_GENES": { + "polars": "1.17.1", + "python": "3.12.8" + }, "DOWNLOAD_ENSEMBL_ANNOTATION": { "bs4": "4.14.2", "pandas": "2.3.3", @@ -588,6 +592,10 @@ "pyyaml": "6.0.2", "requests": "2.32.4" }, + "FILTER_AND_RENAME_GENES": { + "polars": "1.17.1", + "python": "3.12.8" + }, "GET_CANDIDATE_GENES": { "polars": "1.17.1", "python": "3.12.8" @@ -602,7 +610,7 @@ "python": "3.14.0", "tqdm": "4.67.1" }, - "MERGE_RNASEQ_COUNTS": { + "MERGE_PLATFORM_COUNTS": { "polars": "1.34.0", "python": "3.14.0", "tqdm": "4.67.1" @@ -612,15 +620,13 @@ "python": "3.13.7" }, "QUANTILE_NORMALISATION": { - "pandas": "2.2.3", - "pyarrow": "19.0.0", - "python": "3.12.8", - "scikit-learn": "1.6.1" + "polars": "1.36.1", + "python": "3.14.2", + "scikit-learn": "1.8.0" }, - "RENAME_GENE_IDS": { - "pandas": "2.3.3", - "polars": "1.35.2", - "python": "3.14.0" + "REMOVE_SAMPLES_NOT_VALID": { + "polars": "1.17.1", + "python": "3.12.8" }, "Workflow": { "nf-core/stableexpression": "v1.0dev" @@ -666,21 +672,13 @@ "errors/renaming_failure_reasons.tsv", "idmapping", "idmapping/collected_gene_ids", - "idmapping/collected_gene_ids/all_gene_ids.txt", + "idmapping/collected_gene_ids/gene_id_occurrences.csv", + "idmapping/collected_gene_ids/unique_gene_ids.txt", "idmapping/global_gene_id_mapping.csv", "idmapping/global_gene_metadata.csv", "idmapping/gprofiler", "idmapping/gprofiler/gene_metadata.csv", "idmapping/gprofiler/mapped_gene_ids.csv", - "idmapping/original_gene_ids.txt", - "idmapping/renamed", - "idmapping/renamed/E_GEOD_61690_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", - "idmapping/renamed/E_GEOD_77826_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", - "idmapping/renamed/E_MTAB_5038_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", - "idmapping/renamed/E_MTAB_5215_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", - "idmapping/renamed/E_MTAB_552_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", - "idmapping/renamed/E_MTAB_7711_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", - "idmapping/renamed/failure_reason.txt", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", "merged_datasets", @@ -704,6 +702,7 @@ "multiqc/multiqc_data/multiqc_skewness.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_data/multiqc_total_gene_id_occurrence_quantiles.txt", "multiqc/multiqc_plots", "multiqc/multiqc_plots/pdf", "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", @@ -717,6 +716,7 @@ "multiqc/multiqc_plots/pdf/ratio_zeros.pdf", "multiqc/multiqc_plots/pdf/renaming_failure_reasons.pdf", "multiqc/multiqc_plots/pdf/skewness.pdf", + "multiqc/multiqc_plots/pdf/total_gene_id_occurrence_quantiles.pdf", "multiqc/multiqc_plots/png", "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", "multiqc/multiqc_plots/png/eatlas_failure_reasons.png", @@ -729,6 +729,7 @@ "multiqc/multiqc_plots/png/ratio_zeros.png", "multiqc/multiqc_plots/png/renaming_failure_reasons.png", "multiqc/multiqc_plots/png/skewness.png", + "multiqc/multiqc_plots/png/total_gene_id_occurrence_quantiles.png", "multiqc/multiqc_plots/svg", "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", "multiqc/multiqc_plots/svg/eatlas_failure_reasons.svg", @@ -741,39 +742,40 @@ "multiqc/multiqc_plots/svg/ratio_zeros.svg", "multiqc/multiqc_plots/svg/renaming_failure_reasons.svg", "multiqc/multiqc_plots/svg/skewness.svg", + "multiqc/multiqc_plots/svg/total_gene_id_occurrence_quantiles.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", "normalised", "normalised/E_GEOD_61690_rnaseq", "normalised/E_GEOD_61690_rnaseq/quantile_normalised", - "normalised/E_GEOD_61690_rnaseq/quantile_normalised/E_GEOD_61690_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/E_GEOD_61690_rnaseq/quantile_normalised/E_GEOD_61690_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.quant_norm.parquet", "normalised/E_GEOD_61690_rnaseq/tpm", - "normalised/E_GEOD_61690_rnaseq/tpm/E_GEOD_61690_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", + "normalised/E_GEOD_61690_rnaseq/tpm/E_GEOD_61690_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.parquet", "normalised/E_GEOD_77826_rnaseq", "normalised/E_GEOD_77826_rnaseq/quantile_normalised", - "normalised/E_GEOD_77826_rnaseq/quantile_normalised/E_GEOD_77826_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/E_GEOD_77826_rnaseq/quantile_normalised/E_GEOD_77826_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.quant_norm.parquet", "normalised/E_GEOD_77826_rnaseq/tpm", - "normalised/E_GEOD_77826_rnaseq/tpm/E_GEOD_77826_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", + "normalised/E_GEOD_77826_rnaseq/tpm/E_GEOD_77826_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.parquet", "normalised/E_MTAB_5038_rnaseq", "normalised/E_MTAB_5038_rnaseq/quantile_normalised", - "normalised/E_MTAB_5038_rnaseq/quantile_normalised/E_MTAB_5038_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/E_MTAB_5038_rnaseq/quantile_normalised/E_MTAB_5038_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.quant_norm.parquet", "normalised/E_MTAB_5038_rnaseq/tpm", - "normalised/E_MTAB_5038_rnaseq/tpm/E_MTAB_5038_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", + "normalised/E_MTAB_5038_rnaseq/tpm/E_MTAB_5038_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.parquet", "normalised/E_MTAB_5215_rnaseq", "normalised/E_MTAB_5215_rnaseq/quantile_normalised", - "normalised/E_MTAB_5215_rnaseq/quantile_normalised/E_MTAB_5215_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/E_MTAB_5215_rnaseq/quantile_normalised/E_MTAB_5215_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.quant_norm.parquet", "normalised/E_MTAB_5215_rnaseq/tpm", - "normalised/E_MTAB_5215_rnaseq/tpm/E_MTAB_5215_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", + "normalised/E_MTAB_5215_rnaseq/tpm/E_MTAB_5215_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.parquet", "normalised/E_MTAB_552_rnaseq", "normalised/E_MTAB_552_rnaseq/quantile_normalised", - "normalised/E_MTAB_552_rnaseq/quantile_normalised/E_MTAB_552_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/E_MTAB_552_rnaseq/quantile_normalised/E_MTAB_552_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.quant_norm.parquet", "normalised/E_MTAB_552_rnaseq/tpm", - "normalised/E_MTAB_552_rnaseq/tpm/E_MTAB_552_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", + "normalised/E_MTAB_552_rnaseq/tpm/E_MTAB_552_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.parquet", "normalised/E_MTAB_7711_rnaseq", "normalised/E_MTAB_7711_rnaseq/quantile_normalised", - "normalised/E_MTAB_7711_rnaseq/quantile_normalised/E_MTAB_7711_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/E_MTAB_7711_rnaseq/quantile_normalised/E_MTAB_7711_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.quant_norm.parquet", "normalised/E_MTAB_7711_rnaseq/tpm", - "normalised/E_MTAB_7711_rnaseq/tpm/E_MTAB_7711_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", + "normalised/E_MTAB_7711_rnaseq/tpm/E_MTAB_7711_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.parquet", "pipeline_info", "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", "public_data", @@ -805,37 +807,24 @@ "warnings" ], [ - "all_genes_summary.csv:md5,22e424fe9e669c8c38997f1f44fc43ac", - "most_stable_genes_summary.csv:md5,049a1fae30bafa22cd91fa906bb33164", - "most_stable_genes_transposed_counts_filtered.csv:md5,7f70c275cedb0ec9ca9775b14c051105", + "all_genes_summary.csv:md5,b8661916743026427c8df986722c38ea", + "most_stable_genes_summary.csv:md5,e281330972fdc9cce06be4e079af8913", + "most_stable_genes_transposed_counts_filtered.csv:md5,be29a5a60aa6785bcfe06d93cef0cdf8", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_genes_summary.csv:md5,22e424fe9e669c8c38997f1f44fc43ac", + "all_genes_summary.csv:md5,b8661916743026427c8df986722c38ea", "whole_design.csv:md5,cc24405dce8d22b93b9999a2287113ef", - "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", + "environment.yml:md5,f9b192ef98a67f2084ad2fed6da01bc1", "eatlas_failure_reasons.csv:md5,2a8cd0ed795e82647d19c484a79acde6", - "renaming_failure_reasons.tsv:md5,3d83b7001eb0554a7e988f770f06d2b1", - "all_gene_ids.txt:md5,e9681582a09fe58f5258977db1d9da3f", + "renaming_failure_reasons.tsv:md5,af783264770a861c263480141fdd8bf6", + "gene_id_occurrences.csv:md5,5f07f7504156cf5dd6db26f230eb73a2", + "unique_gene_ids.txt:md5,e9681582a09fe58f5258977db1d9da3f", "global_gene_id_mapping.csv:md5,a86823539deb80c0aa44378d3078969d", "global_gene_metadata.csv:md5,e33e0ed63a3dec26bc95fe422f02844c", "gene_metadata.csv:md5,e33e0ed63a3dec26bc95fe422f02844c", "mapped_gene_ids.csv:md5,a86823539deb80c0aa44378d3078969d", - "original_gene_ids.txt:md5,ce844044e326975477db11703390d406", - "E_GEOD_61690_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,dd3367748f4d9b8f92daf0d3dd2fb141", - "E_GEOD_77826_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,9b143e768316f992efe1762359bcb3a9", - "E_MTAB_5038_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,e5e5bf37be6c1689b9be6579d18e7eba", - "E_MTAB_5215_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,e4b3b8b84f5a2ea80d9efda6a8e5a271", - "E_MTAB_552_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,b243dfc62677733049d4c579f265d016", - "E_MTAB_7711_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,2e4aa7a16e060ee5dd4092d8419913cb", - "failure_reason.txt:md5,32e95a7ef5c9e4d026676aadcba8e8b8", "whole_gene_id_mapping.csv:md5,a86823539deb80c0aa44378d3078969d", "whole_gene_metadata.csv:md5,e33e0ed63a3dec26bc95fe422f02844c", "whole_design.csv:md5,cc24405dce8d22b93b9999a2287113ef", - "E_GEOD_61690_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,b1f6d3392501f1670c0afed437c9d6c2", - "E_GEOD_77826_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,b736359148994197d258de62585fa6ff", - "E_MTAB_5038_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,856f32fa3c5a3a92578474de586e08e0", - "E_MTAB_5215_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,c235eed546c50a6b335e45bd238de940", - "E_MTAB_552_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,256158add9a0ee0cfcc800104dcaeeae", - "E_MTAB_7711_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,8731c38eeb16bbbb6fb87f5a80665efd", "accessions.txt:md5,e38a0aaf5191ba5f94cb7a96b8d30aa7", "E_GEOD_61690_rnaseq.design.csv:md5,ba807caf74e0b55f5c3fe23810a89560", "E_GEOD_61690_rnaseq.rnaseq.raw.counts.csv:md5,2e3f1a125b3d41d622e2d24447620eb3", @@ -852,16 +841,16 @@ "E_MTAB_7711_rnaseq.design.csv:md5,3e7748b54a0c25c008d9bd2ddbf1bf00", "E_MTAB_7711_rnaseq.rnaseq.raw.counts.csv:md5,3c02cf432c29d3751c978439539df388", "failure_reason.txt:md5,bf97c58555bcb575f0e36df513e1e4c4", - "id_mapping_stats.csv:md5,61a42a2fe5ca0ace6ced97dfa9082e97", - "ratio_zeros.csv:md5,febc5ccc4635c814492b2234cbb167f1", - "skewness.csv:md5,0b7016ec048e578addf7bb669f405b67" + "id_mapping_stats.csv:md5,ca5e05936cbc8a1e8ea7c942752c883a", + "ratio_zeros.csv:md5,b02e3ef082c377aa69dafb8dc894f754", + "skewness.csv:md5,b7bf553b6c85f5d09612fe2630c06aa9" ] ], "meta": { "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-23T12:32:33.899496741" + "timestamp": "2026-01-13T13:18:59.960782988" }, "-profile test_skip_id_mapping": { "content": [ @@ -896,12 +885,12 @@ "normalised", "normalised/microarray.normalised", "normalised/microarray.normalised/quantile_normalised", - "normalised/microarray.normalised/quantile_normalised/microarray.normalised.quant_norm.parquet", + "normalised/microarray.normalised/quantile_normalised/microarray.normalised.filtered.quant_norm.parquet", "normalised/rnaseq.raw", "normalised/rnaseq.raw/quantile_normalised", - "normalised/rnaseq.raw/quantile_normalised/rnaseq.raw.tpm.quant_norm.parquet", + "normalised/rnaseq.raw/quantile_normalised/rnaseq.raw.filtered.tpm.quant_norm.parquet", "normalised/rnaseq.raw/tpm", - "normalised/rnaseq.raw/tpm/rnaseq.raw.tpm.csv", + "normalised/rnaseq.raw/tpm/rnaseq.raw.filtered.tpm.parquet", "pipeline_info", "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", "statistics", @@ -911,16 +900,15 @@ ], [ "whole_design.csv:md5,70d6c2673e619ca52d2774fb3e368382", - "rnaseq.raw.tpm.csv:md5,3938c48a7d3e5b33de0d16db7e786c78", - "ratio_zeros.csv:md5,d206e45c16e6bd13de75ea6d20bbd30d", - "skewness.csv:md5,9917b39dfe5ee6e680fa1783f8a096c4" + "ratio_zeros.csv:md5,3d3fca62a7d1067f0ae0980c8ff570b9", + "skewness.csv:md5,3380ed4915bdbb1be1e5354c6cd99e8c" ] ], "meta": { "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-23T12:26:45.717138449" + "timestamp": "2026-01-13T13:15:27.393976699" }, "-profile test": { "content": [ @@ -963,16 +951,13 @@ "geo", "idmapping", "idmapping/collected_gene_ids", - "idmapping/collected_gene_ids/all_gene_ids.txt", + "idmapping/collected_gene_ids/gene_id_occurrences.csv", + "idmapping/collected_gene_ids/unique_gene_ids.txt", "idmapping/global_gene_id_mapping.csv", "idmapping/global_gene_metadata.csv", "idmapping/gprofiler", "idmapping/gprofiler/gene_metadata.csv", "idmapping/gprofiler/mapped_gene_ids.csv", - "idmapping/original_gene_ids.txt", - "idmapping/renamed", - "idmapping/renamed/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", - "idmapping/renamed/beta_vulgaris.rnaseq.raw.counts.cleaned.renamed.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", "merged_datasets", @@ -998,6 +983,7 @@ "multiqc/multiqc_data/multiqc_skewness.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_data/multiqc_total_gene_id_occurrence_quantiles.txt", "multiqc/multiqc_plots", "multiqc/multiqc_plots/pdf", "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", @@ -1013,6 +999,7 @@ "multiqc/multiqc_plots/pdf/ranked_most_stable_genes_summary.pdf", "multiqc/multiqc_plots/pdf/ratio_zeros.pdf", "multiqc/multiqc_plots/pdf/skewness.pdf", + "multiqc/multiqc_plots/pdf/total_gene_id_occurrence_quantiles.pdf", "multiqc/multiqc_plots/png", "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", @@ -1027,6 +1014,7 @@ "multiqc/multiqc_plots/png/ranked_most_stable_genes_summary.png", "multiqc/multiqc_plots/png/ratio_zeros.png", "multiqc/multiqc_plots/png/skewness.png", + "multiqc/multiqc_plots/png/total_gene_id_occurrence_quantiles.png", "multiqc/multiqc_plots/svg", "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", @@ -1041,6 +1029,7 @@ "multiqc/multiqc_plots/svg/ranked_most_stable_genes_summary.svg", "multiqc/multiqc_plots/svg/ratio_zeros.svg", "multiqc/multiqc_plots/svg/skewness.svg", + "multiqc/multiqc_plots/svg/total_gene_id_occurrence_quantiles.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", "normalised", @@ -1048,12 +1037,12 @@ "normalised/E_MTAB_8187_rnaseq/quantile_normalised", "normalised/E_MTAB_8187_rnaseq/quantile_normalised/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.quant_norm.parquet", "normalised/E_MTAB_8187_rnaseq/tpm", - "normalised/E_MTAB_8187_rnaseq/tpm/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.csv", + "normalised/E_MTAB_8187_rnaseq/tpm/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.parquet", "normalised/beta_vulgaris.rnaseq.raw.counts", "normalised/beta_vulgaris.rnaseq.raw.counts/quantile_normalised", "normalised/beta_vulgaris.rnaseq.raw.counts/quantile_normalised/beta_vulgaris.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.quant_norm.parquet", "normalised/beta_vulgaris.rnaseq.raw.counts/tpm", - "normalised/beta_vulgaris.rnaseq.raw.counts/tpm/beta_vulgaris.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.csv", + "normalised/beta_vulgaris.rnaseq.raw.counts/tpm/beta_vulgaris.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.parquet", "pipeline_info", "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", "public_data", @@ -1081,34 +1070,30 @@ "warnings/geo_warning_reasons.csv" ], [ - "all_genes_summary.csv:md5,ec52af6957845539143919c0e6eec89c", - "most_stable_genes_summary.csv:md5,81d590008d5582658d307bf8c101e60a", - "most_stable_genes_transposed_counts_filtered.csv:md5,9b52b81241cb4f57be781e29afe52cdc", + "all_genes_summary.csv:md5,bea9d7cee88136a4a64812558d3b3119", + "most_stable_genes_summary.csv:md5,9d5b15d996ab2d9064174bfdfeb8d256", + "most_stable_genes_transposed_counts_filtered.csv:md5,70412588c290cac3cd85b189cdee2db6", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_genes_summary.csv:md5,ec52af6957845539143919c0e6eec89c", + "all_genes_summary.csv:md5,bea9d7cee88136a4a64812558d3b3119", "whole_design.csv:md5,3c1e14c9bd7ad250326b070a0dd4d81f", - "environment.yml:md5,4d32e46adf0ff32a3c65a24b68483fa7", - "all_gene_ids.txt:md5,ba79f5609df755c0f75de41357319c84", + "environment.yml:md5,f9b192ef98a67f2084ad2fed6da01bc1", + "gene_id_occurrences.csv:md5,fab6c66d7793c245f67b0cd6d5053cdd", + "unique_gene_ids.txt:md5,ba79f5609df755c0f75de41357319c84", "global_gene_id_mapping.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", "global_gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", "gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", "mapped_gene_ids.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", - "original_gene_ids.txt:md5,60a0406c1d56424cfc394c438de50c99", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,ed1600e42df05fc885a16a66b77324a1", - "beta_vulgaris.rnaseq.raw.counts.cleaned.renamed.csv:md5,d90b7475356d812ed289bc9fb3cb1acd", "whole_gene_id_mapping.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", "whole_gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", "whole_design.csv:md5,3c1e14c9bd7ad250326b070a0dd4d81f", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.csv:md5,b407990bb10106de943f9e93cda6323e", - "beta_vulgaris.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.csv:md5,2c326f3e419341f955bb757fc8bf4357", "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", "accessions.txt:md5,63a651d9df354aef24400cebe56dd5ec", "warning_reason.txt:md5,603e30b732b7a6b501b59adf9d0e8837", - "id_mapping_stats.csv:md5,7c20b1e561989fcd2ce038c4a061caa5", - "ratio_zeros.csv:md5,9794647ae1d7c87ec212c0c12b658d4e", - "skewness.csv:md5,7e1ecb86c9c51394a0dacfdaca05899b", + "id_mapping_stats.csv:md5,dc2d9d7f34e570411c8cf5885b447719", + "ratio_zeros.csv:md5,a6ca9ce8ab585102df150ae182af68b6", + "skewness.csv:md5,3fecd4b1fa61e11361d6d871763c8e48", "geo_warning_reasons.csv:md5,0a77f9268abb1084fde8cb4c5cc96eca" ] ], @@ -1116,138 +1101,23 @@ "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2026-01-03T11:24:55.793642391" + "timestamp": "2026-01-13T13:09:19.670861344" }, "-profile test_dataset_custom_mapping_and_gene_length": { "content": [ { - "AGGREGATE_RESULTS": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "COLLECT_STATISTICS": { - "pandas": "2.3.3", - "python": "3.13.7" - }, - "COMPUTE_BASE_STATISTICS": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "COMPUTE_BASE_STATISTICS_FOR_MICROARRAY": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "COMPUTE_BASE_STATISTICS_FOR_RNASEQ": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "COMPUTE_DATASET_STATISTICS": { - "pandas": "2.3.3", - "python": "3.13.7" - }, - "COMPUTE_STABILITY_SCORES": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "COMPUTE_TPM": { - "pandas": "2.3.3", - "python": "3.13.7" - }, - "DASH_APP": { - "python": "3.13.8", - "dash": "3.2.0", - "dash-extensions": "2.0.4", - "dash-mantine-components": "2.3.0", - "dash-ag-grid": "32.3.2", - "polars": "1.35.0", - "pandas": "2.3.3", - "pyarrow": "22.0.0", - "scipy": "1.16.3" - }, - "GET_CANDIDATE_GENES": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "MERGE_ALL_COUNTS": { - "polars": "1.34.0", - "python": "3.14.0", - "tqdm": "4.67.1" - }, - "MERGE_MICROARRAY_COUNTS": { - "polars": "1.34.0", - "python": "3.14.0", - "tqdm": "4.67.1" - }, - "MERGE_RNASEQ_COUNTS": { - "polars": "1.34.0", - "python": "3.14.0", - "tqdm": "4.67.1" - }, - "NORMFINDER": { - "polars": "1.33.1", - "python": "3.13.7" - }, - "QUANTILE_NORMALISATION": { - "pandas": "2.2.3", - "pyarrow": "19.0.0", - "python": "3.12.8", - "scikit-learn": "1.6.1" - }, - "RENAME_GENE_IDS": { - "pandas": "2.3.3", - "polars": "1.35.2", - "python": "3.14.0" - }, "Workflow": { "nf-core/stableexpression": "v1.0dev" } }, [ - "aggregated", - "aggregated/all_counts_filtered.parquet", - "aggregated/all_genes_summary.csv", - "aggregated/most_stable_genes_summary.csv", - "aggregated/most_stable_genes_transposed_counts_filtered.csv", - "dash_app", - "dash_app/app.py", - "dash_app/assets", - "dash_app/assets/style.css", - "dash_app/data", - "dash_app/data/all_counts.parquet", - "dash_app/data/all_genes_summary.csv", - "dash_app/data/whole_design.csv", - "dash_app/environment.yml", - "dash_app/src", - "dash_app/src/callbacks", - "dash_app/src/callbacks/common.py", - "dash_app/src/callbacks/genes.py", - "dash_app/src/callbacks/samples.py", - "dash_app/src/components", - "dash_app/src/components/graphs.py", - "dash_app/src/components/icons.py", - "dash_app/src/components/right_sidebar.py", - "dash_app/src/components/settings", - "dash_app/src/components/settings/genes.py", - "dash_app/src/components/settings/samples.py", - "dash_app/src/components/stores.py", - "dash_app/src/components/tables.py", - "dash_app/src/components/tooltips.py", - "dash_app/src/components/top.py", - "dash_app/src/utils", - "dash_app/src/utils/config.py", - "dash_app/src/utils/data_management.py", - "dash_app/src/utils/style.py", "errors", "idmapping", "idmapping/global_gene_id_mapping.csv", "idmapping/global_gene_metadata.csv", - "idmapping/renamed", - "idmapping/renamed/microarray.normalised.renamed.csv", - "idmapping/renamed/rnaseq.raw.renamed.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", "merged_datasets", - "merged_datasets/whole_design.csv", "multiqc", "multiqc/multiqc_data", "multiqc/multiqc_data/llms-full.txt", @@ -1255,84 +1125,27 @@ "multiqc/multiqc_data/multiqc.parquet", "multiqc/multiqc_data/multiqc_citations.txt", "multiqc/multiqc_data/multiqc_data.json", - "multiqc/multiqc_data/multiqc_expression_distributions_most_stable_genes.txt", - "multiqc/multiqc_data/multiqc_gene_statistics.txt", - "multiqc/multiqc_data/multiqc_id_mapping_stats.txt", - "multiqc/multiqc_data/multiqc_ranked_most_stable_genes_summary.txt", - "multiqc/multiqc_data/multiqc_ratio_zeros.txt", - "multiqc/multiqc_data/multiqc_skewness.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", - "multiqc/multiqc_plots", - "multiqc/multiqc_plots/pdf", - "multiqc/multiqc_plots/pdf/expression_distributions_most_stable_genes.pdf", - "multiqc/multiqc_plots/pdf/gene_statistics.pdf", - "multiqc/multiqc_plots/pdf/id_mapping_stats-cnt.pdf", - "multiqc/multiqc_plots/pdf/id_mapping_stats-pct.pdf", - "multiqc/multiqc_plots/pdf/ranked_most_stable_genes_summary.pdf", - "multiqc/multiqc_plots/pdf/ratio_zeros.pdf", - "multiqc/multiqc_plots/pdf/skewness.pdf", - "multiqc/multiqc_plots/png", - "multiqc/multiqc_plots/png/expression_distributions_most_stable_genes.png", - "multiqc/multiqc_plots/png/gene_statistics.png", - "multiqc/multiqc_plots/png/id_mapping_stats-cnt.png", - "multiqc/multiqc_plots/png/id_mapping_stats-pct.png", - "multiqc/multiqc_plots/png/ranked_most_stable_genes_summary.png", - "multiqc/multiqc_plots/png/ratio_zeros.png", - "multiqc/multiqc_plots/png/skewness.png", - "multiqc/multiqc_plots/svg", - "multiqc/multiqc_plots/svg/expression_distributions_most_stable_genes.svg", - "multiqc/multiqc_plots/svg/gene_statistics.svg", - "multiqc/multiqc_plots/svg/id_mapping_stats-cnt.svg", - "multiqc/multiqc_plots/svg/id_mapping_stats-pct.svg", - "multiqc/multiqc_plots/svg/ranked_most_stable_genes_summary.svg", - "multiqc/multiqc_plots/svg/ratio_zeros.svg", - "multiqc/multiqc_plots/svg/skewness.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", - "normalised", - "normalised/microarray.normalised", - "normalised/microarray.normalised/quantile_normalised", - "normalised/microarray.normalised/quantile_normalised/microarray.normalised.renamed.quant_norm.parquet", - "normalised/rnaseq.raw", - "normalised/rnaseq.raw/quantile_normalised", - "normalised/rnaseq.raw/quantile_normalised/rnaseq.raw.renamed.tpm.quant_norm.parquet", - "normalised/rnaseq.raw/tpm", - "normalised/rnaseq.raw/tpm/rnaseq.raw.renamed.tpm.csv", "pipeline_info", "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", "statistics", - "statistics/id_mapping_stats.csv", - "statistics/ratio_zeros.csv", - "statistics/skewness.csv", "warnings" ], [ - "all_genes_summary.csv:md5,03b370a040cac715b58c31865e7c8557", - "most_stable_genes_summary.csv:md5,03b370a040cac715b58c31865e7c8557", - "most_stable_genes_transposed_counts_filtered.csv:md5,9ee131e180ccaa879342af5873cdcf19", - "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_genes_summary.csv:md5,03b370a040cac715b58c31865e7c8557", - "whole_design.csv:md5,70d6c2673e619ca52d2774fb3e368382", - "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", "global_gene_id_mapping.csv:md5,187a86074197044846bb8565e122eb8e", "global_gene_metadata.csv:md5,5ae2d701ca0cb6384d9e1e08a345e452", - "microarray.normalised.renamed.csv:md5,6adb74d67379a5a3d3309b10a0c4bec5", - "rnaseq.raw.renamed.csv:md5,aa22384ba73d180629add4e174c7f37d", "whole_gene_id_mapping.csv:md5,187a86074197044846bb8565e122eb8e", - "whole_gene_metadata.csv:md5,5ae2d701ca0cb6384d9e1e08a345e452", - "whole_design.csv:md5,70d6c2673e619ca52d2774fb3e368382", - "rnaseq.raw.renamed.tpm.csv:md5,dde1d8ea5d271c4e0486c7ba0936a972", - "id_mapping_stats.csv:md5,ee400af7734b2226406fc7ba986dccfa", - "ratio_zeros.csv:md5,d206e45c16e6bd13de75ea6d20bbd30d", - "skewness.csv:md5,9917b39dfe5ee6e680fa1783f8a096c4" + "whole_gene_metadata.csv:md5,5ae2d701ca0cb6384d9e1e08a345e452" ] ], "meta": { "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-23T12:28:01.788499112" + "timestamp": "2026-01-06T17:24:45.471369388" }, "-profile test_accessions_only": { "content": [ @@ -1392,63 +1205,46 @@ "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-23T12:18:58.308749506" + "timestamp": "2026-01-13T13:11:02.671079693" }, "-profile test_one_accession_low_gene_count": { "content": [ { - "AGGREGATE_RESULTS": { + "CLEAN_GENE_IDS": { "polars": "1.17.1", "python": "3.12.8" }, - "CLEAN_GENE_IDS": { - "pandas": "2.3.3", - "polars": "1.35.2", - "python": "3.14.0" - }, "COLLECT_GENE_IDS": { - "pandas": "2.3.3", - "python": "3.14.0", + "python": "3.14.2", "tqdm": "4.67.1" }, "COLLECT_STATISTICS": { "pandas": "2.3.3", "python": "3.13.7" }, - "COMPUTE_BASE_STATISTICS": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "COMPUTE_BASE_STATISTICS_FOR_RNASEQ": { + "COMPUTE_DATASET_STATISTICS": { "polars": "1.17.1", "python": "3.12.8" }, - "COMPUTE_DATASET_STATISTICS": { - "pandas": "2.3.3", - "python": "3.13.7" - }, "COMPUTE_GENE_TRANSCRIPT_LENGTHS": { "pandas": "2.3.3", "python": "3.13.7" }, - "COMPUTE_STABILITY_SCORES": { + "COMPUTE_GLOBAL_STATISTICS": { + "polars": "1.17.1", + "python": "3.12.8" + }, + "COMPUTE_PLATFORM_STATISTICS": { "polars": "1.17.1", "python": "3.12.8" }, "COMPUTE_TPM": { - "pandas": "2.3.3", - "python": "3.13.7" + "polars": "1.17.1", + "python": "3.12.8" }, - "DASH_APP": { - "python": "3.13.8", - "dash": "3.2.0", - "dash-extensions": "2.0.4", - "dash-mantine-components": "2.3.0", - "dash-ag-grid": "32.3.2", - "polars": "1.35.0", - "pandas": "2.3.3", - "pyarrow": "22.0.0", - "scipy": "1.16.3" + "DETECT_RARE_GENES": { + "polars": "1.17.1", + "python": "3.12.8" }, "DOWNLOAD_ENSEMBL_ANNOTATION": { "bs4": "4.14.2", @@ -1461,6 +1257,10 @@ "ExpressionAtlas": "1.30.0", "R": "4.3.3 (2024-02-29)" }, + "FILTER_AND_RENAME_GENES": { + "polars": "1.17.1", + "python": "3.12.8" + }, "GET_CANDIDATE_GENES": { "polars": "1.17.1", "python": "3.12.8" @@ -1475,7 +1275,7 @@ "python": "3.14.0", "tqdm": "4.67.1" }, - "MERGE_RNASEQ_COUNTS": { + "MERGE_PLATFORM_COUNTS": { "polars": "1.34.0", "python": "3.14.0", "tqdm": "4.67.1" @@ -1485,67 +1285,29 @@ "python": "3.13.7" }, "QUANTILE_NORMALISATION": { - "pandas": "2.2.3", - "pyarrow": "19.0.0", - "python": "3.12.8", - "scikit-learn": "1.6.1" + "polars": "1.36.1", + "python": "3.14.2", + "scikit-learn": "1.8.0" }, - "RENAME_GENE_IDS": { - "pandas": "2.3.3", - "polars": "1.35.2", - "python": "3.14.0" + "REMOVE_SAMPLES_NOT_VALID": { + "polars": "1.17.1", + "python": "3.12.8" }, "Workflow": { "nf-core/stableexpression": "v1.0dev" } }, [ - "aggregated", - "aggregated/all_counts_filtered.parquet", - "aggregated/all_genes_summary.csv", - "aggregated/most_stable_genes_summary.csv", - "aggregated/most_stable_genes_transposed_counts_filtered.csv", - "dash_app", - "dash_app/app.py", - "dash_app/assets", - "dash_app/assets/style.css", - "dash_app/data", - "dash_app/data/all_counts.parquet", - "dash_app/data/all_genes_summary.csv", - "dash_app/data/whole_design.csv", - "dash_app/environment.yml", - "dash_app/src", - "dash_app/src/callbacks", - "dash_app/src/callbacks/common.py", - "dash_app/src/callbacks/genes.py", - "dash_app/src/callbacks/samples.py", - "dash_app/src/components", - "dash_app/src/components/graphs.py", - "dash_app/src/components/icons.py", - "dash_app/src/components/right_sidebar.py", - "dash_app/src/components/settings", - "dash_app/src/components/settings/genes.py", - "dash_app/src/components/settings/samples.py", - "dash_app/src/components/stores.py", - "dash_app/src/components/tables.py", - "dash_app/src/components/tooltips.py", - "dash_app/src/components/top.py", - "dash_app/src/utils", - "dash_app/src/utils/config.py", - "dash_app/src/utils/data_management.py", - "dash_app/src/utils/style.py", "errors", "idmapping", "idmapping/collected_gene_ids", - "idmapping/collected_gene_ids/all_gene_ids.txt", + "idmapping/collected_gene_ids/gene_id_occurrences.csv", + "idmapping/collected_gene_ids/unique_gene_ids.txt", "idmapping/global_gene_id_mapping.csv", "idmapping/global_gene_metadata.csv", "idmapping/gprofiler", "idmapping/gprofiler/gene_metadata.csv", "idmapping/gprofiler/mapped_gene_ids.csv", - "idmapping/original_gene_ids.txt", - "idmapping/renamed", - "idmapping/renamed/E_GEOD_51720_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", "merged_datasets", @@ -1557,47 +1319,39 @@ "multiqc/multiqc_data/multiqc.parquet", "multiqc/multiqc_data/multiqc_citations.txt", "multiqc/multiqc_data/multiqc_data.json", - "multiqc/multiqc_data/multiqc_expression_distributions_most_stable_genes.txt", - "multiqc/multiqc_data/multiqc_gene_statistics.txt", "multiqc/multiqc_data/multiqc_id_mapping_stats.txt", - "multiqc/multiqc_data/multiqc_ranked_most_stable_genes_summary.txt", "multiqc/multiqc_data/multiqc_ratio_zeros.txt", "multiqc/multiqc_data/multiqc_skewness.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_data/multiqc_total_gene_id_occurrence_quantiles.txt", "multiqc/multiqc_plots", "multiqc/multiqc_plots/pdf", - "multiqc/multiqc_plots/pdf/expression_distributions_most_stable_genes.pdf", - "multiqc/multiqc_plots/pdf/gene_statistics.pdf", "multiqc/multiqc_plots/pdf/id_mapping_stats-cnt.pdf", "multiqc/multiqc_plots/pdf/id_mapping_stats-pct.pdf", - "multiqc/multiqc_plots/pdf/ranked_most_stable_genes_summary.pdf", "multiqc/multiqc_plots/pdf/ratio_zeros.pdf", "multiqc/multiqc_plots/pdf/skewness.pdf", + "multiqc/multiqc_plots/pdf/total_gene_id_occurrence_quantiles.pdf", "multiqc/multiqc_plots/png", - "multiqc/multiqc_plots/png/expression_distributions_most_stable_genes.png", - "multiqc/multiqc_plots/png/gene_statistics.png", "multiqc/multiqc_plots/png/id_mapping_stats-cnt.png", "multiqc/multiqc_plots/png/id_mapping_stats-pct.png", - "multiqc/multiqc_plots/png/ranked_most_stable_genes_summary.png", "multiqc/multiqc_plots/png/ratio_zeros.png", "multiqc/multiqc_plots/png/skewness.png", + "multiqc/multiqc_plots/png/total_gene_id_occurrence_quantiles.png", "multiqc/multiqc_plots/svg", - "multiqc/multiqc_plots/svg/expression_distributions_most_stable_genes.svg", - "multiqc/multiqc_plots/svg/gene_statistics.svg", "multiqc/multiqc_plots/svg/id_mapping_stats-cnt.svg", "multiqc/multiqc_plots/svg/id_mapping_stats-pct.svg", - "multiqc/multiqc_plots/svg/ranked_most_stable_genes_summary.svg", "multiqc/multiqc_plots/svg/ratio_zeros.svg", "multiqc/multiqc_plots/svg/skewness.svg", + "multiqc/multiqc_plots/svg/total_gene_id_occurrence_quantiles.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", "normalised", "normalised/E_GEOD_51720_rnaseq", "normalised/E_GEOD_51720_rnaseq/quantile_normalised", - "normalised/E_GEOD_51720_rnaseq/quantile_normalised/E_GEOD_51720_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/E_GEOD_51720_rnaseq/quantile_normalised/E_GEOD_51720_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.quant_norm.parquet", "normalised/E_GEOD_51720_rnaseq/tpm", - "normalised/E_GEOD_51720_rnaseq/tpm/E_GEOD_51720_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", + "normalised/E_GEOD_51720_rnaseq/tpm/E_GEOD_51720_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.parquet", "pipeline_info", "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", "public_data", @@ -1612,36 +1366,27 @@ "warnings" ], [ - "all_genes_summary.csv:md5,8308c2f930f305e3db913d992a6acf36", - "most_stable_genes_summary.csv:md5,bd5c71953b259d05d024f68eb4b62942", - "most_stable_genes_transposed_counts_filtered.csv:md5,d6a99e3a8a422af722dea852d84bdc94", - "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_genes_summary.csv:md5,8308c2f930f305e3db913d992a6acf36", - "whole_design.csv:md5,d3aa542c4ad07d0051a84482fe6cd81c", - "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", - "all_gene_ids.txt:md5,9c463e6c1754d4f4c7ea684578aa6849", + "gene_id_occurrences.csv:md5,279e38e052661017d28451c348f39f21", + "unique_gene_ids.txt:md5,9c463e6c1754d4f4c7ea684578aa6849", "global_gene_id_mapping.csv:md5,42491ef436cce231258c0358e1af5745", "global_gene_metadata.csv:md5,b35e20500269d4e6787ef1a3468f16bc", - "gene_metadata.csv:md5,bb7db05964749de50bee10afdded87b0", + "gene_metadata.csv:md5,b35e20500269d4e6787ef1a3468f16bc", "mapped_gene_ids.csv:md5,42491ef436cce231258c0358e1af5745", - "original_gene_ids.txt:md5,31c068e2e8b054ea4d696dec754caed4", - "E_GEOD_51720_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,6f3c401caaf89e534cc13db8976b296c", "whole_gene_id_mapping.csv:md5,42491ef436cce231258c0358e1af5745", "whole_gene_metadata.csv:md5,b35e20500269d4e6787ef1a3468f16bc", "whole_design.csv:md5,d3aa542c4ad07d0051a84482fe6cd81c", - "E_GEOD_51720_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,281e8c83c27ad9d6c732a7c013e627cf", "E_GEOD_51720_rnaseq.design.csv:md5,80805afb29837b6fbb73a6aa6f3a461b", "E_GEOD_51720_rnaseq.rnaseq.raw.counts.csv:md5,07cd448196fc2fea4663bd9705da2b98", - "id_mapping_stats.csv:md5,c5d20cc6298f0863617b1139f41a2da4", - "ratio_zeros.csv:md5,2cff16880a965af8acc438786b9fb110", - "skewness.csv:md5,bdbc6ee3d4c19b907a71d02ad9cac149" + "id_mapping_stats.csv:md5,cd17a5d4afa6b86a48adb03868d3073f", + "ratio_zeros.csv:md5,57f747774c59abc441a353544b7c11be", + "skewness.csv:md5,14ee7163a228d70c26097fa0f9a793ec" ] ], "meta": { "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-23T12:22:56.224515953" + "timestamp": "2026-01-13T13:12:38.322522517" }, "-profile test_bigger_with_genorm": { "content": [ @@ -1651,46 +1396,44 @@ "python": "3.12.8" }, "CLEAN_GENE_IDS": { - "pandas": "2.3.3", - "polars": "1.35.2", - "python": "3.14.0" + "polars": "1.17.1", + "python": "3.12.8" }, "COLLECT_GENE_IDS": { - "pandas": "2.3.3", - "python": "3.14.0", + "python": "3.14.2", "tqdm": "4.67.1" }, "COLLECT_STATISTICS": { "pandas": "2.3.3", "python": "3.13.7" }, - "COMPUTE_BASE_STATISTICS": { - "polars": "1.17.1", - "python": "3.12.8" - }, - "COMPUTE_BASE_STATISTICS_FOR_RNASEQ": { + "COMPUTE_DATASET_STATISTICS": { "polars": "1.17.1", "python": "3.12.8" }, - "COMPUTE_DATASET_STATISTICS": { - "pandas": "2.3.3", - "python": "3.13.7" - }, "COMPUTE_GENE_TRANSCRIPT_LENGTHS": { "pandas": "2.3.3", "python": "3.13.7" }, + "COMPUTE_GLOBAL_STATISTICS": { + "polars": "1.17.1", + "python": "3.12.8" + }, "COMPUTE_M_MEASURE": { "polars": "1.17.1", "python": "3.12.8" }, + "COMPUTE_PLATFORM_STATISTICS": { + "polars": "1.17.1", + "python": "3.12.8" + }, "COMPUTE_STABILITY_SCORES": { "polars": "1.17.1", "python": "3.12.8" }, "COMPUTE_TPM": { - "pandas": "2.3.3", - "python": "3.13.7" + "polars": "1.17.1", + "python": "3.12.8" }, "CROSS_JOIN": { "polars": "1.17.1", @@ -1707,6 +1450,10 @@ "pyarrow": "22.0.0", "scipy": "1.16.3" }, + "DETECT_RARE_GENES": { + "polars": "1.17.1", + "python": "3.12.8" + }, "DOWNLOAD_ENSEMBL_ANNOTATION": { "bs4": "4.14.2", "pandas": "2.3.3", @@ -1727,6 +1474,10 @@ "polars": "1.17.1", "python": "3.12.8" }, + "FILTER_AND_RENAME_GENES": { + "polars": "1.17.1", + "python": "3.12.8" + }, "GET_CANDIDATE_GENES": { "polars": "1.17.1", "python": "3.12.8" @@ -1745,7 +1496,7 @@ "python": "3.14.0", "tqdm": "4.67.1" }, - "MERGE_RNASEQ_COUNTS": { + "MERGE_PLATFORM_COUNTS": { "polars": "1.34.0", "python": "3.14.0", "tqdm": "4.67.1" @@ -1755,19 +1506,17 @@ "python": "3.13.7" }, "QUANTILE_NORMALISATION": { - "pandas": "2.2.3", - "pyarrow": "19.0.0", - "python": "3.12.8", - "scikit-learn": "1.6.1" + "polars": "1.36.1", + "python": "3.14.2", + "scikit-learn": "1.8.0" }, "RATIO_STANDARD_VARIATION": { "polars": "1.17.1", "python": "3.12.8" }, - "RENAME_GENE_IDS": { - "pandas": "2.3.3", - "polars": "1.35.2", - "python": "3.14.0" + "REMOVE_SAMPLES_NOT_VALID": { + "polars": "1.17.1", + "python": "3.12.8" }, "Workflow": { "nf-core/stableexpression": "v1.0dev" @@ -1811,15 +1560,13 @@ "errors", "idmapping", "idmapping/collected_gene_ids", - "idmapping/collected_gene_ids/all_gene_ids.txt", + "idmapping/collected_gene_ids/gene_id_occurrences.csv", + "idmapping/collected_gene_ids/unique_gene_ids.txt", "idmapping/global_gene_id_mapping.csv", "idmapping/global_gene_metadata.csv", "idmapping/gprofiler", "idmapping/gprofiler/gene_metadata.csv", "idmapping/gprofiler/mapped_gene_ids.csv", - "idmapping/original_gene_ids.txt", - "idmapping/renamed", - "idmapping/renamed/E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", "merged_datasets", @@ -1841,6 +1588,7 @@ "multiqc/multiqc_data/multiqc_skewness.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_data/multiqc_total_gene_id_occurrence_quantiles.txt", "multiqc/multiqc_plots", "multiqc/multiqc_plots/pdf", "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", @@ -1852,6 +1600,7 @@ "multiqc/multiqc_plots/pdf/ranked_most_stable_genes_summary.pdf", "multiqc/multiqc_plots/pdf/ratio_zeros.pdf", "multiqc/multiqc_plots/pdf/skewness.pdf", + "multiqc/multiqc_plots/pdf/total_gene_id_occurrence_quantiles.pdf", "multiqc/multiqc_plots/png", "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", @@ -1862,6 +1611,7 @@ "multiqc/multiqc_plots/png/ranked_most_stable_genes_summary.png", "multiqc/multiqc_plots/png/ratio_zeros.png", "multiqc/multiqc_plots/png/skewness.png", + "multiqc/multiqc_plots/png/total_gene_id_occurrence_quantiles.png", "multiqc/multiqc_plots/svg", "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", @@ -1872,14 +1622,15 @@ "multiqc/multiqc_plots/svg/ranked_most_stable_genes_summary.svg", "multiqc/multiqc_plots/svg/ratio_zeros.svg", "multiqc/multiqc_plots/svg/skewness.svg", + "multiqc/multiqc_plots/svg/total_gene_id_occurrence_quantiles.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", "normalised", "normalised/E_MTAB_5072_rnaseq", "normalised/E_MTAB_5072_rnaseq/quantile_normalised", - "normalised/E_MTAB_5072_rnaseq/quantile_normalised/E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/E_MTAB_5072_rnaseq/quantile_normalised/E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.quant_norm.parquet", "normalised/E_MTAB_5072_rnaseq/tpm", - "normalised/E_MTAB_5072_rnaseq/tpm/E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", + "normalised/E_MTAB_5072_rnaseq/tpm/E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.parquet", "pipeline_info", "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", "public_data", @@ -1898,37 +1649,35 @@ "warnings" ], [ - "all_genes_summary.csv:md5,d4b5d36061b8fd31ba27edc55f738a01", - "most_stable_genes_summary.csv:md5,b47891fe0414abd0a4b7c9137d53fc1d", - "most_stable_genes_transposed_counts_filtered.csv:md5,46d96c944cc9ca3dd83e16da52c528f2", + "all_genes_summary.csv:md5,a0d04bcad1ead65f551d466b23a5a6fb", + "most_stable_genes_summary.csv:md5,de1cfd50d23435117f6643c86f922c47", + "most_stable_genes_transposed_counts_filtered.csv:md5,5ebf832e94abb4e1bac0b9fad41c6dbb", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_genes_summary.csv:md5,d4b5d36061b8fd31ba27edc55f738a01", + "all_genes_summary.csv:md5,a0d04bcad1ead65f551d466b23a5a6fb", "whole_design.csv:md5,c9bfd7bc8ca365222e03e67eb24b9a76", - "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", - "all_gene_ids.txt:md5,13ae1b52833134f8ed6d982c00487927", + "environment.yml:md5,f9b192ef98a67f2084ad2fed6da01bc1", + "gene_id_occurrences.csv:md5,631317db987951a12f15e1c3e76068cd", + "unique_gene_ids.txt:md5,13ae1b52833134f8ed6d982c00487927", "global_gene_id_mapping.csv:md5,efc95a8e276be1eb0af9639f72e48145", "global_gene_metadata.csv:md5,1d342577587bc48c1eff077a594929fa", "gene_metadata.csv:md5,1d342577587bc48c1eff077a594929fa", "mapped_gene_ids.csv:md5,efc95a8e276be1eb0af9639f72e48145", - "original_gene_ids.txt:md5,5b88757be20075a2458a257221703f2a", - "E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,6244da0761b4437a6de5cff49e4d2687", "whole_gene_id_mapping.csv:md5,efc95a8e276be1eb0af9639f72e48145", "whole_gene_metadata.csv:md5,1d342577587bc48c1eff077a594929fa", "whole_design.csv:md5,c9bfd7bc8ca365222e03e67eb24b9a76", - "E_MTAB_5072_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,4a2aa87d0bd2990c13e088f0117cc682", "accessions.txt:md5,561a967c16b2ef6c29fb643cd4002947", "E_MTAB_5072_rnaseq.design.csv:md5,a1f33d126dde387a2d542381c44bc1f3", "E_MTAB_5072_rnaseq.rnaseq.raw.counts.csv:md5,c41c84899a380515d759b99eeccfe43e", - "id_mapping_stats.csv:md5,6a84babdbb434ffd5975fc771ec2db44", - "ratio_zeros.csv:md5,15eb210c4ffff8a826e0c9f45d0ab4bd", - "skewness.csv:md5,6dbe4934961471c31fc6a1671577a8fc" + "id_mapping_stats.csv:md5,70d0c1eacf06cd1312caaefb2f614811", + "ratio_zeros.csv:md5,b541c01e58e21cc28707534147aff80d", + "skewness.csv:md5,74dd30b70c807032d6bacdb669908dc1" ] ], "meta": { "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-23T12:41:00.400997562" + "timestamp": "2026-01-13T13:25:48.350670705" }, "-profile test_download_only": { "content": [ @@ -1995,7 +1744,7 @@ "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-23T12:19:38.305186676" + "timestamp": "2026-01-13T13:11:30.403331158" }, "-profile test_gprofiler_target_database_entrez": { "content": [ @@ -2037,15 +1786,13 @@ "errors", "idmapping", "idmapping/collected_gene_ids", - "idmapping/collected_gene_ids/all_gene_ids.txt", + "idmapping/collected_gene_ids/gene_id_occurrences.csv", + "idmapping/collected_gene_ids/unique_gene_ids.txt", "idmapping/global_gene_id_mapping.csv", "idmapping/global_gene_metadata.csv", "idmapping/gprofiler", "idmapping/gprofiler/gene_metadata.csv", "idmapping/gprofiler/mapped_gene_ids.csv", - "idmapping/original_gene_ids.txt", - "idmapping/renamed", - "idmapping/renamed/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv", "idmapping/whole_gene_id_mapping.csv", "idmapping/whole_gene_metadata.csv", "merged_datasets", @@ -2067,6 +1814,7 @@ "multiqc/multiqc_data/multiqc_skewness.txt", "multiqc/multiqc_data/multiqc_software_versions.txt", "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_data/multiqc_total_gene_id_occurrence_quantiles.txt", "multiqc/multiqc_plots", "multiqc/multiqc_plots/pdf", "multiqc/multiqc_plots/pdf/eatlas_all_experiments_metadata.pdf", @@ -2078,6 +1826,7 @@ "multiqc/multiqc_plots/pdf/ranked_most_stable_genes_summary.pdf", "multiqc/multiqc_plots/pdf/ratio_zeros.pdf", "multiqc/multiqc_plots/pdf/skewness.pdf", + "multiqc/multiqc_plots/pdf/total_gene_id_occurrence_quantiles.pdf", "multiqc/multiqc_plots/png", "multiqc/multiqc_plots/png/eatlas_all_experiments_metadata.png", "multiqc/multiqc_plots/png/eatlas_selected_experiments_metadata.png", @@ -2088,6 +1837,7 @@ "multiqc/multiqc_plots/png/ranked_most_stable_genes_summary.png", "multiqc/multiqc_plots/png/ratio_zeros.png", "multiqc/multiqc_plots/png/skewness.png", + "multiqc/multiqc_plots/png/total_gene_id_occurrence_quantiles.png", "multiqc/multiqc_plots/svg", "multiqc/multiqc_plots/svg/eatlas_all_experiments_metadata.svg", "multiqc/multiqc_plots/svg/eatlas_selected_experiments_metadata.svg", @@ -2098,14 +1848,15 @@ "multiqc/multiqc_plots/svg/ranked_most_stable_genes_summary.svg", "multiqc/multiqc_plots/svg/ratio_zeros.svg", "multiqc/multiqc_plots/svg/skewness.svg", + "multiqc/multiqc_plots/svg/total_gene_id_occurrence_quantiles.svg", "multiqc/multiqc_report.html", "multiqc/versions.yml", "normalised", "normalised/E_MTAB_8187_rnaseq", "normalised/E_MTAB_8187_rnaseq/quantile_normalised", - "normalised/E_MTAB_8187_rnaseq/quantile_normalised/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.quant_norm.parquet", + "normalised/E_MTAB_8187_rnaseq/quantile_normalised/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.quant_norm.parquet", "normalised/E_MTAB_8187_rnaseq/tpm", - "normalised/E_MTAB_8187_rnaseq/tpm/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv", + "normalised/E_MTAB_8187_rnaseq/tpm/E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.filtered.tpm.parquet", "pipeline_info", "pipeline_info/nf_core_stableexpression_software_mqc_versions.yml", "public_data", @@ -2124,36 +1875,34 @@ "warnings" ], [ - "all_genes_summary.csv:md5,32be3aeed5cd2715847ccc2fd45c09a0", - "most_stable_genes_summary.csv:md5,2e34161e2beb2f1e0921dbf6c0ce55ba", - "most_stable_genes_transposed_counts_filtered.csv:md5,5083c04020d1c504c86689e14f731ef7", + "all_genes_summary.csv:md5,829fd6221705fc1588a11bfdb1a37210", + "most_stable_genes_summary.csv:md5,73cbb4d6462e01ed7778f076726613fd", + "most_stable_genes_transposed_counts_filtered.csv:md5,4f50fafaa96ffd7a7b0d98d9c8d6beb4", "style.css:md5,e6ba182eaf06980dbda49920efbf6e64", - "all_genes_summary.csv:md5,32be3aeed5cd2715847ccc2fd45c09a0", + "all_genes_summary.csv:md5,829fd6221705fc1588a11bfdb1a37210", "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", - "environment.yml:md5,3c87bfa06bbb068bcfb6ed4ebc608949", - "all_gene_ids.txt:md5,ba79f5609df755c0f75de41357319c84", + "environment.yml:md5,f9b192ef98a67f2084ad2fed6da01bc1", + "gene_id_occurrences.csv:md5,b3ee7b1c575f83d247c5bce88382fb2b", + "unique_gene_ids.txt:md5,ba79f5609df755c0f75de41357319c84", "global_gene_id_mapping.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", "global_gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", "gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", "mapped_gene_ids.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", - "original_gene_ids.txt:md5,60a0406c1d56424cfc394c438de50c99", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.csv:md5,ed1600e42df05fc885a16a66b77324a1", "whole_gene_id_mapping.csv:md5,7eecbd2d88adaf5f213f238a72d28b99", "whole_gene_metadata.csv:md5,cc8d4afdbaf03cd39a4e10f2a9040b7e", "whole_design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", - "E_MTAB_8187_rnaseq.rnaseq.raw.counts.cleaned.renamed.tpm.csv:md5,b407990bb10106de943f9e93cda6323e", "accessions.txt:md5,76e5e3af7c72eac7a1993a2bd75b4d1a", "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", - "id_mapping_stats.csv:md5,6189d2a346cce55f182e00769e3fea5f", - "ratio_zeros.csv:md5,d3b518709a097d9e41a05142b524f03c", - "skewness.csv:md5,b38aabc94d60d93b979c3cef3a922299" + "id_mapping_stats.csv:md5,17ccaa8e70c67c7d0de4ec3c630c2e5b", + "ratio_zeros.csv:md5,32889cf6de2af6413c42b8810a99a2df", + "skewness.csv:md5,178449bbd2361aa1e804e3f18e092ef1" ] ], "meta": { "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-23T12:35:29.173977298" + "timestamp": "2026-01-13T13:20:59.702133972" } } \ No newline at end of file diff --git a/tests/modules/local/aggregate_results/main.nf.test b/tests/modules/local/aggregate_results/main.nf.test index 5d4a0bf9..20016d9b 100644 --- a/tests/modules/local/aggregate_results/main.nf.test +++ b/tests/modules/local/aggregate_results/main.nf.test @@ -12,10 +12,9 @@ nextflow_process { """ input[0] = file( '$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet', checkIfExists: true) input[1] = file( '$projectDir/tests/test_data/base_statistics/output/stats_all_genes.csv', checkIfExists: true) - input[2] = file( '$projectDir/tests/test_data/aggregate_results/rnaseq_stats_all_genes.csv', checkIfExists: true) - input[3] = [] - input[4] = file( '$projectDir/tests/test_data/aggregate_results/metadata.csv', checkIfExists: true) - input[5] = file( '$projectDir/tests/test_data/aggregate_results/mapping.csv', checkIfExists: true) + input[2] = [ file( '$projectDir/tests/test_data/aggregate_results/rnaseq_stats_all_genes.csv', checkIfExists: true) ] + input[3] = file( '$projectDir/tests/test_data/aggregate_results/metadata.csv', checkIfExists: true) + input[4] = file( '$projectDir/tests/test_data/aggregate_results/mapping.csv', checkIfExists: true) """ } } @@ -36,10 +35,12 @@ nextflow_process { """ input[0] = file( '$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet', checkIfExists: true) input[1] = file( '$projectDir/tests/test_data/base_statistics/output/stats_all_genes.csv', checkIfExists: true) - input[2] = file( '$projectDir/tests/test_data/aggregate_results/rnaseq_stats_all_genes.csv', checkIfExists: true) - input[3] = file( '$projectDir/tests/test_data/aggregate_results/microarray_stats_all_genes.csv', checkIfExists: true) - input[4] = file( '$projectDir/tests/test_data/aggregate_results/metadata.csv', checkIfExists: true) - input[5] = file( '$projectDir/tests/test_data/aggregate_results/mapping.csv', checkIfExists: true) + input[2] = [ + file( '$projectDir/tests/test_data/aggregate_results/rnaseq_stats_all_genes.csv', checkIfExists: true), + file( '$projectDir/tests/test_data/aggregate_results/microarray_stats_all_genes.csv', checkIfExists: true) + ] + input[3] = file( '$projectDir/tests/test_data/aggregate_results/metadata.csv', checkIfExists: true) + input[4] = file( '$projectDir/tests/test_data/aggregate_results/mapping.csv', checkIfExists: true) """ } } diff --git a/tests/modules/local/compute_base_statistics/main.nf.test b/tests/modules/local/compute_base_statistics/main.nf.test index 57474fd4..5a2ed2c3 100644 --- a/tests/modules/local/compute_base_statistics/main.nf.test +++ b/tests/modules/local/compute_base_statistics/main.nf.test @@ -10,8 +10,10 @@ nextflow_process { when { process { """ - input[0] = file( '$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet', checkIfExists: true) - input[1] = [] + input[0] = [ + [ platform: 'all' ], + file( '$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet', checkIfExists: true) + ] """ } } @@ -30,8 +32,10 @@ nextflow_process { when { process { """ - input[0] = file( '$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet', checkIfExists: true) - input[1] = 'rnaseq' + input[0] = [ + [ platform: 'rnaseq' ], + file( '$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet', checkIfExists: true) + ] """ } } diff --git a/tests/modules/local/compute_dataset_statistics/main.nf.test.snap b/tests/modules/local/compute_dataset_statistics/main.nf.test.snap index 3ac70367..20ceb9b7 100644 --- a/tests/modules/local/compute_dataset_statistics/main.nf.test.snap +++ b/tests/modules/local/compute_dataset_statistics/main.nf.test.snap @@ -3,23 +3,41 @@ "content": [ { "0": [ - + [ + [ + "nb_samples.ipynb:md5,eaea35c0b57650ecf2f88322e6060926" + ], + "skewness.txt:md5,3f2c6b786ec7344d8d21444cfd3714c5" + ] ], "1": [ - + [ + [ + "nb_samples.ipynb:md5,eaea35c0b57650ecf2f88322e6060926" + ], + "ratio_zeros.txt:md5,2fc93fe393cf18725bbc6d575e3d2d89" + ] ], "2": [ - + [ + "COMPUTE_DATASET_STATISTICS", + "python", + "3.12.8" + ] ], "3": [ - + [ + "COMPUTE_DATASET_STATISTICS", + "polars", + "1.17.1" + ] ] } ], "meta": { - "nf-test": "0.9.2", + "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-04T11:55:04.60917467" + "timestamp": "2026-01-06T17:37:06.331244835" } } \ No newline at end of file diff --git a/tests/modules/local/rename_gene_ids/main.nf.test b/tests/modules/local/filter_and_rename_genes/main.nf.test similarity index 53% rename from tests/modules/local/rename_gene_ids/main.nf.test rename to tests/modules/local/filter_and_rename_genes/main.nf.test index 5aa88f67..a48b2fba 100644 --- a/tests/modules/local/rename_gene_ids/main.nf.test +++ b/tests/modules/local/filter_and_rename_genes/main.nf.test @@ -1,9 +1,9 @@ nextflow_process { - name "Test Process RENAME_GENE_IDS" - script "modules/local/rename_gene_ids/main.nf" - process "RENAME_GENE_IDS" - tag "rename_gene_ids" + name "Test Process FILTER_AND_RENAME_GENES" + script "modules/local/filter_and_rename_genes/main.nf" + process "FILTER_AND_RENAME_GENES" + tag "filter_and_rename_genes" test("Map Ensembl IDs") { @@ -17,6 +17,33 @@ nextflow_process { ] ) input[1] = file("$projectDir/tests/test_data/idmapping/mapped/mapped_gene_ids.csv", checkIfExists: true) + input[2] = file("$projectDir/tests/test_data/idmapping/mapped/valid_gene_ids.txt", checkIfExists: true) + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("No valid gene") { + + when { + process { + """ + input[0] = channel.of( + [ + [ dataset: "test" ], + file("$projectDir/tests/test_data/idmapping/base/counts.ensembl_ids.csv", checkIfExists: true) + ] + ) + input[1] = file("$projectDir/tests/test_data/idmapping/mapped/mapped_gene_ids.csv", checkIfExists: true) + input[2] = file("$projectDir/tests/test_data/idmapping/mapped/no_valid_gene_id.txt", checkIfExists: true) """ } } @@ -44,6 +71,7 @@ nextflow_process { ] ) input[1] = file( "$projectDir/tests/test_data/idmapping/tsv/mapping.tsv", checkIfExists: true) + input[2] = file("$projectDir/tests/test_data/idmapping/tsv/valid_gene_ids.txt", checkIfExists: true) """ } } diff --git a/tests/modules/local/filter_and_rename_genes/main.nf.test.snap b/tests/modules/local/filter_and_rename_genes/main.nf.test.snap new file mode 100644 index 00000000..0f7c284b --- /dev/null +++ b/tests/modules/local/filter_and_rename_genes/main.nf.test.snap @@ -0,0 +1,170 @@ +{ + "Custom mapping - TSV": { + "content": [ + { + "0": [ + + ], + "1": [ + [ + [ + "nb_samples.ipynb:md5,eaea35c0b57650ecf2f88322e6060926" + ], + "failure_reason.txt:md5,0eea8256c81d0362f3f10979ab2de23e" + ] + ], + "2": [ + + ], + "3": [ + [ + [ + "nb_samples.ipynb:md5,eaea35c0b57650ecf2f88322e6060926" + ], + "0", + "0", + "3", + "0" + ] + ], + "4": [ + [ + "FILTER_AND_RENAME_GENES", + "python", + "3.12.8" + ] + ], + "5": [ + [ + "FILTER_AND_RENAME_GENES", + "polars", + "1.17.1" + ] + ], + "counts": [ + + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.2" + }, + "timestamp": "2026-01-06T19:01:46.153372095" + }, + "Map Ensembl IDs": { + "content": [ + { + "0": [ + [ + { + "dataset": [ + "nb_samples.ipynb:md5,eaea35c0b57650ecf2f88322e6060926" + ] + }, + "counts.ensembl_ids.renamed.parquet:md5,dcdec4c4a0bdcc5802a6d4c3c24d0af6" + ] + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + [ + [ + "nb_samples.ipynb:md5,eaea35c0b57650ecf2f88322e6060926" + ], + "2", + "1", + "1", + "0" + ] + ], + "4": [ + [ + "FILTER_AND_RENAME_GENES", + "python", + "3.12.8" + ] + ], + "5": [ + [ + "FILTER_AND_RENAME_GENES", + "polars", + "1.17.1" + ] + ], + "counts": [ + [ + { + "dataset": [ + "nb_samples.ipynb:md5,eaea35c0b57650ecf2f88322e6060926" + ] + }, + "counts.ensembl_ids.renamed.parquet:md5,dcdec4c4a0bdcc5802a6d4c3c24d0af6" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.2" + }, + "timestamp": "2026-01-06T19:00:32.790897635" + }, + "No valid gene": { + "content": [ + { + "0": [ + + ], + "1": [ + [ + [ + "nb_samples.ipynb:md5,eaea35c0b57650ecf2f88322e6060926" + ], + "failure_reason.txt:md5,0eea8256c81d0362f3f10979ab2de23e" + ] + ], + "2": [ + + ], + "3": [ + [ + [ + "nb_samples.ipynb:md5,eaea35c0b57650ecf2f88322e6060926" + ], + "0", + "0", + "3", + "0" + ] + ], + "4": [ + [ + "FILTER_AND_RENAME_GENES", + "python", + "3.12.8" + ] + ], + "5": [ + [ + "FILTER_AND_RENAME_GENES", + "polars", + "1.17.1" + ] + ], + "counts": [ + + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.2" + }, + "timestamp": "2026-01-06T19:00:41.34477175" + } +} \ No newline at end of file diff --git a/tests/modules/local/gprofiler/idmapping/main.nf.test.snap b/tests/modules/local/gprofiler/idmapping/main.nf.test.snap index 0f33a601..5c458b7a 100644 --- a/tests/modules/local/gprofiler/idmapping/main.nf.test.snap +++ b/tests/modules/local/gprofiler/idmapping/main.nf.test.snap @@ -35,9 +35,9 @@ } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-11-20T18:15:36.900376779" + "timestamp": "2026-01-07T10:30:46.747985228" } } \ No newline at end of file diff --git a/tests/modules/local/merge_counts/main.nf.test b/tests/modules/local/merge_counts/main.nf.test index 315f96ae..a8f55ea1 100644 --- a/tests/modules/local/merge_counts/main.nf.test +++ b/tests/modules/local/merge_counts/main.nf.test @@ -12,10 +12,12 @@ nextflow_process { process { """ input[0] = [ - file("$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet", checkIfExists: true), - file( "$projectDir/tests/test_data/dataset_statistics/input/count2.raw.cpm.quant_norm.parquet", checkIfExists: true) + [ platform: 'rnaseq' ], + [ + file("$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet", checkIfExists: true), + file( "$projectDir/tests/test_data/dataset_statistics/input/count2.raw.cpm.quant_norm.parquet", checkIfExists: true) + ] ] - input[1] = 100 """ } } @@ -36,10 +38,12 @@ nextflow_process { process { """ input[0] = [ - file("$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet", checkIfExists: true), - file( "$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet", checkIfExists: true) + [ platform: 'rnaseq' ], + [ + file("$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet", checkIfExists: true), + file( "$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet", checkIfExists: true) + ] ] - input[1] = 100 """ } } @@ -60,9 +64,11 @@ nextflow_process { process { """ input[0] = [ - file("$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet", checkIfExists: true) + [ platform: 'microarray' ], + [ + file("$projectDir/tests/test_data/dataset_statistics/input/count.raw.cpm.quant_norm.parquet", checkIfExists: true) + ] ] - input[1] = 100 """ } } diff --git a/tests/modules/local/merge_counts/main.nf.test.snap b/tests/modules/local/merge_counts/main.nf.test.snap index 6edd3d29..bdd077da 100644 --- a/tests/modules/local/merge_counts/main.nf.test.snap +++ b/tests/modules/local/merge_counts/main.nf.test.snap @@ -3,7 +3,12 @@ "content": [ { "0": [ - "all_counts.parquet:md5,ea386983967ba07c233245b530c3edd0" + [ + { + "platform": "rnaseq" + }, + "all_counts.parquet:md5,ea386983967ba07c233245b530c3edd0" + ] ], "1": [ [ @@ -27,21 +32,31 @@ ] ], "counts": [ - "all_counts.parquet:md5,ea386983967ba07c233245b530c3edd0" + [ + { + "platform": "rnaseq" + }, + "all_counts.parquet:md5,ea386983967ba07c233245b530c3edd0" + ] ] } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-12-03T18:58:05.174305853" + "timestamp": "2026-01-06T18:55:36.577938151" }, "2 identical files": { "content": [ { "0": [ - "all_counts.parquet:md5,a83c94a90be32af4fc3bcc4909f6f1d2" + [ + { + "platform": "rnaseq" + }, + "all_counts.parquet:md5,a83c94a90be32af4fc3bcc4909f6f1d2" + ] ], "1": [ [ @@ -65,21 +80,31 @@ ] ], "counts": [ - "all_counts.parquet:md5,a83c94a90be32af4fc3bcc4909f6f1d2" + [ + { + "platform": "rnaseq" + }, + "all_counts.parquet:md5,a83c94a90be32af4fc3bcc4909f6f1d2" + ] ] } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-12-03T18:58:11.980577512" + "timestamp": "2026-01-06T18:55:45.369797346" }, "1 file": { "content": [ { "0": [ - "all_counts.parquet:md5,57629ccf12df0e16a39281dfe02df4bc" + [ + { + "platform": "microarray" + }, + "all_counts.parquet:md5,57629ccf12df0e16a39281dfe02df4bc" + ] ], "1": [ [ @@ -103,14 +128,19 @@ ] ], "counts": [ - "all_counts.parquet:md5,57629ccf12df0e16a39281dfe02df4bc" + [ + { + "platform": "microarray" + }, + "all_counts.parquet:md5,57629ccf12df0e16a39281dfe02df4bc" + ] ] } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-12-03T18:58:18.896026558" + "timestamp": "2026-01-06T18:55:54.187807965" } } \ No newline at end of file diff --git a/tests/modules/local/normalisation/compute_cpm/main.nf.test.snap b/tests/modules/local/normalisation/compute_cpm/main.nf.test.snap index 5ea72bbb..6948468f 100644 --- a/tests/modules/local/normalisation/compute_cpm/main.nf.test.snap +++ b/tests/modules/local/normalisation/compute_cpm/main.nf.test.snap @@ -5,9 +5,11 @@ "0": [ [ { - "dataset": "test" + "dataset": [ + "nb_samples.ipynb:md5,eaea35c0b57650ecf2f88322e6060926" + ] }, - "counts.cpm.csv:md5,9f7c5941fb9bb3293ac26f7130275975" + "counts.cpm.parquet:md5,5f9f89a0711ea45a216dcd29805d806a" ] ], "1": [ @@ -20,31 +22,33 @@ [ "NORMALISATION_COMPUTE_CPM", "python", - "3.13.7" + "3.12.8" ] ], "4": [ [ "NORMALISATION_COMPUTE_CPM", - "pandas", - "2.3.3" + "polars", + "1.17.1" ] ], "counts": [ [ { - "dataset": "test" + "dataset": [ + "nb_samples.ipynb:md5,eaea35c0b57650ecf2f88322e6060926" + ] }, - "counts.cpm.csv:md5,9f7c5941fb9bb3293ac26f7130275975" + "counts.cpm.parquet:md5,5f9f89a0711ea45a216dcd29805d806a" ] ] } ], "meta": { - "nf-test": "0.9.2", + "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-04T11:28:41.011650184" + "timestamp": "2026-01-06T17:46:20.91627774" }, "One group": { "content": [ @@ -54,7 +58,7 @@ { "dataset": "accession" }, - "counts.cpm.csv:md5,288f9099c4dd37ddeaec3f90230431fa" + "counts.cpm.parquet:md5,0af81a4e4e335bb2be6c4fa2e375696c" ] ], "1": [ @@ -67,14 +71,14 @@ [ "NORMALISATION_COMPUTE_CPM", "python", - "3.13.7" + "3.12.8" ] ], "4": [ [ "NORMALISATION_COMPUTE_CPM", - "pandas", - "2.3.3" + "polars", + "1.17.1" ] ], "counts": [ @@ -82,16 +86,16 @@ { "dataset": "accession" }, - "counts.cpm.csv:md5,288f9099c4dd37ddeaec3f90230431fa" + "counts.cpm.parquet:md5,0af81a4e4e335bb2be6c4fa2e375696c" ] ] } ], "meta": { - "nf-test": "0.9.2", + "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-04T11:30:00.012222498" + "timestamp": "2026-01-06T17:46:38.175795079" }, "TSV files": { "content": [ @@ -101,7 +105,7 @@ { "dataset": "accession" }, - "counts.cpm.csv:md5,9f7c5941fb9bb3293ac26f7130275975" + "counts.cpm.parquet:md5,5f9f89a0711ea45a216dcd29805d806a" ] ], "1": [ @@ -114,14 +118,14 @@ [ "NORMALISATION_COMPUTE_CPM", "python", - "3.13.7" + "3.12.8" ] ], "4": [ [ "NORMALISATION_COMPUTE_CPM", - "pandas", - "2.3.3" + "polars", + "1.17.1" ] ], "counts": [ @@ -129,16 +133,16 @@ { "dataset": "accession" }, - "counts.cpm.csv:md5,9f7c5941fb9bb3293ac26f7130275975" + "counts.cpm.parquet:md5,5f9f89a0711ea45a216dcd29805d806a" ] ] } ], "meta": { - "nf-test": "0.9.2", + "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-04T11:30:07.970928668" + "timestamp": "2026-01-06T17:46:46.581965221" }, "Rows with many zeros": { "content": [ @@ -146,9 +150,11 @@ "0": [ [ { - "dataset": "test" + "dataset": [ + "nb_samples.ipynb:md5,eaea35c0b57650ecf2f88322e6060926" + ] }, - "counts.cpm.csv:md5,b9feb7bfc08ed07c717fecee08c2c8b4" + "counts.cpm.parquet:md5,a342cf59dee7ab9eadbe8df3420e3477" ] ], "1": [ @@ -161,30 +167,32 @@ [ "NORMALISATION_COMPUTE_CPM", "python", - "3.13.7" + "3.12.8" ] ], "4": [ [ "NORMALISATION_COMPUTE_CPM", - "pandas", - "2.3.3" + "polars", + "1.17.1" ] ], "counts": [ [ { - "dataset": "test" + "dataset": [ + "nb_samples.ipynb:md5,eaea35c0b57650ecf2f88322e6060926" + ] }, - "counts.cpm.csv:md5,b9feb7bfc08ed07c717fecee08c2c8b4" + "counts.cpm.parquet:md5,a342cf59dee7ab9eadbe8df3420e3477" ] ] } ], "meta": { - "nf-test": "0.9.2", + "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-04T11:29:48.474873097" + "timestamp": "2026-01-06T17:46:29.367031711" } } \ No newline at end of file diff --git a/tests/modules/local/normalisation/compute_tpm/main.nf.test.snap b/tests/modules/local/normalisation/compute_tpm/main.nf.test.snap index 87cc9528..5d11c3cb 100644 --- a/tests/modules/local/normalisation/compute_tpm/main.nf.test.snap +++ b/tests/modules/local/normalisation/compute_tpm/main.nf.test.snap @@ -5,9 +5,11 @@ "0": [ [ { - "dataset": "test" + "dataset": [ + "nb_samples.ipynb:md5,eaea35c0b57650ecf2f88322e6060926" + ] }, - "counts.tpm.csv:md5,2c9efc0a2ca95cfc4b626e5cbf3c6dbe" + "counts.tpm.parquet:md5,2fe2ac9557f7d3955d5104563185cb31" ] ], "1": [ @@ -20,31 +22,33 @@ [ "NORMALISATION_COMPUTE_TPM", "python", - "3.13.7" + "3.12.8" ] ], "4": [ [ "NORMALISATION_COMPUTE_TPM", - "pandas", - "2.3.3" + "polars", + "1.17.1" ] ], "counts": [ [ { - "dataset": "test" + "dataset": [ + "nb_samples.ipynb:md5,eaea35c0b57650ecf2f88322e6060926" + ] }, - "counts.tpm.csv:md5,2c9efc0a2ca95cfc4b626e5cbf3c6dbe" + "counts.tpm.parquet:md5,2fe2ac9557f7d3955d5104563185cb31" ] ] } ], "meta": { - "nf-test": "0.9.2", + "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-04T11:31:38.651488715" + "timestamp": "2026-01-06T17:46:55.157210607" }, "One group": { "content": [ @@ -52,9 +56,11 @@ "0": [ [ { - "dataset": "test" + "dataset": [ + "nb_samples.ipynb:md5,eaea35c0b57650ecf2f88322e6060926" + ] }, - "counts.tpm.csv:md5,4a0b42acaeb45f435f97b7bc9c99f36e" + "counts.tpm.parquet:md5,7cc642e81f82432bf38690f105e5d2de" ] ], "1": [ @@ -67,31 +73,33 @@ [ "NORMALISATION_COMPUTE_TPM", "python", - "3.13.7" + "3.12.8" ] ], "4": [ [ "NORMALISATION_COMPUTE_TPM", - "pandas", - "2.3.3" + "polars", + "1.17.1" ] ], "counts": [ [ { - "dataset": "test" + "dataset": [ + "nb_samples.ipynb:md5,eaea35c0b57650ecf2f88322e6060926" + ] }, - "counts.tpm.csv:md5,4a0b42acaeb45f435f97b7bc9c99f36e" + "counts.tpm.parquet:md5,7cc642e81f82432bf38690f105e5d2de" ] ] } ], "meta": { - "nf-test": "0.9.2", + "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-04T11:32:01.357798876" + "timestamp": "2026-01-06T17:47:12.492274549" }, "TSV files": { "content": [ @@ -99,9 +107,11 @@ "0": [ [ { - "dataset": "test" + "dataset": [ + "nb_samples.ipynb:md5,eaea35c0b57650ecf2f88322e6060926" + ] }, - "counts.tpm.csv:md5,2c9efc0a2ca95cfc4b626e5cbf3c6dbe" + "counts.tpm.parquet:md5,2fe2ac9557f7d3955d5104563185cb31" ] ], "1": [ @@ -114,31 +124,33 @@ [ "NORMALISATION_COMPUTE_TPM", "python", - "3.13.7" + "3.12.8" ] ], "4": [ [ "NORMALISATION_COMPUTE_TPM", - "pandas", - "2.3.3" + "polars", + "1.17.1" ] ], "counts": [ [ { - "dataset": "test" + "dataset": [ + "nb_samples.ipynb:md5,eaea35c0b57650ecf2f88322e6060926" + ] }, - "counts.tpm.csv:md5,2c9efc0a2ca95cfc4b626e5cbf3c6dbe" + "counts.tpm.parquet:md5,2fe2ac9557f7d3955d5104563185cb31" ] ] } ], "meta": { - "nf-test": "0.9.2", + "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-04T11:32:12.08145428" + "timestamp": "2026-01-06T17:47:20.896198862" }, "Rows with many zeros": { "content": [ @@ -146,9 +158,11 @@ "0": [ [ { - "dataset": "test" + "dataset": [ + "nb_samples.ipynb:md5,eaea35c0b57650ecf2f88322e6060926" + ] }, - "counts.tpm.csv:md5,7cdca05fb62e5216a79e46286e0466b7" + "counts.tpm.parquet:md5,72d0424d7465443a882963b3a77a2162" ] ], "1": [ @@ -161,30 +175,32 @@ [ "NORMALISATION_COMPUTE_TPM", "python", - "3.13.7" + "3.12.8" ] ], "4": [ [ "NORMALISATION_COMPUTE_TPM", - "pandas", - "2.3.3" + "polars", + "1.17.1" ] ], "counts": [ [ { - "dataset": "test" + "dataset": [ + "nb_samples.ipynb:md5,eaea35c0b57650ecf2f88322e6060926" + ] }, - "counts.tpm.csv:md5,7cdca05fb62e5216a79e46286e0466b7" + "counts.tpm.parquet:md5,72d0424d7465443a882963b3a77a2162" ] ] } ], "meta": { - "nf-test": "0.9.2", + "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-04T11:31:50.194232905" + "timestamp": "2026-01-06T17:47:03.903930502" } } \ No newline at end of file diff --git a/tests/modules/local/quantile_normalisation/main.nf.test.snap b/tests/modules/local/quantile_normalisation/main.nf.test.snap index 0816204c..69d066a3 100644 --- a/tests/modules/local/quantile_normalisation/main.nf.test.snap +++ b/tests/modules/local/quantile_normalisation/main.nf.test.snap @@ -5,54 +5,51 @@ "0": [ [ { - "dataset": "test" + "dataset": [ + "nb_samples.ipynb:md5,eaea35c0b57650ecf2f88322e6060926" + ] }, - "count.raw.cpm.quant_norm.parquet:md5,25e7be19da270c4211ebbbb7dcfb59bd" + "count.raw.cpm.quant_norm.parquet:md5,57629ccf12df0e16a39281dfe02df4bc" ] ], "1": [ [ "QUANTILE_NORMALISATION", "python", - "3.12.8" + "3.14.2" ] ], "2": [ [ "QUANTILE_NORMALISATION", - "pandas", - "2.2.3" + "polars", + "1.36.1" ] ], "3": [ [ "QUANTILE_NORMALISATION", "scikit-learn", - "1.6.1" - ] - ], - "4": [ - [ - "QUANTILE_NORMALISATION", - "pyarrow", - "19.0.0" + "1.8.0" ] ], "counts": [ [ { - "dataset": "test" + "dataset": [ + "nb_samples.ipynb:md5,eaea35c0b57650ecf2f88322e6060926" + ] }, - "count.raw.cpm.quant_norm.parquet:md5,25e7be19da270c4211ebbbb7dcfb59bd" + "count.raw.cpm.quant_norm.parquet:md5,57629ccf12df0e16a39281dfe02df4bc" ] ] } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-12-03T18:59:52.183063354" + "timestamp": "2026-01-06T17:48:09.829197251" }, "Normal target distribution": { "content": [ @@ -60,53 +57,50 @@ "0": [ [ { - "dataset": "test" + "dataset": [ + "nb_samples.ipynb:md5,eaea35c0b57650ecf2f88322e6060926" + ] }, - "count.raw.cpm.quant_norm.parquet:md5,a2fc7e21b3e26558ff4f76933ed6d512" + "count.raw.cpm.quant_norm.parquet:md5,93484b73e81c9e3a6138aaddc1f79c41" ] ], "1": [ [ "QUANTILE_NORMALISATION", "python", - "3.12.8" + "3.14.2" ] ], "2": [ [ "QUANTILE_NORMALISATION", - "pandas", - "2.2.3" + "polars", + "1.36.1" ] ], "3": [ [ "QUANTILE_NORMALISATION", "scikit-learn", - "1.6.1" - ] - ], - "4": [ - [ - "QUANTILE_NORMALISATION", - "pyarrow", - "19.0.0" + "1.8.0" ] ], "counts": [ [ { - "dataset": "test" + "dataset": [ + "nb_samples.ipynb:md5,eaea35c0b57650ecf2f88322e6060926" + ] }, - "count.raw.cpm.quant_norm.parquet:md5,a2fc7e21b3e26558ff4f76933ed6d512" + "count.raw.cpm.quant_norm.parquet:md5,93484b73e81c9e3a6138aaddc1f79c41" ] ] } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-12-03T19:00:01.455299438" + "timestamp": "2026-01-06T17:48:21.228107331" } } \ No newline at end of file diff --git a/tests/modules/local/rename_gene_ids/main.nf.test.snap b/tests/modules/local/rename_gene_ids/main.nf.test.snap deleted file mode 100644 index 70d08eaa..00000000 --- a/tests/modules/local/rename_gene_ids/main.nf.test.snap +++ /dev/null @@ -1,124 +0,0 @@ -{ - "Custom mapping - TSV": { - "content": [ - { - "0": [ - [ - { - "dataset": "test" - }, - "counts.ensembl_ids.renamed.csv:md5,c0fa7b914239a91f84e6685b465983cf" - ] - ], - "1": [ - - ], - "2": [ - - ], - "3": [ - [ - "test", - "3", - "0" - ] - ], - "4": [ - [ - "RENAME_GENE_IDS", - "python", - "3.14.0" - ] - ], - "5": [ - [ - "RENAME_GENE_IDS", - "pandas", - "2.3.3" - ] - ], - "6": [ - [ - "RENAME_GENE_IDS", - "polars", - "1.35.2" - ] - ], - "counts": [ - [ - { - "dataset": "test" - }, - "counts.ensembl_ids.renamed.csv:md5,c0fa7b914239a91f84e6685b465983cf" - ] - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-12-04T10:35:54.485094966" - }, - "Map Ensembl IDs": { - "content": [ - { - "0": [ - [ - { - "dataset": "test" - }, - "counts.ensembl_ids.renamed.csv:md5,ed287900c6de53bfff9d73c42a57f3dc" - ] - ], - "1": [ - - ], - "2": [ - - ], - "3": [ - [ - "test", - "3", - "0" - ] - ], - "4": [ - [ - "RENAME_GENE_IDS", - "python", - "3.14.0" - ] - ], - "5": [ - [ - "RENAME_GENE_IDS", - "pandas", - "2.3.3" - ] - ], - "6": [ - [ - "RENAME_GENE_IDS", - "polars", - "1.35.2" - ] - ], - "counts": [ - [ - { - "dataset": "test" - }, - "counts.ensembl_ids.renamed.csv:md5,ed287900c6de53bfff9d73c42a57f3dc" - ] - ] - } - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" - }, - "timestamp": "2025-12-04T10:35:48.129574517" - } -} \ No newline at end of file diff --git a/tests/subworkflows/local/expression_normalisation/main.nf.test.snap b/tests/subworkflows/local/expression_normalisation/main.nf.test.snap index 6c8417f5..ef5a5f99 100644 --- a/tests/subworkflows/local/expression_normalisation/main.nf.test.snap +++ b/tests/subworkflows/local/expression_normalisation/main.nf.test.snap @@ -10,7 +10,7 @@ "dataset": "rnaseq_raw", "platform": "rnaseq" }, - "rnaseq.raw.cpm.quant_norm.parquet:md5,a9143f3a40dfb90d839c44af73d16f44" + "rnaseq.raw.cpm.quant_norm.parquet:md5,447d804d600b61f0bc86326d3e0972cc" ], [ { @@ -19,7 +19,7 @@ "dataset": "microarray_normalised", "platform": "microarray" }, - "microarray.normalised.quant_norm.parquet:md5,b32fba01e39c2e54458f3b6d1b5fafc1" + "microarray.normalised.quant_norm.parquet:md5,9c3aec01cdb7ac94b0c28acd711a12a0" ] ], "counts": [ @@ -30,7 +30,7 @@ "dataset": "rnaseq_raw", "platform": "rnaseq" }, - "rnaseq.raw.cpm.quant_norm.parquet:md5,a9143f3a40dfb90d839c44af73d16f44" + "rnaseq.raw.cpm.quant_norm.parquet:md5,447d804d600b61f0bc86326d3e0972cc" ], [ { @@ -39,16 +39,16 @@ "dataset": "microarray_normalised", "platform": "microarray" }, - "microarray.normalised.quant_norm.parquet:md5,b32fba01e39c2e54458f3b6d1b5fafc1" + "microarray.normalised.quant_norm.parquet:md5,9c3aec01cdb7ac94b0c28acd711a12a0" ] ] } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-12-03T18:31:21.419202123" + "timestamp": "2026-01-06T17:50:09.532650251" }, "No rnaseq normalisation": { "content": [ @@ -61,7 +61,7 @@ "dataset": "microarray_normalised", "platform": "microarray" }, - "microarray.normalised.quant_norm.parquet:md5,b32fba01e39c2e54458f3b6d1b5fafc1" + "microarray.normalised.quant_norm.parquet:md5,9c3aec01cdb7ac94b0c28acd711a12a0" ] ], "counts": [ @@ -72,16 +72,16 @@ "dataset": "microarray_normalised", "platform": "microarray" }, - "microarray.normalised.quant_norm.parquet:md5,b32fba01e39c2e54458f3b6d1b5fafc1" + "microarray.normalised.quant_norm.parquet:md5,9c3aec01cdb7ac94b0c28acd711a12a0" ] ] } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-12-03T18:34:48.109732612" + "timestamp": "2026-01-06T17:50:21.467295296" }, "TPM Normalisation with gene length": { "content": [ @@ -94,7 +94,7 @@ "dataset": "rnaseq_raw", "platform": "rnaseq" }, - "rnaseq.raw.tpm.quant_norm.parquet:md5,0a12a61db30db8c70e609270d32bc33d" + "rnaseq.raw.tpm.quant_norm.parquet:md5,f7e75c14dde78849897a89f6e2d6ef65" ], [ { @@ -103,7 +103,7 @@ "dataset": "microarray_normalised", "platform": "microarray" }, - "microarray.normalised.quant_norm.parquet:md5,b32fba01e39c2e54458f3b6d1b5fafc1" + "microarray.normalised.quant_norm.parquet:md5,9c3aec01cdb7ac94b0c28acd711a12a0" ] ], "counts": [ @@ -114,7 +114,7 @@ "dataset": "rnaseq_raw", "platform": "rnaseq" }, - "rnaseq.raw.tpm.quant_norm.parquet:md5,0a12a61db30db8c70e609270d32bc33d" + "rnaseq.raw.tpm.quant_norm.parquet:md5,f7e75c14dde78849897a89f6e2d6ef65" ], [ { @@ -123,7 +123,7 @@ "dataset": "microarray_normalised", "platform": "microarray" }, - "microarray.normalised.quant_norm.parquet:md5,b32fba01e39c2e54458f3b6d1b5fafc1" + "microarray.normalised.quant_norm.parquet:md5,9c3aec01cdb7ac94b0c28acd711a12a0" ] ] } @@ -132,7 +132,7 @@ "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-23T09:56:09.269591811" + "timestamp": "2026-01-06T17:49:56.051333397" }, "TPM Normalisation": { "content": [ @@ -145,7 +145,7 @@ "dataset": "rnaseq_raw", "platform": "rnaseq" }, - "rnaseq.raw.tpm.quant_norm.parquet:md5,0a12a61db30db8c70e609270d32bc33d" + "rnaseq.raw.tpm.quant_norm.parquet:md5,f7e75c14dde78849897a89f6e2d6ef65" ], [ { @@ -154,7 +154,7 @@ "dataset": "microarray_normalised", "platform": "microarray" }, - "microarray.normalised.quant_norm.parquet:md5,b32fba01e39c2e54458f3b6d1b5fafc1" + "microarray.normalised.quant_norm.parquet:md5,9c3aec01cdb7ac94b0c28acd711a12a0" ] ], "counts": [ @@ -165,7 +165,7 @@ "dataset": "rnaseq_raw", "platform": "rnaseq" }, - "rnaseq.raw.tpm.quant_norm.parquet:md5,0a12a61db30db8c70e609270d32bc33d" + "rnaseq.raw.tpm.quant_norm.parquet:md5,f7e75c14dde78849897a89f6e2d6ef65" ], [ { @@ -174,15 +174,15 @@ "dataset": "microarray_normalised", "platform": "microarray" }, - "microarray.normalised.quant_norm.parquet:md5,b32fba01e39c2e54458f3b6d1b5fafc1" + "microarray.normalised.quant_norm.parquet:md5,9c3aec01cdb7ac94b0c28acd711a12a0" ] ] } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.8" + "nf-test": "0.9.3", + "nextflow": "25.10.2" }, - "timestamp": "2025-12-03T18:31:12.441977406" + "timestamp": "2026-01-06T17:49:33.232295092" } } \ No newline at end of file diff --git a/tests/test_data/idmapping/mapped/no_valid_gene_id.txt b/tests/test_data/idmapping/mapped/no_valid_gene_id.txt new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_data/idmapping/mapped/valid_gene_ids.txt b/tests/test_data/idmapping/mapped/valid_gene_ids.txt new file mode 100644 index 00000000..4fc4b319 --- /dev/null +++ b/tests/test_data/idmapping/mapped/valid_gene_ids.txt @@ -0,0 +1,2 @@ +ENSRNA049434199 +ENSRNA049434246 diff --git a/tests/test_data/idmapping/tsv/valid_gene_ids.txt b/tests/test_data/idmapping/tsv/valid_gene_ids.txt new file mode 100644 index 00000000..4fc4b319 --- /dev/null +++ b/tests/test_data/idmapping/tsv/valid_gene_ids.txt @@ -0,0 +1,2 @@ +ENSRNA049434199 +ENSRNA049434246 diff --git a/workflows/stableexpression.nf b/workflows/stableexpression.nf index 262cf2b7..8e7985cc 100644 --- a/workflows/stableexpression.nf +++ b/workflows/stableexpression.nf @@ -18,7 +18,6 @@ include { COMPUTE_DATASET_STATISTICS } from '../modules/local/comput include { AGGREGATE_RESULTS } from '../modules/local/aggregate_results' include { DASH_APP } from '../modules/local/dash_app' -include { storeDatasetSize } from '../subworkflows/local/utils_nfcore_stableexpression_pipeline' include { checkCounts } from '../subworkflows/local/utils_nfcore_stableexpression_pipeline' /* @@ -84,8 +83,6 @@ workflow STABLEEXPRESSION { if ( !params.accessions_only && !params.download_only ) { ch_counts = ch_input_datasets.mix( ch_downloaded_datasets ) - // store nb of genes and nb of samples at this stage in the meta maps - ch_counts = storeDatasetSize( ch_counts, "nb_genes", "nb_samples" ) // returns an error with a message if no dataset was found checkCounts( ch_counts )

      f6upHW{M8A(8--Blr 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 Date: Thu, 18 Dec 2025 15:35:10 +0100 Subject: [PATCH 240/258] remove geo datasets from pipeline test snapshots --- tests/.nftignore | 1 + tests/default.nf.test.snap | 9 ++++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/.nftignore b/tests/.nftignore index fd424e3f..53461f90 100644 --- a/tests/.nftignore +++ b/tests/.nftignore @@ -2,3 +2,4 @@ pipeline_info/*.{html,json,txt,yml} multiqc/** **.parquet +public_data/geo/datasets/** diff --git a/tests/default.nf.test.snap b/tests/default.nf.test.snap index 21d562ae..c8b365a0 100644 --- a/tests/default.nf.test.snap +++ b/tests/default.nf.test.snap @@ -1191,10 +1191,9 @@ "E_MTAB_8187_rnaseq.design.csv:md5,fbd18d011d7d855452e5a30a303afcbf", "E_MTAB_8187_rnaseq.rnaseq.raw.counts.csv:md5,fe221fd94f66df7120b0590091e14eb1", "accessions.txt:md5,63a651d9df354aef24400cebe56dd5ec", - "geo_all_datasets.metadata.tsv:md5,9412aab7c5031f41910d6dce03797784", - "geo_rejected_datasets.metadata.tsv:md5,0a66c9d519b4590e48b04e4c37d66416", - "geo_selected_datasets.metadata.tsv:md5,6762daf0ab2824cf872a7de208b6e81d", - "warning_reason.txt:md5,603e30b732b7a6b501b59adf9d0e8837", + "geo_all_datasets.metadata.tsv:md5,689fdd3683d0bfaa174b648def695dc7", + "geo_rejected_datasets.metadata.tsv:md5,660763cc38454d85ad96cbda1a69391a", + "geo_selected_datasets.metadata.tsv:md5,5fb3140b07aa92b5bcbfaa00c1c8396a", "id_mapping_stats.csv:md5,7c20b1e561989fcd2ce038c4a061caa5", "ratio_zeros.csv:md5,9794647ae1d7c87ec212c0c12b658d4e", "skewness.csv:md5,7e1ecb86c9c51394a0dacfdaca05899b", @@ -1205,7 +1204,7 @@ "nf-test": "0.9.3", "nextflow": "25.10.2" }, - "timestamp": "2025-12-16T13:56:28.230895486" + "timestamp": "2025-12-18T14:36:48.357141617" }, "-profile test_accessions_only": { "content": [ From 09666bcf83eb1c23b7197448ebf73e39512648de Mon Sep 17 00:00:00 2001 From: Olivier Coen Date: Mon, 22 Dec 2025 15:38:44 +0100 Subject: [PATCH 241/258] add possibility to provide custom gene length file instead of --- assets/schema_design.json | 2 +- assets/schema_gene_id_mapping.json | 2 +- assets/schema_gene_length.json | 23 ++ assets/schema_gene_metadata.json | 2 +- bin/{ => old}/clean_count_data.py | 0 .../formatters/schema/parameter/__init__.py | 2 +- nextflow.config | 2 +- nextflow_schema.json | 23 +- .../local/expression_normalisation/main.nf | 20 +- test/nb_samples.ipynb | 268 +++++++++++++++++ tests/default.nf.test | 5 +- tests/default.nf.test.snap | 269 ++++++++++++++++++ .../test_data/input_datasets/gene_lengths.csv | 10 + workflows/stableexpression.nf | 3 +- 14 files changed, 609 insertions(+), 22 deletions(-) create mode 100644 assets/schema_gene_length.json rename bin/{ => old}/clean_count_data.py (100%) create mode 100644 test/nb_samples.ipynb create mode 100644 tests/test_data/input_datasets/gene_lengths.csv diff --git a/assets/schema_design.json b/assets/schema_design.json index 9d3335bf..dc1e4b87 100644 --- a/assets/schema_design.json +++ b/assets/schema_design.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://raw.githubusercontent.com/nf-core/stableexpression/master/assets/schema_design.json", "title": "nf-core/stableexpression pipeline - design schema", - "description": "Schema for the design file provided in the design column of the params.datasets CSV file", + "description": "Schema for the design file provided in the design column of the params.datasets CSV / TSV file", "type": "array", "items": { "type": "object", diff --git a/assets/schema_gene_id_mapping.json b/assets/schema_gene_id_mapping.json index ef9467f3..fc537199 100644 --- a/assets/schema_gene_id_mapping.json +++ b/assets/schema_gene_id_mapping.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://raw.githubusercontent.com/nf-core/stableexpression/master/assets/schema_gene_id_mapping.json", "title": "nf-core/stableexpression pipeline - custom mappings schema", - "description": "Schema for the file provided with in the design column of the params.gene_id_mapping CSV file", + "description": "Schema for the file provided with the params.gene_id_mapping CSV / TSV file", "type": "array", "items": { "type": "object", diff --git a/assets/schema_gene_length.json b/assets/schema_gene_length.json new file mode 100644 index 00000000..b395cea0 --- /dev/null +++ b/assets/schema_gene_length.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/nf-core/stableexpression/master/assets/schema_gene_length.json", + "title": "nf-core/stableexpression pipeline - custom mappings schema", + "description": "Schema for the file provided with in the design column of the params.gene_length CSV file", + "type": "array", + "items": { + "type": "object", + "properties": { + "gene_id": { + "type": "string", + "pattern": "^\\S+$", + "errorMessage": "You must provide a column for original gene IDs." + }, + "length": { + "type": "integer", + "minimum": 0, + "errorMessage": "You must provide a column for gene lengths." + } + }, + "required": ["gene_id", "length"] + } +} diff --git a/assets/schema_gene_metadata.json b/assets/schema_gene_metadata.json index 7fcddc17..d3faad8c 100644 --- a/assets/schema_gene_metadata.json +++ b/assets/schema_gene_metadata.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://raw.githubusercontent.com/nf-core/stableexpression/master/assets/schema_gene_metadata.json", "title": "nf-core/stableexpression pipeline - custom mappings schema", - "description": "Schema for the file provided with in the design column of the params.gene_metadata CSV file", + "description": "Schema for the file provided with the params.gene_metadata CSV / TSV file", "type": "array", "items": { "type": "object", diff --git a/bin/clean_count_data.py b/bin/old/clean_count_data.py similarity index 100% rename from bin/clean_count_data.py rename to bin/old/clean_count_data.py diff --git a/galaxy/build/formatters/schema/parameter/__init__.py b/galaxy/build/formatters/schema/parameter/__init__.py index 74e9e956..2708d3be 100644 --- a/galaxy/build/formatters/schema/parameter/__init__.py +++ b/galaxy/build/formatters/schema/parameter/__init__.py @@ -1,13 +1,13 @@ from .base import BaseParameterFormatter from .datasets import DatasetsParameterFormatter from .required import RequiredParameterFormatter + # from .default_value import DefaultValueParameterFormatter PARAMETER_TO_CUSTOM_CLASS = { "datasets": DatasetsParameterFormatter, "normalisation_method": RequiredParameterFormatter, "nb_top_gene_candidates": RequiredParameterFormatter, - "ks_pvalue_threshold": RequiredParameterFormatter, # "species": DefaultValueParameterFormatter, } diff --git a/nextflow.config b/nextflow.config index 51c52e21..9e92b0f3 100644 --- a/nextflow.config +++ b/nextflow.config @@ -37,9 +37,9 @@ params { // statistics normalisation_method = 'tpm' + gene_length = null quantile_norm_target_distrib = 'uniform' nb_top_gene_candidates = 5000 - ks_pvalue_threshold = -1 min_expr_threshold = 0.2 // stability scoring diff --git a/nextflow_schema.json b/nextflow_schema.json index e9616b1c..ae45dbbc 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -152,7 +152,7 @@ "exists": true, "schema": "assets/schema_gene_id_mapping.json", "mimetype": "text/csv", - "pattern": "^\\S+\\.(csv|tsv|dat)$", + "pattern": "^\\S+\\.(csv|dat)$", "description": "Custom gene id mapping file", "help_text": "Path to comma-separated file containing custom gene id mappings. Each row represents a mapping from the original gene ID in your count datasets to a prefered gene ID. The mapping file should be a comma-separated file with 2 columns (original_gene_id and gene_id) and a header row.", "fa_icon": "fas fa-file" @@ -163,7 +163,7 @@ "exists": true, "schema": "assets/schema_gene_metadata.json", "mimetype": "text/csv", - "pattern": "^\\S+\\.(csv|tsv|dat)$", + "pattern": "^\\S+\\.(csv|dat)$", "description": "Custom gene metadata file", "help_text": "Path to comma-separated file containing custom gene metadata information. Each row represents a gene and links its gene ID to its name and description. The metadata file should be a comma-separated file with 3 columns (gene_id, name and description) and a header row.", "fa_icon": "fas fa-file" @@ -184,6 +184,17 @@ "default": "tpm", "help_text": "Raw RNAseq data must be normalised before further processing. `tmp offers a more accurate representation of gene expression levels as it is unbiased toward gene length. However, you can choose `cpm` if you do not have access to a genome annotation." }, + "gene_length": { + "type": "string", + "format": "file-path", + "exists": true, + "schema": "assets/schema_gene_length.json", + "mimetype": "text/csv", + "pattern": "^\\S+\\.(csv|dat)$", + "description": "Gene length file", + "help_text": "Path to comma-separated file containing gene lengths. Each row represents a gene and gives the length of its longest transcript. The file should be a comma-separated file with 2 columns (gene_id and length) and a header row.", + "fa_icon": "fas fa-file" + }, "quantile_norm_target_distrib": { "type": "string", "description": "Target distribution for quantile normalisation", @@ -192,14 +203,6 @@ "default": "uniform", "help_text": "In order to compare counts between samples and different datasets, all normalised counts are quantile normalised and mapped to a specific distribution. The pipeline uses scikit-learn's quantile_transform function. You can select the target distribution to map counts to." }, - "ks_pvalue_threshold": { - "type": "number", - "description": "Threshold for KS p-value for considering samples counts as a uniform distribution", - "fa_icon": "fas fa-battery-three-quarters", - "maximum": 1, - "default": -1, - "help_text": "P-value threshold for the Kolmogorov-Smirnov test of samples counts against a uniform distribution. Samples showing a p-value equal or below this threshold are considered not uniform and will therefore not be considered for computation of the stability score. Examples: `0`, `'0.05'`, `'1E-27'`. Provide a negative value to disable this filter. By default, all samples showing a pvalue of 0 will be discarded." - }, "min_expr_threshold": { "type": "number", "description": "Minimum percentage of quantile expression level", diff --git a/subworkflows/local/expression_normalisation/main.nf b/subworkflows/local/expression_normalisation/main.nf index c0f3721d..0223d8c1 100644 --- a/subworkflows/local/expression_normalisation/main.nf +++ b/subworkflows/local/expression_normalisation/main.nf @@ -17,6 +17,7 @@ workflow EXPRESSION_NORMALISATION { ch_datasets normalisation_method quantile_norm_target_distrib + gene_length main: @@ -35,19 +36,30 @@ workflow EXPRESSION_NORMALISATION { if ( normalisation_method == 'tpm' ) { - // download genome annotation - // and computing length of the longest transcript gene per gene - GET_TRANSCRIPT_LENGTHS (species) + if ( params.gene_length ) { + + ch_gene_length_file = channel.fromPath( params.gene_length, checkIfExists: true ) + + } else { + + // download genome annotation + // and computing length of the longest transcript gene per gene + GET_TRANSCRIPT_LENGTHS (species) + ch_gene_length_file = GET_TRANSCRIPT_LENGTHS.out.csv + + } COMPUTE_TPM( ch_raw_rnaseq_datasets_to_normalise, - GET_TRANSCRIPT_LENGTHS.out.csv + ch_gene_length_file ) ch_raw_rnaseq_datasets_normalised = COMPUTE_TPM.out.counts } else { // 'cpm' + COMPUTE_CPM( ch_raw_rnaseq_datasets_to_normalise ) ch_raw_rnaseq_datasets_normalised = COMPUTE_CPM.out.counts + } // diff --git a/test/nb_samples.ipynb b/test/nb_samples.ipynb new file mode 100644 index 00000000..cea98f7a --- /dev/null +++ b/test/nb_samples.ipynb @@ -0,0 +1,268 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 18, + "id": "2054a417", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "37fbbdec", + "metadata": {}, + "outputs": [], + "source": [ + "file = \"/home/olivier/repositories/nf-core-stableexpression/test/selected_accession_to_nb_samples.csv\"\n", + "df = pd.read_csv(file)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "2f72632c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "