From 3c33efd1fa243be0fc2429dede7583ed277933b9 Mon Sep 17 00:00:00 2001 From: JoseEspinosa Date: Thu, 17 Oct 2024 10:01:49 +0200 Subject: [PATCH 01/25] Initial template commit --- .devcontainer/devcontainer.json | 20 + .editorconfig | 37 ++ .gitattributes | 4 + .github/.dockstore.yml | 6 + .github/CONTRIBUTING.md | 123 +++++ .github/ISSUE_TEMPLATE/bug_report.yml | 50 ++ .github/ISSUE_TEMPLATE/config.yml | 7 + .github/ISSUE_TEMPLATE/feature_request.yml | 11 + .github/PULL_REQUEST_TEMPLATE.md | 26 + .github/workflows/awsfulltest.yml | 54 +++ .github/workflows/awstest.yml | 33 ++ .github/workflows/branch.yml | 44 ++ .github/workflows/ci.yml | 59 +++ .github/workflows/clean-up.yml | 24 + .github/workflows/download_pipeline.yml | 120 +++++ .github/workflows/fix-linting.yml | 89 ++++ .github/workflows/linting.yml | 83 ++++ .github/workflows/linting_comment.yml | 28 ++ .github/workflows/release-announcements.yml | 75 +++ .../workflows/template_version_comment.yml | 43 ++ .gitignore | 8 + .gitpod.yml | 17 + .nf-core.yml | 33 ++ .pre-commit-config.yaml | 13 + .prettierignore | 13 + .prettierrc.yml | 1 + CHANGELOG.md | 16 + CITATIONS.md | 38 ++ CODE_OF_CONDUCT.md | 182 +++++++ LICENSE | 21 + README.md | 109 +++++ assets/adaptivecard.json | 67 +++ assets/email_template.html | 53 +++ assets/email_template.txt | 39 ++ assets/nf-core-deepmodeloptim_logo_light.png | Bin 0 -> 89335 bytes assets/samplesheet.csv | 3 + assets/schema_input.json | 33 ++ assets/sendmail_template.txt | 31 ++ assets/slackreport.json | 34 ++ conf/base.config | 62 +++ conf/modules.config | 23 + conf/test.config | 31 ++ conf/test_full.config | 24 + docs/README.md | 10 + .../nf-core-deepmodeloptim_logo_dark.png | Bin 0 -> 24583 bytes .../nf-core-deepmodeloptim_logo_light.png | Bin 0 -> 20614 bytes docs/output.md | 33 ++ docs/usage.md | 218 +++++++++ main.nf | 89 ++++ modules.json | 31 ++ nextflow.config | 254 ++++++++++ nextflow_schema.json | 167 +++++++ .../main.nf | 230 +++++++++ .../nf-core/utils_nextflow_pipeline/main.nf | 124 +++++ .../nf-core/utils_nextflow_pipeline/meta.yml | 38 ++ .../tests/main.function.nf.test | 54 +++ .../tests/main.function.nf.test.snap | 20 + .../tests/main.workflow.nf.test | 111 +++++ .../tests/nextflow.config | 9 + .../utils_nextflow_pipeline/tests/tags.yml | 2 + .../nf-core/utils_nfcore_pipeline/main.nf | 445 ++++++++++++++++++ .../nf-core/utils_nfcore_pipeline/meta.yml | 24 + .../tests/main.function.nf.test | 134 ++++++ .../tests/main.function.nf.test.snap | 166 +++++++ .../tests/main.workflow.nf.test | 29 ++ .../tests/main.workflow.nf.test.snap | 19 + .../tests/nextflow.config | 9 + .../utils_nfcore_pipeline/tests/tags.yml | 2 + .../nf-core/utils_nfschema_plugin/main.nf | 46 ++ .../nf-core/utils_nfschema_plugin/meta.yml | 35 ++ .../utils_nfschema_plugin/tests/main.nf.test | 117 +++++ .../tests/nextflow.config | 8 + .../tests/nextflow_schema.json | 96 ++++ tower.yml | 3 + workflows/deepmodeloptim.nf | 49 ++ 75 files changed, 4359 insertions(+) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/.dockstore.yml create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/awsfulltest.yml create mode 100644 .github/workflows/awstest.yml create mode 100644 .github/workflows/branch.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/clean-up.yml create mode 100644 .github/workflows/download_pipeline.yml create mode 100644 .github/workflows/fix-linting.yml create mode 100644 .github/workflows/linting.yml create mode 100644 .github/workflows/linting_comment.yml create mode 100644 .github/workflows/release-announcements.yml create mode 100644 .github/workflows/template_version_comment.yml create mode 100644 .gitignore create mode 100644 .gitpod.yml create mode 100644 .nf-core.yml create mode 100644 .pre-commit-config.yaml create mode 100644 .prettierignore create mode 100644 .prettierrc.yml create mode 100644 CHANGELOG.md create mode 100644 CITATIONS.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 assets/adaptivecard.json create mode 100644 assets/email_template.html create mode 100644 assets/email_template.txt create mode 100644 assets/nf-core-deepmodeloptim_logo_light.png create mode 100644 assets/samplesheet.csv create mode 100644 assets/schema_input.json create mode 100644 assets/sendmail_template.txt create mode 100644 assets/slackreport.json create mode 100644 conf/base.config create mode 100644 conf/modules.config create mode 100644 conf/test.config create mode 100644 conf/test_full.config create mode 100644 docs/README.md create mode 100644 docs/images/nf-core-deepmodeloptim_logo_dark.png create mode 100644 docs/images/nf-core-deepmodeloptim_logo_light.png create mode 100644 docs/output.md create mode 100644 docs/usage.md create mode 100644 main.nf create mode 100644 modules.json create mode 100644 nextflow.config create mode 100644 nextflow_schema.json create mode 100644 subworkflows/local/utils_nfcore_deepmodeloptim_pipeline/main.nf create mode 100644 subworkflows/nf-core/utils_nextflow_pipeline/main.nf create mode 100644 subworkflows/nf-core/utils_nextflow_pipeline/meta.yml create mode 100644 subworkflows/nf-core/utils_nextflow_pipeline/tests/main.function.nf.test create mode 100644 subworkflows/nf-core/utils_nextflow_pipeline/tests/main.function.nf.test.snap create mode 100644 subworkflows/nf-core/utils_nextflow_pipeline/tests/main.workflow.nf.test create mode 100644 subworkflows/nf-core/utils_nextflow_pipeline/tests/nextflow.config create mode 100644 subworkflows/nf-core/utils_nextflow_pipeline/tests/tags.yml create mode 100644 subworkflows/nf-core/utils_nfcore_pipeline/main.nf create mode 100644 subworkflows/nf-core/utils_nfcore_pipeline/meta.yml create mode 100644 subworkflows/nf-core/utils_nfcore_pipeline/tests/main.function.nf.test create mode 100644 subworkflows/nf-core/utils_nfcore_pipeline/tests/main.function.nf.test.snap create mode 100644 subworkflows/nf-core/utils_nfcore_pipeline/tests/main.workflow.nf.test create mode 100644 subworkflows/nf-core/utils_nfcore_pipeline/tests/main.workflow.nf.test.snap create mode 100644 subworkflows/nf-core/utils_nfcore_pipeline/tests/nextflow.config create mode 100644 subworkflows/nf-core/utils_nfcore_pipeline/tests/tags.yml create mode 100644 subworkflows/nf-core/utils_nfschema_plugin/main.nf create mode 100644 subworkflows/nf-core/utils_nfschema_plugin/meta.yml create mode 100644 subworkflows/nf-core/utils_nfschema_plugin/tests/main.nf.test create mode 100644 subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow.config create mode 100644 subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow_schema.json create mode 100644 tower.yml create mode 100644 workflows/deepmodeloptim.nf diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..b290e09 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,20 @@ +{ + "name": "nfcore", + "image": "nfcore/gitpod:latest", + "remoteUser": "gitpod", + "runArgs": ["--privileged"], + + // 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" + }, + + // 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"] + } + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e105881 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,37 @@ +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 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7a2dabc --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +*.config linguist-language=nextflow +*.nf.test linguist-language=nextflow +modules/nf-core/** linguist-generated +subworkflows/nf-core/** linguist-generated diff --git a/.github/.dockstore.yml b/.github/.dockstore.yml new file mode 100644 index 0000000..191fabd --- /dev/null +++ b/.github/.dockstore.yml @@ -0,0 +1,6 @@ +# Dockstore config version, not pipeline version +version: 1.2 +workflows: + - subclass: nfl + primaryDescriptorPath: /nextflow.config + publish: True diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..2cfa080 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,123 @@ +# nf-core/deepmodeloptim: Contributing Guidelines + +Hi there! +Many thanks for taking an interest in improving nf-core/deepmodeloptim. + +We try to manage the required tasks for nf-core/deepmodeloptim using GitHub issues, you probably came to this page when creating one. +Please use the pre-filled template to save time. + +However, don't be put off by this template - other more general issues and suggestions are welcome! +Contributions to the code are even more welcome ;) + +> [!NOTE] +> If you need help using or modifying nf-core/deepmodeloptim then the best place to ask is on the nf-core Slack [#deepmodeloptim](https://nfcore.slack.com/channels/deepmodeloptim) channel ([join our Slack here](https://nf-co.re/join/slack)). + +## Contribution workflow + +If you'd like to write some code for nf-core/deepmodeloptim, the standard workflow is as follows: + +1. Check that there isn't already an issue about your idea in the [nf-core/deepmodeloptim issues](https://github.com/nf-core/deepmodeloptim/issues) to avoid duplicating work. If there isn't one already, please create one so that others know you're working on this +2. [Fork](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) the [nf-core/deepmodeloptim repository](https://github.com/nf-core/deepmodeloptim) to your GitHub account +3. Make the necessary changes / additions within your forked repository following [Pipeline conventions](#pipeline-contribution-conventions) +4. Use `nf-core pipelines schema build` and add any new parameters to the pipeline JSON schema (requires [nf-core tools](https://github.com/nf-core/tools) >= 1.10). +5. Submit a Pull Request against the `dev` branch and wait for the code to be reviewed and merged + +If you're not used to this workflow with git, you can start with some [docs from GitHub](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests) or even their [excellent `git` resources](https://try.github.io/). + +## Tests +You have the option to test your changes locally by running the pipeline. For receiving warnings about process selectors and other `debug` information, it is recommended to use the debug profile. Execute all the tests with the following command: + +```bash +nf-test test --profile debug,test,docker --verbose +``` + +When you create a pull request with changes, [GitHub Actions](https://github.com/features/actions) will run automatic tests. +Typically, pull-requests are only fully reviewed when these tests are passing, though of course we can help out before then. + +There are typically two types of tests that run: + +### Lint tests + +`nf-core` has a [set of guidelines](https://nf-co.re/developers/guidelines) which all pipelines must adhere to. +To enforce these and ensure that all pipelines stay in sync, we have developed a helper tool which runs checks on the pipeline code. This is in the [nf-core/tools repository](https://github.com/nf-core/tools) and once installed can be run locally with the `nf-core pipelines lint ` command. + +If any failures or warnings are encountered, please follow the listed URL for more documentation. + +### Pipeline tests + +Each `nf-core` pipeline should be set up with a minimal set of test-data. +`GitHub Actions` then runs the pipeline on this data to ensure that it exits successfully. +If there are any failures then the automated tests fail. +These tests are run both with the latest available version of `Nextflow` and also the minimum required version that is stated in the pipeline code. + +## Patch + +:warning: Only in the unlikely and regretful event of a release happening with a bug. + +- On your own fork, make a new branch `patch` based on `upstream/master`. +- Fix the bug, and bump version (X.Y.Z+1). +- A PR should be made on `master` from patch to directly this particular bug. + +## Getting help + +For further information/help, please consult the [nf-core/deepmodeloptim documentation](https://nf-co.re/deepmodeloptim/usage) and don't hesitate to get in touch on the nf-core Slack [#deepmodeloptim](https://nfcore.slack.com/channels/deepmodeloptim) channel ([join our Slack here](https://nf-co.re/join/slack)). + +## Pipeline contribution conventions + +To make the nf-core/deepmodeloptim code and processing logic more understandable for new contributors and to ensure quality, we semi-standardise the way the code and other contributions are written. + +### Adding a new step + +If you wish to contribute a new step, please use the following coding standards: + +1. Define the corresponding input channel into your new process from the expected previous process channel +2. Write the process block (see below). +3. Define the output channel if needed (see below). +4. Add any new parameters to `nextflow.config` with a default (see below). +5. Add any new parameters to `nextflow_schema.json` with help text (via the `nf-core pipelines schema build` tool). +6. Add sanity checks and validation for all relevant parameters. +7. Perform local tests to validate that the new code works as expected. +8. If applicable, add a new test command in `.github/workflow/ci.yml`. + +### Default values + +Parameters should be initialised / defined with default values in `nextflow.config` under the `params` scope. + +Once there, use `nf-core pipelines schema build` to add to `nextflow_schema.json`. + +### Default processes resource requirements + +Sensible defaults for process resource requirements (CPUs / memory / time) for a process should be defined in `conf/base.config`. These should generally be specified generic with `withLabel:` selectors so they can be shared across multiple processes/steps of the pipeline. A nf-core standard set of labels that should be followed where possible can be seen in the [nf-core pipeline template](https://github.com/nf-core/tools/blob/main/nf_core/pipeline-template/conf/base.config), which has the default process as a single core-process, and then different levels of multi-core configurations for increasingly large memory requirements defined with standardised labels. + +The process resources can be passed on to the tool dynamically within the process with the `${task.cpus}` and `${task.memory}` variables in the `script:` block. + +### Naming schemes + +Please use the following naming schemes, to make it easy to understand what is going where. + +- initial process channel: `ch_output_from_` +- intermediate and terminal channels: `ch__for_` + +### Nextflow version bumping + +If you are using a new feature from core Nextflow, you may bump the minimum required version of nextflow in the pipeline with: `nf-core pipelines bump-version --nextflow . [min-nf-version]` + +### Images and figures + +For overview images and other documents we follow the nf-core [style guidelines and examples](https://nf-co.re/developers/design_guidelines). + +## GitHub Codespaces + +This repo includes a devcontainer configuration which will create a GitHub Codespaces for Nextflow development! This is an online developer environment that runs in your browser, complete with VSCode and a terminal. + +To get started: + +- Open the repo in [Codespaces](https://github.com/nf-core/deepmodeloptim/codespaces) +- Tools installed + - nf-core + - Nextflow + +Devcontainer specs: + +- [DevContainer config](.devcontainer/devcontainer.json) + diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..48f759e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,50 @@ +name: Bug report +description: Report something that is broken or incorrect +labels: bug +body: + - type: markdown + attributes: + value: | + Before you post this issue, please check the documentation: + + - [nf-core website: troubleshooting](https://nf-co.re/usage/troubleshooting) + - [nf-core/deepmodeloptim pipeline documentation](https://nf-co.re/deepmodeloptim/usage) + + - type: textarea + id: description + attributes: + label: Description of the bug + description: A clear and concise description of what the bug is. + validations: + required: true + + - type: textarea + id: command_used + attributes: + label: Command used and terminal output + description: Steps to reproduce the behaviour. Please paste the command you used to launch the pipeline and the output from your terminal. + render: console + placeholder: | + $ nextflow run ... + + Some output where something broke + + - type: textarea + id: files + attributes: + label: Relevant files + description: | + Please drag and drop the relevant files here. Create a `.zip` archive if the extension is not allowed. + Your verbose log file `.nextflow.log` is often useful _(this is a hidden file in the directory where you launched the pipeline)_ as well as custom Nextflow configuration files. + + - type: textarea + id: system + attributes: + label: System information + description: | + * Nextflow version _(eg. 23.04.0)_ + * Hardware _(eg. HPC, Desktop, Cloud)_ + * Executor _(eg. slurm, local, awsbatch)_ + * Container engine: _(e.g. Docker, Singularity, Conda, Podman, Shifter, Charliecloud, or Apptainer)_ + * OS _(eg. CentOS Linux, macOS, Linux Mint)_ + * Version of nf-core/deepmodeloptim _(eg. 1.1, 1.5, 1.8.2)_ diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..e9a9c11 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,7 @@ +contact_links: + - name: Join nf-core + url: https://nf-co.re/join + about: Please join the nf-core community here + - name: "Slack #deepmodeloptim channel" + url: https://nfcore.slack.com/channels/deepmodeloptim + about: Discussion about the nf-core/deepmodeloptim pipeline diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..c140b5b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,11 @@ +name: Feature request +description: Suggest an idea for the nf-core/deepmodeloptim pipeline +labels: enhancement +body: + - type: textarea + id: description + attributes: + label: Description of feature + description: Please describe your suggestion for a new feature. It might help to describe a problem or use case, plus any alternatives that you have considered. + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..8a0bf64 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,26 @@ + + +## 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/deepmodeloptim/tree/master/.github/CONTRIBUTING.md) +- [ ] If necessary, also make a PR on the nf-core/deepmodeloptim _branch_ on the [nf-core/test-datasets](https://github.com/nf-core/test-datasets) repository. +- [ ] Make sure your code lints (`nf-core pipelines lint`). +- [ ] Ensure the test suite passes (`nextflow run . -profile test,docker --outdir `). +- [ ] Check for unexpected warnings in debug mode (`nextflow run . -profile debug,test,docker --outdir `). +- [ ] Usage Documentation in `docs/usage.md` is updated. +- [ ] Output Documentation in `docs/output.md` is updated. +- [ ] `CHANGELOG.md` is updated. +- [ ] `README.md` is updated (including new tool citations and authors/contributors). diff --git a/.github/workflows/awsfulltest.yml b/.github/workflows/awsfulltest.yml new file mode 100644 index 0000000..9847dae --- /dev/null +++ b/.github/workflows/awsfulltest.yml @@ -0,0 +1,54 @@ +name: nf-core AWS full size tests +# This workflow is triggered on PRs opened against the master branch. +# It can be additionally triggered manually with GitHub actions workflow dispatch button. +# It runs the -profile 'test_full' on AWS batch + +on: + pull_request: + branches: + - master + workflow_dispatch: + pull_request_review: + types: [submitted] + +jobs: + run-platform: + name: Run AWS full tests + if: github.repository == 'nf-core/deepmodeloptim' && github.event.review.state == 'approved' + runs-on: ubuntu-latest + steps: + - uses: octokit/request-action@v2.x + id: check_approvals + with: + route: GET /repos/${{ github.repository }}/pulls/${{ github.event.review.number }}/reviews + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - id: test_variables + 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 + - name: Launch workflow via Seqera Platform + uses: seqeralabs/action-tower-launch@v2 + # TODO nf-core: You can customise AWS full pipeline tests as required + # Add full size test data (but still relatively small datasets for few samples) + # on the `test_full.config` test runs with only one set of parameters + with: + 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/deepmodeloptim/work-${{ github.sha }} + parameters: | + { + "hook_url": "${{ secrets.MEGATESTS_ALERTS_SLACK_HOOK_URL }}", + "outdir": "s3://${{ secrets.AWS_S3_BUCKET }}/deepmodeloptim/results-${{ github.sha }}" + } + profiles: test_full + + - uses: actions/upload-artifact@v4 + with: + name: Seqera Platform debug log file + path: | + seqera_platform_action_*.log + seqera_platform_action_*.json diff --git a/.github/workflows/awstest.yml b/.github/workflows/awstest.yml new file mode 100644 index 0000000..efa3e85 --- /dev/null +++ b/.github/workflows/awstest.yml @@ -0,0 +1,33 @@ +name: nf-core AWS test +# This workflow can be triggered manually with the GitHub actions workflow dispatch button. +# It runs the -profile 'test' on AWS batch + +on: + workflow_dispatch: +jobs: + run-platform: + name: Run AWS tests + if: github.repository == 'nf-core/deepmodeloptim' + runs-on: ubuntu-latest + steps: + # Launch workflow using Seqera Platform CLI tool action + - name: Launch workflow via Seqera Platform + uses: seqeralabs/action-tower-launch@v2 + with: + 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/deepmodeloptim/work-${{ github.sha }} + parameters: | + { + "outdir": "s3://${{ secrets.AWS_S3_BUCKET }}/deepmodeloptim/results-test-${{ github.sha }}" + } + profiles: test + + - uses: actions/upload-artifact@v4 + with: + name: Seqera Platform debug log file + path: | + seqera_platform_action_*.log + seqera_platform_action_*.json diff --git a/.github/workflows/branch.yml b/.github/workflows/branch.yml new file mode 100644 index 0000000..2b4dceb --- /dev/null +++ b/.github/workflows/branch.yml @@ -0,0 +1,44 @@ +name: nf-core branch protection +# This workflow is triggered on PRs to master branch on the repository +# It fails when someone tries to make a PR against the nf-core `master` branch instead of `dev` +on: + pull_request_target: + branches: [master] + +jobs: + test: + runs-on: ubuntu-latest + steps: + # PRs to the nf-core repo master branch are only ok if coming from the nf-core repo `dev` or any `patch` branches + - name: Check PRs + if: github.repository == 'nf-core/deepmodeloptim' + run: | + { [[ ${{github.event.pull_request.head.repo.full_name }} == nf-core/deepmodeloptim ]] && [[ $GITHUB_HEAD_REF == "dev" ]]; } || [[ $GITHUB_HEAD_REF == "patch" ]] + + # If the above check failed, post a comment on the PR explaining the failure + # NOTE - this doesn't currently work if the PR is coming from a fork, due to limitations in GitHub actions secrets + - name: Post PR comment + if: failure() + uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2 + with: + message: | + ## This PR is against the `master` branch :x: + + * Do not close this PR + * Click _Edit_ and change the `base` to `dev` + * This CI test will remain failed until you push a new commit + + --- + + Hi @${{ github.event.pull_request.user.login }}, + + It looks like this pull-request is has been made against the [${{github.event.pull_request.head.repo.full_name }}](https://github.com/${{github.event.pull_request.head.repo.full_name }}) `master` branch. + The `master` branch on nf-core repositories should always contain code from the latest release. + Because of this, PRs to `master` are only allowed if they come from the [${{github.event.pull_request.head.repo.full_name }}](https://github.com/${{github.event.pull_request.head.repo.full_name }}) `dev` branch. + + You do not need to close this PR, you can change the target branch to `dev` by clicking the _"Edit"_ button at the top of this page. + Note that even after this, the test will continue to show as failing until you push a new commit. + + Thanks again for your contribution! + repo-token: ${{ secrets.GITHUB_TOKEN }} + allow-repeats: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..879f513 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,59 @@ +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 + +concurrency: + group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}" + cancel-in-progress: true + +jobs: + test: + name: Run pipeline with test data + # 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/deepmodeloptim') }}" + runs-on: ubuntu-latest + strategy: + matrix: + NXF_VER: + - "24.04.2" + - "latest-everything" + steps: + - name: Check out pipeline code + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + + - name: Install Nextflow + uses: nf-core/setup-nextflow@v2 + with: + version: "${{ matrix.NXF_VER }}" + + - name: Disk space cleanup + uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 + + - name: Run pipeline with test data (docker) + # TODO nf-core: You can customise CI pipeline run tests as required + # For example: adding multiple test runs with different parameters + # Remember that you can parallelise this by using strategy.matrix + run: | + nextflow run ${GITHUB_WORKSPACE} -profile test,docker --outdir ./results + + - name: Run pipeline with test data (singularity) + # TODO nf-core: You can customise CI pipeline run tests as required + run: | + nextflow run ${GITHUB_WORKSPACE} -profile test,singularity --outdir ./results + if: "${{ github.base_ref == 'master' }}" + + - name: Run pipeline with test data (conda) + # TODO nf-core: You can customise CI pipeline run tests as required + run: | + nextflow run ${GITHUB_WORKSPACE} -profile test,conda --outdir ./results + if: "${{ github.base_ref == 'master' }}" diff --git a/.github/workflows/clean-up.yml b/.github/workflows/clean-up.yml new file mode 100644 index 0000000..0b6b1f2 --- /dev/null +++ b/.github/workflows/clean-up.yml @@ -0,0 +1,24 @@ +name: "Close user-tagged issues and PRs" +on: + schedule: + - cron: "0 0 * * 0" # Once a week + +jobs: + clean-up: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # 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." + close-issue-message: "This issue was closed because it has been tagged as awaiting-changes or awaiting-feedback by an nf-core contributor and then staled for 20 days with no activity." + days-before-stale: 30 + days-before-close: 20 + days-before-pr-close: -1 + any-of-labels: "awaiting-changes,awaiting-feedback" + exempt-issue-labels: "WIP" + exempt-pr-labels: "WIP" + repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/download_pipeline.yml b/.github/workflows/download_pipeline.yml new file mode 100644 index 0000000..63f7dcc --- /dev/null +++ b/.github/workflows/download_pipeline.yml @@ -0,0 +1,120 @@ +name: Test successful pipeline download with 'nf-core pipelines download' + +# Run the workflow when: +# - dispatched manually +# - when a PR is opened or reopened to master branch +# - the head branch of the pull request is updated, i.e. if fixes for a release are pushed last minute to dev. +on: + workflow_dispatch: + inputs: + testbranch: + description: "The specific branch you wish to utilize for the test execution of nf-core pipelines download." + required: true + default: "dev" + pull_request: + types: + - opened + - edited + - synchronize + branches: + - master + pull_request_target: + branches: + - master + +env: + NXF_ANSI_LOG: false + +jobs: + download: + runs-on: ubuntu-latest + steps: + - name: Install Nextflow + uses: nf-core/setup-nextflow@v2 + + - name: Disk space cleanup + uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 + + - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 + with: + python-version: "3.12" + architecture: "x64" + + - name: Setup Apptainer + uses: eWaterCycle/setup-apptainer@4bb22c52d4f63406c49e94c804632975787312b3 # v2.0.0 + with: + apptainer-version: 1.3.4 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install git+https://github.com/nf-core/tools.git@dev + + - name: Get the repository name and current branch set as environment variable + run: | + echo "REPO_LOWERCASE=${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV} + echo "REPOTITLE_LOWERCASE=$(basename ${GITHUB_REPOSITORY,,})" >> ${GITHUB_ENV} + echo "REPO_BRANCH=${{ github.event.inputs.testbranch || 'dev' }}" >> ${GITHUB_ENV} + + - name: Make a cache directory for the container images + run: | + mkdir -p ./singularity_container_images + + - name: Download the pipeline + env: + NXF_SINGULARITY_CACHEDIR: ./singularity_container_images + run: | + nf-core pipelines download ${{ env.REPO_LOWERCASE }} \ + --revision ${{ env.REPO_BRANCH }} \ + --outdir ./${{ env.REPOTITLE_LOWERCASE }} \ + --compress "none" \ + --container-system 'singularity' \ + --container-library "quay.io" -l "docker.io" -l "community.wave.seqera.io" \ + --container-cache-utilisation 'amend' \ + --download-configuration 'yes' + + - name: Inspect download + run: tree ./${{ env.REPOTITLE_LOWERCASE }} + + - name: Count the downloaded number of container images + id: count_initial + run: | + image_count=$(ls -1 ./singularity_container_images | wc -l | xargs) + echo "Initial container image count: $image_count" + echo "IMAGE_COUNT_INITIAL=$image_count" >> ${GITHUB_ENV} + + - name: Run the downloaded pipeline (stub) + id: stub_run_pipeline + continue-on-error: true + env: + NXF_SINGULARITY_CACHEDIR: ./singularity_container_images + NXF_SINGULARITY_HOME_MOUNT: true + run: nextflow run ./${{ env.REPOTITLE_LOWERCASE }}/$( sed 's/\W/_/g' <<< ${{ env.REPO_BRANCH }}) -stub -profile test,singularity --outdir ./results + - name: Run the downloaded pipeline (stub run not supported) + id: run_pipeline + if: ${{ job.steps.stub_run_pipeline.status == failure() }} + env: + NXF_SINGULARITY_CACHEDIR: ./singularity_container_images + NXF_SINGULARITY_HOME_MOUNT: true + run: nextflow run ./${{ env.REPOTITLE_LOWERCASE }}/$( sed 's/\W/_/g' <<< ${{ env.REPO_BRANCH }}) -profile test,singularity --outdir ./results + + - name: Count the downloaded number of container images + id: count_afterwards + run: | + image_count=$(ls -1 ./singularity_container_images | wc -l | xargs) + echo "Post-pipeline run container image count: $image_count" + echo "IMAGE_COUNT_AFTER=$image_count" >> ${GITHUB_ENV} + + - name: Compare container image counts + run: | + if [ "${{ env.IMAGE_COUNT_INITIAL }}" -ne "${{ env.IMAGE_COUNT_AFTER }}" ]; then + initial_count=${{ env.IMAGE_COUNT_INITIAL }} + final_count=${{ env.IMAGE_COUNT_AFTER }} + difference=$((final_count - initial_count)) + echo "$difference additional container images were \n downloaded at runtime . The pipeline has no support for offline runs!" + tree ./singularity_container_images + exit 1 + else + echo "The pipeline can be downloaded successfully!" + fi + diff --git a/.github/workflows/fix-linting.yml b/.github/workflows/fix-linting.yml new file mode 100644 index 0000000..4939a8e --- /dev/null +++ b/.github/workflows/fix-linting.yml @@ -0,0 +1,89 @@ +name: Fix linting from a comment +on: + issue_comment: + types: [created] + +jobs: + fix-linting: + # Only run if comment is on a PR with the main repo, and if it contains the magic keywords + if: > + contains(github.event.comment.html_url, '/pull/') && + contains(github.event.comment.body, '@nf-core-bot fix linting') && + github.repository == 'nf-core/deepmodeloptim' + runs-on: ubuntu-latest + steps: + # Use the @nf-core-bot token to check out so we can push later + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + 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 + with: + comment-id: ${{ github.event.comment.id }} + reactions: eyes + + # Action runs on the issue comment, so we don't get the PR by default + # Use the gh cli to check out the PR + - name: Checkout Pull Request + run: gh pr checkout ${{ github.event.issue.number }} + env: + GITHUB_TOKEN: ${{ secrets.nf_core_bot_auth_token }} + + # Install and run pre-commit + - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 + with: + python-version: "3.12" + + - name: Install pre-commit + run: pip install pre-commit + + - name: Run pre-commit + id: pre-commit + run: pre-commit run --all-files + continue-on-error: true + + # 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 + with: + comment-id: ${{ github.event.comment.id }} + reactions: "+1" + + - name: Commit & push changes + id: commit-and-push + if: steps.pre-commit.outcome == 'failure' + run: | + git config user.email "core@nf-co.re" + git config user.name "nf-core-bot" + git config push.default upstream + git add . + git status + git commit -m "[automated] Fix code linting" + git push + + - name: react if linting errors were fixed + id: react-if-fixed + if: steps.commit-and-push.outcome == 'success' + uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4 + 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 + 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 + with: + issue-number: ${{ github.event.issue.number }} + body: | + @${{ github.actor }} I tried to fix the linting errors, but it didn't work. Please fix them manually. + See [CI log](https://github.com/nf-core/deepmodeloptim/actions/runs/${{ github.run_id }}) for more details. diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml new file mode 100644 index 0000000..b882838 --- /dev/null +++ b/.github/workflows/linting.yml @@ -0,0 +1,83 @@ +name: nf-core linting +# This workflow is triggered on pushes and PRs to the repository. +# 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] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 + with: + python-version: "3.12" + + - name: Install pre-commit + run: pip install pre-commit + + - name: Run pre-commit + run: pre-commit run --all-files + + nf-core: + runs-on: ubuntu-latest + steps: + - name: Check out pipeline code + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + + - name: Install Nextflow + uses: nf-core/setup-nextflow@v2 + + - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 + with: + python-version: "3.12" + architecture: "x64" + + - name: read .nf-core.yml + uses: pietrobolcato/action-read-yaml@1.0.0 + id: read_yml + with: + config: ${{ github.workspace }}/.nf-core.yaml + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install nf-core==${{ steps.read_yml.outputs['nf_core_version'] }} + + - name: Run nf-core pipelines lint + if: ${{ github.base_ref != 'master' }} + env: + GITHUB_COMMENTS_URL: ${{ github.event.pull_request.comments_url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_PR_COMMIT: ${{ github.event.pull_request.head.sha }} + run: nf-core -l lint_log.txt pipelines lint --dir ${GITHUB_WORKSPACE} --markdown lint_results.md + + - name: Run nf-core pipelines lint --release + if: ${{ github.base_ref == 'master' }} + env: + GITHUB_COMMENTS_URL: ${{ github.event.pull_request.comments_url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_PR_COMMIT: ${{ github.event.pull_request.head.sha }} + run: nf-core -l lint_log.txt pipelines lint --release --dir ${GITHUB_WORKSPACE} --markdown lint_results.md + + - name: Save PR number + if: ${{ always() }} + run: echo ${{ github.event.pull_request.number }} > PR_number.txt + + - name: Upload linting log file artifact + if: ${{ always() }} + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4 + with: + name: linting-logs + path: | + lint_log.txt + lint_results.md + PR_number.txt diff --git a/.github/workflows/linting_comment.yml b/.github/workflows/linting_comment.yml new file mode 100644 index 0000000..42e519b --- /dev/null +++ b/.github/workflows/linting_comment.yml @@ -0,0 +1,28 @@ +name: nf-core linting comment +# This workflow is triggered after the linting action is complete +# It posts an automated comment to the PR, even if the PR is coming from a fork + +on: + workflow_run: + workflows: ["nf-core linting"] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Download lint results + uses: dawidd6/action-download-artifact@bf251b5aa9c2f7eeb574a96ee720e24f801b7c11 # v6 + with: + workflow: linting.yml + workflow_conclusion: completed + + - name: Get PR number + id: pr_number + run: echo "pr_number=$(cat linting-logs/PR_number.txt)" >> $GITHUB_OUTPUT + + - name: Post PR comment + uses: marocchino/sticky-pull-request-comment@331f8f5b4215f0445d3c07b4967662a32a2d3e31 # v2 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + number: ${{ steps.pr_number.outputs.pr_number }} + path: linting-logs/lint_results.md diff --git a/.github/workflows/release-announcements.yml b/.github/workflows/release-announcements.yml new file mode 100644 index 0000000..c6ba35d --- /dev/null +++ b/.github/workflows/release-announcements.yml @@ -0,0 +1,75 @@ +name: release-announcements +# Automatic release toot and tweet anouncements +on: + release: + types: [published] + workflow_dispatch: + +jobs: + toot: + runs-on: ubuntu-latest + steps: + - name: get topics and convert to hashtags + id: get_topics + run: | + echo "topics=$(curl -s https://nf-co.re/pipelines.json | jq -r '.remote_workflows[] | select(.full_name == "${{ github.repository }}") | .topics[]' | awk '{print "#"$0}' | tr '\n' ' ')" | sed 's/-//g' >> $GITHUB_OUTPUT + + - uses: rzr/fediverse-action@master + with: + access-token: ${{ secrets.MASTODON_ACCESS_TOKEN }} + host: "mstdn.science" # custom host if not "mastodon.social" (default) + # GitHub event payload + # 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 }}! + + Please see the changelog: ${{ github.event.release.html_url }} + + ${{ steps.get_topics.outputs.topics }} #nfcore #openscience #nextflow #bioinformatics + + send-tweet: + runs-on: ubuntu-latest + + steps: + - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 + with: + python-version: "3.10" + - name: Install dependencies + run: pip install tweepy==4.14.0 + - name: Send tweet + shell: python + run: | + import os + import tweepy + + client = tweepy.Client( + access_token=os.getenv("TWITTER_ACCESS_TOKEN"), + access_token_secret=os.getenv("TWITTER_ACCESS_TOKEN_SECRET"), + consumer_key=os.getenv("TWITTER_CONSUMER_KEY"), + consumer_secret=os.getenv("TWITTER_CONSUMER_SECRET"), + ) + tweet = os.getenv("TWEET") + client.create_tweet(text=tweet) + env: + TWEET: | + Pipeline release! ${{ github.repository }} v${{ github.event.release.tag_name }} - ${{ github.event.release.name }}! + + Please see the changelog: ${{ github.event.release.html_url }} + TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }} + TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }} + TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} + TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} + + bsky-post: + runs-on: ubuntu-latest + steps: + - uses: zentered/bluesky-post-action@80dbe0a7697de18c15ad22f4619919ceb5ccf597 # v0.1.0 + with: + post: | + Pipeline release! ${{ github.repository }} v${{ github.event.release.tag_name }} - ${{ github.event.release.name }}! + + Please see the changelog: ${{ github.event.release.html_url }} + env: + BSKY_IDENTIFIER: ${{ secrets.BSKY_IDENTIFIER }} + BSKY_PASSWORD: ${{ secrets.BSKY_PASSWORD }} + # diff --git a/.github/workflows/template_version_comment.yml b/.github/workflows/template_version_comment.yml new file mode 100644 index 0000000..9dea41f --- /dev/null +++ b/.github/workflows/template_version_comment.yml @@ -0,0 +1,43 @@ +name: nf-core template version comment +# This workflow is triggered on PRs to check if the pipeline template version matches the latest nf-core version. +# It posts a comment to the PR, even if it comes from a fork. + +on: pull_request_target + +jobs: + template_version: + runs-on: ubuntu-latest + steps: + - name: Check out pipeline code + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + + - name: Read template version from .nf-core.yml + uses: pietrobolcato/action-read-yaml@1.0.0 + id: read_yml + with: + config: ${{ github.workspace }}/.nf-core.yml + + - name: Install nf-core + run: | + python -m pip install --upgrade pip + pip install nf-core==${{ steps.read_yml.outputs['nf_core_version'] }} + + - name: Check nf-core outdated + id: nf_core_outdated + run: pip list --outdated | grep nf-core + + - name: Post nf-core template version comment + uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2 + if: | + ${{ steps.nf_core_outdated.outputs.stdout }} =~ 'nf-core' + with: + repo-token: ${{ secrets.NF_CORE_BOT_AUTH_TOKEN }} + allow-repeats: false + message: | + ## :warning: Newer version of the nf-core template is available. + + Your pipeline is using an old version of the nf-core template: ${{ steps.read_yml.outputs['nf_core_version'] }}. + Please update your pipeline to the latest version. + + For more documentation on how to update your pipeline, please see the [nf-core documentation](https://github.com/nf-core/tools?tab=readme-ov-file#sync-a-pipeline-with-the-template) and [Synchronisation documentation](https://nf-co.re/docs/contributing/sync). + # diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5124c9a --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.nextflow* +work/ +data/ +results/ +.DS_Store +testing/ +testing* +*.pyc diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 0000000..4611863 --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,17 @@ +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/.nf-core.yml b/.nf-core.yml new file mode 100644 index 0000000..1ef8d01 --- /dev/null +++ b/.nf-core.yml @@ -0,0 +1,33 @@ +bump_version: null +lint: + files_exist: + - conf/igenomes.config + - conf/igenomes_ignored.config + - assets/multiqc_config.yml + - conf/igenomes.config + - conf/igenomes_ignored.config + - assets/multiqc_config.yml + files_unchanged: + - .github/CONTRIBUTING.md + - assets/sendmail_template.txt + - .github/CONTRIBUTING.md + - assets/sendmail_template.txt + multiqc_config: false +nf_core_version: 3.0.0 +org_path: null +repository_type: pipeline +template: + author: Mathys Grappote + description: nf-core/deepmodeloptim is an end-to-end nextflow based pipeline for + statistically testing training procedures of machine learning models + force: true + is_nfcore: true + name: deepmodeloptim + org: nf-core + outdir: . + skip_features: + - igenomes + - multiqc + - fastqc + version: 1.0.0dev +update: null diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..9e9f0e1 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,13 @@ +repos: + - repo: https://github.com/pre-commit/mirrors-prettier + rev: "v3.1.0" + hooks: + - id: prettier + additional_dependencies: + - prettier@3.2.5 + + - repo: https://github.com/editorconfig-checker/editorconfig-checker.python + rev: "3.0.3" + hooks: + - id: editorconfig-checker + alias: ec diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..610e506 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,13 @@ + +email_template.html +adaptivecard.json +slackreport.json +.nextflow* +work/ +data/ +results/ +.DS_Store +testing/ +testing* +*.pyc +bin/ diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 0000000..c81f9a7 --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1 @@ +printWidth: 120 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..31e4309 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# nf-core/deepmodeloptim: Changelog + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## v1.0.0dev - [date] + +Initial release of nf-core/deepmodeloptim, created with the [nf-core](https://nf-co.re/) template. + +### `Added` + +### `Fixed` + +### `Dependencies` + +### `Deprecated` diff --git a/CITATIONS.md b/CITATIONS.md new file mode 100644 index 0000000..fd9fb93 --- /dev/null +++ b/CITATIONS.md @@ -0,0 +1,38 @@ +# nf-core/deepmodeloptim: Citations + +## [nf-core](https://pubmed.ncbi.nlm.nih.gov/32055031/) + +> Ewels PA, Peltzer A, Fillinger S, Patel H, Alneberg J, Wilm A, Garcia MU, Di Tommaso P, Nahnsen S. The nf-core framework for community-curated bioinformatics pipelines. Nat Biotechnol. 2020 Mar;38(3):276-278. doi: 10.1038/s41587-020-0439-x. PubMed PMID: 32055031. + +## [Nextflow](https://pubmed.ncbi.nlm.nih.gov/28398311/) + +> Di Tommaso P, Chatzou M, Floden EW, Barja PP, Palumbo E, Notredame C. Nextflow enables reproducible computational workflows. Nat Biotechnol. 2017 Apr 11;35(4):316-319. doi: 10.1038/nbt.3820. PubMed PMID: 28398311. + +## Pipeline tools + + + + + +## Software packaging/containerisation tools + +- [Anaconda](https://anaconda.com) + + > Anaconda Software Distribution. Computer software. Vers. 2-2.4.0. Anaconda, Nov. 2016. Web. + +- [Bioconda](https://pubmed.ncbi.nlm.nih.gov/29967506/) + + > Grüning B, Dale R, Sjödin A, Chapman BA, Rowe J, Tomkins-Tinch CH, Valieris R, Köster J; Bioconda Team. Bioconda: sustainable and comprehensive software distribution for the life sciences. Nat Methods. 2018 Jul;15(7):475-476. doi: 10.1038/s41592-018-0046-7. PubMed PMID: 29967506. + +- [BioContainers](https://pubmed.ncbi.nlm.nih.gov/28379341/) + + > da Veiga Leprevost F, Grüning B, Aflitos SA, Röst HL, Uszkoreit J, Barsnes H, Vaudel M, Moreno P, Gatto L, Weber J, Bai M, Jimenez RC, Sachsenberg T, Pfeuffer J, Alvarez RV, Griss J, Nesvizhskii AI, Perez-Riverol Y. BioContainers: an open-source and community-driven framework for software standardization. Bioinformatics. 2017 Aug 15;33(16):2580-2582. doi: 10.1093/bioinformatics/btx192. PubMed PMID: 28379341; PubMed Central PMCID: PMC5870671. + +- [Docker](https://dl.acm.org/doi/10.5555/2600239.2600241) + + > Merkel, D. (2014). Docker: lightweight linux containers for consistent development and deployment. Linux Journal, 2014(239), 2. doi: 10.5555/2600239.2600241. + +- [Singularity](https://pubmed.ncbi.nlm.nih.gov/28494014/) + + > Kurtzer GM, Sochat V, Bauer MW. Singularity: Scientific containers for mobility of compute. PLoS One. 2017 May 11;12(5):e0177459. doi: 10.1371/journal.pone.0177459. eCollection 2017. PubMed PMID: 28494014; PubMed Central PMCID: PMC5426675. + > diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..c089ec7 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,182 @@ +# Code of Conduct at nf-core (v1.4) + +## Our Pledge + +In the interest of fostering an open, collaborative, and welcoming environment, we as contributors and maintainers of nf-core pledge to making participation in our projects and community a harassment-free experience for everyone, regardless of: + +- Age +- Ability +- Body size +- Caste +- Familial status +- Gender identity and expression +- Geographical location +- Level of experience +- Nationality and national origins +- Native language +- Neurodiversity +- Race or ethnicity +- Religion +- Sexual identity and orientation +- Socioeconomic status + +Please note that the list above is alphabetised and is therefore not ranked in any order of preference or importance. + +## Preamble + +:::note +This Code of Conduct (CoC) has been drafted by Renuka Kudva, Cris Tuñí, and Michael Heuer, with input from the nf-core Core Team and Susanna Marquez from the nf-core community. "We", in this document, refers to the Safety Officers and members of the nf-core Core Team, both of whom are deemed to be members of the nf-core community and are therefore required to abide by this Code of Conduct. This document will be amended periodically to keep it up-to-date. In case of any dispute, the most current version will apply. +::: + +An up-to-date list of members of the nf-core core team can be found [here](https://nf-co.re/about). + +Our Safety Officers are Saba Nafees, Cris Tuñí, and Michael Heuer. + +nf-core is a young and growing community that welcomes contributions from anyone with a shared vision for [Open Science Policies](https://www.fosteropenscience.eu/taxonomy/term/8). Open science policies encompass inclusive behaviours and we strive to build and maintain a safe and inclusive environment for all individuals. + +We have therefore adopted this CoC, which we require all members of our community and attendees of nf-core events to adhere to in all our workspaces at all times. Workspaces include, but are not limited to, Slack, meetings on Zoom, gather.town, YouTube live etc. + +Our CoC will be strictly enforced and the nf-core team reserves the right to exclude participants who do not comply with our guidelines from our workspaces and future nf-core activities. + +We ask all members of our community to help maintain supportive and productive workspaces and to avoid behaviours that can make individuals feel unsafe or unwelcome. Please help us maintain and uphold this CoC. + +Questions, concerns, or ideas on what we can include? Contact members of the Safety Team on Slack or email safety [at] nf-co [dot] re. + +## Our Responsibilities + +Members of the Safety Team (the Safety Officers) are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behaviour. + +The Safety Team, in consultation with the nf-core core team, have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this CoC, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +Members of the core team or the Safety Team who violate the CoC will be required to recuse themselves pending investigation. They will not have access to any reports of the violations and will be subject to the same actions as others in violation of the CoC. + +## When and where does this Code of Conduct apply? + +Participation in the nf-core community is contingent on following these guidelines in all our workspaces and events, such as hackathons, workshops, bytesize, and collaborative workspaces on gather.town. These guidelines include, but are not limited to, the following (listed alphabetically and therefore in no order of preference): + +- Communicating with an official project email address. +- Communicating with community members within the nf-core Slack channel. +- Participating in hackathons organised by nf-core (both online and in-person events). +- Participating in collaborative work on GitHub, Google Suite, community calls, mentorship meetings, email correspondence, and on the nf-core gather.town workspace. +- Participating in workshops, training, and seminar series organised by nf-core (both online and in-person events). This applies to events hosted on web-based platforms such as Zoom, gather.town, Jitsi, YouTube live etc. +- Representing nf-core on social media. This includes both official and personal accounts. + +## nf-core cares 😊 + +nf-core's CoC and expectations of respectful behaviours for all participants (including organisers and the nf-core team) include, but are not limited to, the following (listed in alphabetical order): + +- Ask for consent before sharing another community member’s personal information (including photographs) on social media. +- Be respectful of differing viewpoints and experiences. We are all here to learn from one another and a difference in opinion can present a good learning opportunity. +- Celebrate your accomplishments! (Get creative with your use of emojis 🎉 🥳 💯 🙌 !) +- Demonstrate empathy towards other community members. (We don’t all have the same amount of time to dedicate to nf-core. If tasks are pending, don’t hesitate to gently remind members of your team. If you are leading a task, ask for help if you feel overwhelmed.) +- Engage with and enquire after others. (This is especially important given the geographically remote nature of the nf-core community, so let’s do this the best we can) +- Focus on what is best for the team and the community. (When in doubt, ask) +- Accept feedback, yet be unafraid to question, deliberate, and learn. +- Introduce yourself to members of the community. (We’ve all been outsiders and we know that talking to strangers can be hard for some, but remember we’re interested in getting to know you and your visions for open science!) +- Show appreciation and **provide clear feedback**. (This is especially important because we don’t see each other in person and it can be harder to interpret subtleties. Also remember that not everyone understands a certain language to the same extent as you do, so **be clear in your communication to be kind.**) +- Take breaks when you feel like you need them. +- Use welcoming and inclusive language. (Participants are encouraged to display their chosen pronouns on Zoom or in communication on Slack) + +## nf-core frowns on 😕 + +The following behaviours from any participants within the nf-core community (including the organisers) will be considered unacceptable under this CoC. Engaging or advocating for any of the following could result in expulsion from nf-core workspaces: + +- Deliberate intimidation, stalking or following and sustained disruption of communication among participants of the community. This includes hijacking shared screens through actions such as using the annotate tool in conferencing software such as Zoom. +- “Doxing” i.e. posting (or threatening to post) another person’s personal identifying information online. +- Spamming or trolling of individuals on social media. +- Use of sexual or discriminatory imagery, comments, jokes, or unwelcome sexual attention. +- Verbal and text comments that reinforce social structures of domination related to gender, gender identity and expression, sexual orientation, ability, physical appearance, body size, race, age, religion, or work experience. + +### Online Trolling + +The majority of nf-core interactions and events are held online. Unfortunately, holding events online comes with the risk of online trolling. This is unacceptable — reports of such behaviour will be taken very seriously and perpetrators will be excluded from activities immediately. + +All community members are **required** to ask members of the group they are working with for explicit consent prior to taking screenshots of individuals during video calls. + +## Procedures for reporting CoC violations + +If someone makes you feel uncomfortable through their behaviours or actions, report it as soon as possible. + +You can reach out to members of the Safety Team (Saba Nafees, Cris Tuñí, and Michael Heuer) on Slack. Alternatively, contact a member of the nf-core core team [nf-core core team](https://nf-co.re/about), and they will forward your concerns to the Safety Team. + +Issues directly concerning members of the Core Team or the Safety Team will be dealt with by other members of the core team and the safety manager — possible conflicts of interest will be taken into account. nf-core is also in discussions about having an ombudsperson and details will be shared in due course. + +All reports will be handled with the utmost discretion and confidentiality. + +You can also report any CoC violations to safety [at] nf-co [dot] re. In your email report, please do your best to include: + +- Your contact information. +- Identifying information (e.g. names, nicknames, pseudonyms) of the participant who has violated the Code of Conduct. +- The behaviour that was in violation and the circumstances surrounding the incident. +- The approximate time of the behaviour (if different than the time the report was made). +- Other people involved in the incident, if applicable. +- If you believe the incident is ongoing. +- If there is a publicly available record (e.g. mailing list record, a screenshot). +- Any additional information. + +After you file a report, one or more members of our Safety Team will contact you to follow up on your report. + +## Who will read and handle reports + +All reports will be read and handled by the members of the Safety Team at nf-core. + +If members of the Safety Team are deemed to have a conflict of interest with a report, they will be required to recuse themselves as per our Code of Conduct and will not have access to any follow-ups. + +To keep this first report confidential from any of the Safety Team members, please submit your first report by direct messaging on Slack/direct email to any of the nf-core members you are comfortable disclosing the information to, and be explicit about which member(s) you do not consent to sharing the information with. + +## Reviewing reports + +After receiving the report, members of the Safety Team will review the incident report to determine whether immediate action is required, for example, whether there is immediate threat to participants’ safety. + +The Safety Team, in consultation with members of the nf-core core team, will assess the information to determine whether the report constitutes a Code of Conduct violation, for them to decide on a course of action. + +In the case of insufficient information, one or more members of the Safety Team may contact the reporter, the reportee, or any other attendees to obtain more information. + +Once additional information is gathered, the Safety Team will collectively review and decide on the best course of action to take, if any. The Safety Team reserves the right to not act on a report. + +## Confidentiality + +All reports, and any additional information included, are only shared with the team of safety officers (and possibly members of the core team, in case the safety officer is in violation of the CoC). We will respect confidentiality requests for the purpose of protecting victims of abuse. + +We will not name harassment victims, beyond discussions between the safety officer and members of the nf-core team, without the explicit consent of the individuals involved. + +## Enforcement + +Actions taken by the nf-core’s Safety Team may include, but are not limited to: + +- Asking anyone to stop a behaviour. +- Asking anyone to leave the event and online spaces either temporarily, for the remainder of the event, or permanently. +- Removing access to the gather.town and Slack, either temporarily or permanently. +- Communicating to all participants to reinforce our expectations for conduct and remind what is unacceptable behaviour; this may be public for practical reasons. +- Communicating to all participants that an incident has taken place and how we will act or have acted — this may be for the purpose of letting event participants know we are aware of and dealing with the incident. +- Banning anyone from participating in nf-core-managed spaces, future events, and activities, either temporarily or permanently. +- No action. + +## Attribution and Acknowledgements + +- The [Contributor Covenant, version 1.4](http://contributor-covenant.org/version/1/4) +- The [OpenCon 2017 Code of Conduct](http://www.opencon2017.org/code_of_conduct) (CC BY 4.0 OpenCon organisers, SPARC and Right to Research Coalition) +- The [eLife innovation sprint 2020 Code of Conduct](https://sprint.elifesciences.org/code-of-conduct/) +- The [Mozilla Community Participation Guidelines v3.1](https://www.mozilla.org/en-US/about/governance/policies/participation/) (version 3.1, CC BY-SA 3.0 Mozilla) + +## Changelog + +### v1.4 - February 8th, 2022 + +- Included a new member of the Safety Team. Corrected a typographical error in the text. + +### v1.3 - December 10th, 2021 + +- Added a statement that the CoC applies to nf-core gather.town workspaces. Corrected typographical errors in the text. + +### v1.2 - November 12th, 2021 + +- Removed information specific to reporting CoC violations at the Hackathon in October 2021. + +### v1.1 - October 14th, 2021 + +- Updated with names of new Safety Officers and specific information for the hackathon in October 2021. + +### v1.0 - March 15th, 2021 + +- Complete rewrite from original [Contributor Covenant](http://contributor-covenant.org/) CoC. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..58fbc71 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Mathys Grappote + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c10cb51 --- /dev/null +++ b/README.md @@ -0,0 +1,109 @@ +

+ + + nf-core/deepmodeloptim + +

+ +[![GitHub Actions CI Status](https://github.com/nf-core/deepmodeloptim/actions/workflows/ci.yml/badge.svg)](https://github.com/nf-core/deepmodeloptim/actions/workflows/ci.yml) +[![GitHub Actions Linting Status](https://github.com/nf-core/deepmodeloptim/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/deepmodeloptim/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/deepmodeloptim/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/) +[![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/deepmodeloptim) + +[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23deepmodeloptim-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/deepmodeloptim)[![Follow on Twitter](http://img.shields.io/badge/twitter-%40nf__core-1DA1F2?labelColor=000000&logo=twitter)](https://twitter.com/nf_core)[![Follow on Mastodon](https://img.shields.io/badge/mastodon-nf__core-6364ff?labelColor=FFFFFF&logo=mastodon)](https://mstdn.science/@nf_core)[![Watch on YouTube](http://img.shields.io/badge/youtube-nf--core-FF0000?labelColor=000000&logo=youtube)](https://www.youtube.com/c/nf-core) + +## Introduction + +**nf-core/deepmodeloptim** is a bioinformatics pipeline that ... + + + + + + + + + +## 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. + + + +Now, you can run the pipeline using: + + + +```bash +nextflow run nf-core/deepmodeloptim \ + -profile \ + --input samplesheet.csv \ + --outdir +``` + +> [!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/deepmodeloptim/usage) and the [parameter documentation](https://nf-co.re/deepmodeloptim/parameters). + +## Pipeline output + +To see the results of an example test run with a full size dataset refer to the [results](https://nf-co.re/deepmodeloptim/results) tab on the nf-core website pipeline page. +For more details about the output files and reports, please refer to the +[output documentation](https://nf-co.re/deepmodeloptim/output). + +## Credits + +nf-core/deepmodeloptim was originally written by Mathys Grappote. + +We thank the following people for their extensive assistance in the development of this pipeline: + + + +## Contributions and Support + +If you would like to contribute to this pipeline, please see the [contributing guidelines](.github/CONTRIBUTING.md). + +For further information or help, don't hesitate to get in touch on the [Slack `#deepmodeloptim` channel](https://nfcore.slack.com/channels/deepmodeloptim) (you can join with [this invite](https://nf-co.re/join/slack)). + +## Citations + + + + + + +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: + +> **The nf-core framework for community-curated bioinformatics pipelines.** +> +> Philip Ewels, Alexander Peltzer, Sven Fillinger, Harshil Patel, Johannes Alneberg, Andreas Wilm, Maxime Ulysse Garcia, Paolo Di Tommaso & Sven Nahnsen. +> +> _Nat Biotechnol._ 2020 Feb 13. doi: [10.1038/s41587-020-0439-x](https://dx.doi.org/10.1038/s41587-020-0439-x). diff --git a/assets/adaptivecard.json b/assets/adaptivecard.json new file mode 100644 index 0000000..40838b8 --- /dev/null +++ b/assets/adaptivecard.json @@ -0,0 +1,67 @@ +{ + "type": "message", + "attachments": [ + { + "contentType": "application/vnd.microsoft.card.adaptive", + "contentUrl": null, + "content": { + "\$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "msteams": { + "width": "Full" + }, + "type": "AdaptiveCard", + "version": "1.2", + "body": [ + { + "type": "TextBlock", + "size": "Large", + "weight": "Bolder", + "color": "<% if (success) { %>Good<% } else { %>Attention<%} %>", + "text": "nf-core/deepmodeloptim v${version} - ${runName}", + "wrap": true + }, + { + "type": "TextBlock", + "spacing": "None", + "text": "Completed at ${dateComplete} (duration: ${duration})", + "isSubtle": true, + "wrap": true + }, + { + "type": "TextBlock", + "text": "<% if (success) { %>Pipeline completed successfully!<% } else { %>Pipeline completed with errors. The full error message was: ${errorReport}.<% } %>", + "wrap": true + }, + { + "type": "TextBlock", + "text": "The command used to launch the workflow was as follows:", + "wrap": true + }, + { + "type": "TextBlock", + "text": "${commandLine}", + "isSubtle": true, + "wrap": true + } + ], + "actions": [ + { + "type": "Action.ShowCard", + "title": "Pipeline Configuration", + "card": { + "type": "AdaptiveCard", + "\$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "body": [ + { + "type": "FactSet", + "facts": [<% out << summary.collect{ k,v -> "{\"title\": \"$k\", \"value\" : \"$v\"}"}.join(",\n") %> + ] + } + ] + } + } + ] + } + } + ] +} diff --git a/assets/email_template.html b/assets/email_template.html new file mode 100644 index 0000000..52f8cf5 --- /dev/null +++ b/assets/email_template.html @@ -0,0 +1,53 @@ + + + + + + + + nf-core/deepmodeloptim Pipeline Report + + +
+ + + +

nf-core/deepmodeloptim ${version}

+

Run Name: $runName

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

nf-core/deepmodeloptim execution completed unsuccessfully!

+

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

+

The full error message was:

+
${errorReport}
+
+ """ +} else { + out << """ +
+ nf-core/deepmodeloptim execution completed successfully! +
+ """ +} +%> + +

The workflow was completed at $dateComplete (duration: $duration)

+

The command used to launch the workflow was as follows:

+
$commandLine
+ +

Pipeline Configuration:

+ + + <% out << summary.collect{ k,v -> "" }.join("\n") %> + +
$k
$v
+ +

nf-core/deepmodeloptim

+

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

+ +
+ + + diff --git a/assets/email_template.txt b/assets/email_template.txt new file mode 100644 index 0000000..64013de --- /dev/null +++ b/assets/email_template.txt @@ -0,0 +1,39 @@ +---------------------------------------------------- + ,--./,-. + ___ __ __ __ ___ /,-._.--~\\ + |\\ | |__ __ / ` / \\ |__) |__ } { + | \\| | \\__, \\__/ | \\ |___ \\`-._,-`-, + `._,._,' + nf-core/deepmodeloptim ${version} +---------------------------------------------------- +Run Name: $runName + +<% if (success){ + out << "## nf-core/deepmodeloptim execution completed successfully! ##" +} else { + out << """#################################################### +## nf-core/deepmodeloptim execution completed unsuccessfully! ## +#################################################### +The exit status of the task that caused the workflow execution to fail was: $exitStatus. +The full error message was: + +${errorReport} +""" +} %> + + +The workflow was completed at $dateComplete (duration: $duration) + +The command used to launch the workflow was as follows: + + $commandLine + + + +Pipeline Configuration: +----------------------- +<% out << summary.collect{ k,v -> " - $k: $v" }.join("\n") %> + +-- +nf-core/deepmodeloptim +https://github.com/nf-core/deepmodeloptim diff --git a/assets/nf-core-deepmodeloptim_logo_light.png b/assets/nf-core-deepmodeloptim_logo_light.png new file mode 100644 index 0000000000000000000000000000000000000000..3dfa2cff329c2b826bb09d131f9d51a090fa1ec9 GIT binary patch literal 89335 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�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 z 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 y9XU^kA;~PvB<>r^8W!BKy2Iq literal 0 HcmV?d00001 diff --git a/assets/samplesheet.csv b/assets/samplesheet.csv new file mode 100644 index 0000000..5f653ab --- /dev/null +++ b/assets/samplesheet.csv @@ -0,0 +1,3 @@ +sample,fastq_1,fastq_2 +SAMPLE_PAIRED_END,/path/to/fastq/files/AEG588A1_S1_L002_R1_001.fastq.gz,/path/to/fastq/files/AEG588A1_S1_L002_R2_001.fastq.gz +SAMPLE_SINGLE_END,/path/to/fastq/files/AEG588A4_S4_L003_R1_001.fastq.gz, diff --git a/assets/schema_input.json b/assets/schema_input.json new file mode 100644 index 0000000..ea7bc14 --- /dev/null +++ b/assets/schema_input.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/nf-core/deepmodeloptim/master/assets/schema_input.json", + "title": "nf-core/deepmodeloptim pipeline - params.input schema", + "description": "Schema for the file provided with params.input", + "type": "array", + "items": { + "type": "object", + "properties": { + "sample": { + "type": "string", + "pattern": "^\\S+$", + "errorMessage": "Sample name must be provided and cannot contain spaces", + "meta": ["id"] + }, + "fastq_1": { + "type": "string", + "format": "file-path", + "exists": true, + "pattern": "^\\S+\\.f(ast)?q\\.gz$", + "errorMessage": "FastQ file for reads 1 must be provided, cannot contain spaces and must have extension '.fq.gz' or '.fastq.gz'" + }, + "fastq_2": { + "type": "string", + "format": "file-path", + "exists": true, + "pattern": "^\\S+\\.f(ast)?q\\.gz$", + "errorMessage": "FastQ file for reads 2 cannot contain spaces and must have extension '.fq.gz' or '.fastq.gz'" + } + }, + "required": ["sample", "fastq_1"] + } +} diff --git a/assets/sendmail_template.txt b/assets/sendmail_template.txt new file mode 100644 index 0000000..a477370 --- /dev/null +++ b/assets/sendmail_template.txt @@ -0,0 +1,31 @@ +To: $email +Subject: $subject +Mime-Version: 1.0 +Content-Type: multipart/related;boundary="nfcoremimeboundary" + +--nfcoremimeboundary +Content-Type: text/html; charset=utf-8 + +$email_html + +--nfcoremimeboundary +Content-Type: image/png;name="nf-core-deepmodeloptim_logo.png" +Content-Transfer-Encoding: base64 +Content-ID: +Content-Disposition: inline; filename="nf-core-deepmodeloptim_logo_light.png" + +<% out << new File("$projectDir/assets/nf-core-deepmodeloptim_logo_light.png"). + bytes. + encodeBase64(). + toString(). + tokenize( '\n' )*. + toList()*. + collate( 76 )*. + collect { it.join() }. + flatten(). + join( '\n' ) %> + +<% +%> + +--nfcoremimeboundary-- diff --git a/assets/slackreport.json b/assets/slackreport.json new file mode 100644 index 0000000..759b21e --- /dev/null +++ b/assets/slackreport.json @@ -0,0 +1,34 @@ +{ + "attachments": [ + { + "fallback": "Plain-text summary of the attachment.", + "color": "<% if (success) { %>good<% } else { %>danger<%} %>", + "author_name": "nf-core/deepmodeloptim ${version} - ${runName}", + "author_icon": "https://www.nextflow.io/docs/latest/_static/favicon.ico", + "text": "<% if (success) { %>Pipeline completed successfully!<% } else { %>Pipeline completed with errors<% } %>", + "fields": [ + { + "title": "Command used to launch the workflow", + "value": "```${commandLine}```", + "short": false + } + <% + if (!success) { %> + , + { + "title": "Full error message", + "value": "```${errorReport}```", + "short": false + }, + { + "title": "Pipeline configuration", + "value": "<% out << summary.collect{ k,v -> k == "hook_url" ? "_${k}_: (_hidden_)" : ( ( v.class.toString().contains('Path') || ( v.class.toString().contains('String') && v.contains('/') ) ) ? "_${k}_: `${v}`" : (v.class.toString().contains('DateTime') ? ("_${k}_: " + v.format(java.time.format.DateTimeFormatter.ofLocalizedDateTime(java.time.format.FormatStyle.MEDIUM))) : "_${k}_: ${v}") ) }.join(",\n") %>", + "short": false + } + <% } + %> + ], + "footer": "Completed at <% out << dateComplete.format(java.time.format.DateTimeFormatter.ofLocalizedDateTime(java.time.format.FormatStyle.MEDIUM)) %> (duration: ${duration})" + } + ] +} diff --git a/conf/base.config b/conf/base.config new file mode 100644 index 0000000..87d7b57 --- /dev/null +++ b/conf/base.config @@ -0,0 +1,62 @@ +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + nf-core/deepmodeloptim Nextflow base config file +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + A 'blank slate' config file, appropriate for general use on most high performance + compute environments. Assumes that all software is installed and available on + the PATH. Runs in `local` mode - all jobs will be run on the logged in environment. +---------------------------------------------------------------------------------------- +*/ + +process { + + // TODO nf-core: Check the defaults for all processes + cpus = { 1 * task.attempt } + memory = { 6.GB * task.attempt } + time = { 4.h * task.attempt } + + errorStrategy = { task.exitStatus in ((130..145) + 104) ? 'retry' : 'finish' } + maxRetries = 1 + maxErrors = '-1' + + // Process-specific resource requirements + // NOTE - Please try and re-use the labels below as much as possible. + // These labels are used and recognised by default in DSL2 files hosted on nf-core/modules. + // If possible, it would be nice to keep the same label naming convention when + // adding in your local modules too. + // TODO nf-core: Customise requirements for specific processes. + // See https://www.nextflow.io/docs/latest/config.html#config-process-selectors + withLabel:process_single { + cpus = { 1 } + memory = { 6.GB * task.attempt } + time = { 4.h * task.attempt } + } + withLabel:process_low { + cpus = { 2 * task.attempt } + memory = { 12.GB * task.attempt } + time = { 4.h * task.attempt } + } + withLabel:process_medium { + cpus = { 6 * task.attempt } + memory = { 36.GB * task.attempt } + time = { 8.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 } + } + withLabel:error_ignore { + errorStrategy = 'ignore' + } + withLabel:error_retry { + errorStrategy = 'retry' + maxRetries = 2 + } +} diff --git a/conf/modules.config b/conf/modules.config new file mode 100644 index 0000000..3ca9a37 --- /dev/null +++ b/conf/modules.config @@ -0,0 +1,23 @@ +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Config file for defining DSL2 per module options and publishing paths +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Available keys to override module options: + ext.args = Additional arguments appended to command in module. + ext.args2 = Second set of arguments appended to command in module (multi-tool modules). + ext.args3 = Third set of arguments appended to command in module (multi-tool modules). + ext.prefix = File name prefix for output files. +---------------------------------------------------------------------------------------- +*/ + +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 } + ] + + + +} diff --git a/conf/test.config b/conf/test.config new file mode 100644 index 0000000..ffee398 --- /dev/null +++ b/conf/test.config @@ -0,0 +1,31 @@ +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Nextflow config file for running minimal tests +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Defines input files and everything required to run a fast and simple pipeline test. + + Use as follows: + nextflow run nf-core/deepmodeloptim -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 + // 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' + + +} diff --git a/conf/test_full.config b/conf/test_full.config new file mode 100644 index 0000000..5826280 --- /dev/null +++ b/conf/test_full.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. + + Use as follows: + nextflow run nf-core/deepmodeloptim -profile test_full, --outdir + +---------------------------------------------------------------------------------------- +*/ + +params { + config_profile_name = 'Full test profile' + config_profile_description = 'Full test dataset to check pipeline function' + + // Input data for full size test + // TODO nf-core: Specify the paths to your full test data ( on nf-core/test-datasets or directly in repositories, e.g. SRA) + // TODO nf-core: Give any required params for the test so that command line flags are not needed + input = params.pipelines_testdata_base_path + 'viralrecon/samplesheet/samplesheet_full_illumina_amplicon.csv' + + // 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/README.md b/docs/README.md new file mode 100644 index 0000000..5ab7937 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,10 @@ +# nf-core/deepmodeloptim: Documentation + +The nf-core/deepmodeloptim documentation is split into the following pages: + +- [Usage](usage.md) + - An overview of how the pipeline works, how to run it and a description of all of the different command-line flags. +- [Output](output.md) + - An overview of the different results produced by the pipeline and how to interpret them. + +You can find a lot more documentation about installing, configuring and running nf-core pipelines on the website: [https://nf-co.re](https://nf-co.re) diff --git a/docs/images/nf-core-deepmodeloptim_logo_dark.png b/docs/images/nf-core-deepmodeloptim_logo_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..49f0fecb19b0adc488d8d17261388ce5664e6def GIT binary patch literal 24583 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%0ATRNT9U&2B7j+{MRv^_FI!lbd2aARyD>{Y84GVpp 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 YDwS~?ko(;T4)}+e8C&5>vDY8{KL-##U;qFB literal 0 HcmV?d00001 diff --git a/docs/images/nf-core-deepmodeloptim_logo_light.png b/docs/images/nf-core-deepmodeloptim_logo_light.png new file mode 100644 index 0000000000000000000000000000000000000000..f988159f7ea3ca4fa1de79d7f027b7cab3e0c9d1 GIT binary patch literal 20614 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 + +## Pipeline overview + +The pipeline is built using [Nextflow](https://www.nextflow.io/) and processes data using the following steps: + + + + +- [Pipeline information](#pipeline-information) - Report metrics generated during the workflow execution + +### Pipeline information + +

+Output files + +- `pipeline_info/` + - Reports generated by Nextflow: `execution_report.html`, `execution_timeline.html`, `execution_trace.txt` and `pipeline_dag.dot`/`pipeline_dag.svg`. + - Reports generated by the pipeline: `pipeline_report.html`, `pipeline_report.txt` and `software_versions.yml`. The `pipeline_report*` files will only be present if the `--email` / `--email_on_fail` parameter's are used when running the pipeline. + - Reformatted samplesheet files used as input to the pipeline: `samplesheet.valid.csv`. + - Parameters used by the pipeline run: `params.json`. + +
+ +[Nextflow](https://www.nextflow.io/docs/latest/tracing.html) provides excellent functionality for generating various reports relevant to the running and execution of the pipeline. This will allow you to troubleshoot errors with the running of the pipeline, and also provide you with other information such as launch commands, run times and resource usage. diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..b32fbfa --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,218 @@ +# nf-core/deepmodeloptim: Usage + +## :warning: Please read this documentation on the nf-core website: [https://nf-co.re/deepmodeloptim/usage](https://nf-co.re/deepmodeloptim/usage) + +> _Documentation of pipeline parameters is generated automatically from the pipeline schema and can no longer be found in markdown files._ + +## Introduction + + + +## Samplesheet input + +You will need to create a samplesheet with information about the samples you would like to analyse before running the pipeline. Use this parameter to specify its location. It has to be a comma-separated file with 3 columns, and a header row as shown in the examples below. + +```bash +--input '[path to samplesheet file]' +``` + +### Multiple runs of the same sample + +The `sample` identifiers have to be the same when you have re-sequenced the same sample more than once e.g. to increase sequencing depth. The pipeline will concatenate the raw reads before performing any downstream analysis. Below is an example for the same sample sequenced across 3 lanes: + +```csv title="samplesheet.csv" +sample,fastq_1,fastq_2 +CONTROL_REP1,AEG588A1_S1_L002_R1_001.fastq.gz,AEG588A1_S1_L002_R2_001.fastq.gz +CONTROL_REP1,AEG588A1_S1_L003_R1_001.fastq.gz,AEG588A1_S1_L003_R2_001.fastq.gz +CONTROL_REP1,AEG588A1_S1_L004_R1_001.fastq.gz,AEG588A1_S1_L004_R2_001.fastq.gz +``` + +### Full samplesheet + +The pipeline will auto-detect whether a sample is single- or paired-end using the information provided in the samplesheet. The samplesheet can have as many columns as you desire, however, there is a strict requirement for the first 3 columns to match those defined in the table below. + +A final samplesheet file consisting of both single- and paired-end data may look something like the one below. This is for 6 samples, where `TREATMENT_REP3` has been sequenced twice. + +```csv title="samplesheet.csv" +sample,fastq_1,fastq_2 +CONTROL_REP1,AEG588A1_S1_L002_R1_001.fastq.gz,AEG588A1_S1_L002_R2_001.fastq.gz +CONTROL_REP2,AEG588A2_S2_L002_R1_001.fastq.gz,AEG588A2_S2_L002_R2_001.fastq.gz +CONTROL_REP3,AEG588A3_S3_L002_R1_001.fastq.gz,AEG588A3_S3_L002_R2_001.fastq.gz +TREATMENT_REP1,AEG588A4_S4_L003_R1_001.fastq.gz, +TREATMENT_REP2,AEG588A5_S5_L003_R1_001.fastq.gz, +TREATMENT_REP3,AEG588A6_S6_L003_R1_001.fastq.gz, +TREATMENT_REP3,AEG588A6_S6_L004_R1_001.fastq.gz, +``` + +| Column | Description | +| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `sample` | Custom sample name. This entry will be identical for multiple sequencing libraries/runs from the same sample. Spaces in sample names are automatically converted to underscores (`_`). | +| `fastq_1` | Full path to FastQ file for Illumina short reads 1. File has to be gzipped and have the extension ".fastq.gz" or ".fq.gz". | +| `fastq_2` | Full path to FastQ file for Illumina short reads 2. File has to be gzipped and have the extension ".fastq.gz" or ".fq.gz". | + +An [example samplesheet](../assets/samplesheet.csv) has been provided with the pipeline. + +## Running the pipeline + +The typical command for running the pipeline is as follows: + +```bash +nextflow run nf-core/deepmodeloptim --input ./samplesheet.csv --outdir ./results --genome GRCh37 -profile docker +``` + +This will launch the pipeline with the `docker` configuration profile. See below for more information about profiles. + +Note that the pipeline will create the following files in your working directory: + +```bash +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. +``` + +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 `. + +:::warning +Do not use `-c ` to specify parameters as this will result in errors. Custom config files specified with `-c` must only be used for [tuning process resource specifications](https://nf-co.re/docs/usage/configuration#tuning-workflow-resources), other infrastructural tweaks (such as output directories), or module arguments (args). +::: + +The above pipeline run specified with a params file in yaml format: + +```bash +nextflow run nf-core/deepmodeloptim -profile docker -params-file params.yaml +``` + +with: + +```yaml title="params.yaml" +input: './samplesheet.csv' +outdir: './results/' +genome: 'GRCh37' +<...> +``` + +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: + +```bash +nextflow pull nf-core/deepmodeloptim +``` + +### Reproducibility + +It is a good idea to specify a pipeline version when running the pipeline on your data. This ensures that a specific version of the pipeline code and software are used when you run your pipeline. If you keep using the same tag, you'll be running the same version of the pipeline, even if there have been changes to the code since. + +First, go to the [nf-core/deepmodeloptim releases page](https://github.com/nf-core/deepmodeloptim/releases) and find the latest pipeline version - numeric only (eg. `1.3.1`). Then specify this when running the pipeline with `-r` (one hyphen) - eg. `-r 1.3.1`. Of course, you can switch to another version by changing the number after the `-r` flag. + +This version number will be logged in reports when you run the pipeline, so that you'll know what you used when you look back in the future. + +To further assist in reproducbility, you can use share and re-use [parameter files](#running-the-pipeline) to repeat pipeline runs with the same settings without having to write out a command with every single parameter. + +:::tip +If you wish to share such profile (such as upload as supplementary material for academic publications), make sure to NOT include cluster specific paths to files, nor institutional specific profiles. +::: + +## Core Nextflow arguments + +:::note +These options are part of Nextflow and use a _single_ hyphen (pipeline parameters use a double-hyphen). +::: + +### `-profile` + +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. + +:::info +We highly recommend the use of Docker or Singularity containers for full pipeline reproducibility, however when this is not possible, Conda is also supported. +::: + +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 see if your system is available in these configs please see the [nf-core/configs documentation](https://github.com/nf-core/configs#documentation). + +Note that multiple profiles can be loaded, for example: `-profile test,docker` - the order of arguments is important! +They are loaded in sequence, so later profiles can overwrite earlier profiles. + +If `-profile` is not specified, the pipeline will run locally and expect all software to be installed and available on the `PATH`. This is _not_ recommended, since it can lead to different results on different machines dependent on the computer enviroment. + +- `test` + - A profile with a complete configuration for automated testing + - Includes links to test data so needs no other parameters +- `docker` + - A generic configuration profile to be used with [Docker](https://docker.com/) +- `singularity` + - A generic configuration profile to be used with [Singularity](https://sylabs.io/docs/) +- `podman` + - A generic configuration profile to be used with [Podman](https://podman.io/) +- `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/) +- `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. + +### `-resume` + +Specify this when restarting a pipeline. Nextflow will use cached results from any pipeline steps where the inputs are the same, continuing from where it got to previously. For input to be considered the same, not only the names must be identical but the files' contents as well. For more info about this parameter, see [this blog post](https://www.nextflow.io/blog/2019/demystifying-nextflow-resume.html). + +You can also supply a run name to resume a specific run: `-resume [run-name]`. Use the `nextflow log` command to show previous run names. + +### `-c` + +Specify the path to a specific config file (this is a core Nextflow command). See the [nf-core website documentation](https://nf-co.re/usage/configuration) for more information. + +## Custom configuration + +### Resource requests + +Whilst the default requirements set within the pipeline will hopefully work for most people and with most input data, you may find that you want to customise the compute resources that the pipeline requests. Each step in the pipeline has a default set of requirements for number of CPUs, memory and time. For most of the steps in the pipeline, if the job exits with any of the error codes specified [here](https://github.com/nf-core/rnaseq/blob/4c27ef5610c87db00c3c5a3eed10b1d161abf575/conf/base.config#L18) it will automatically be resubmitted with higher requests (2 x original, then 3 x original). If it still fails after the third attempt then the pipeline execution is stopped. + +To change the resource requests, please see the [max resources](https://nf-co.re/docs/usage/configuration#max-resources) and [tuning workflow resources](https://nf-co.re/docs/usage/configuration#tuning-workflow-resources) section of the nf-core website. + +### Custom Containers + +In some cases you may wish to change which container or conda environment a step of the pipeline uses for a particular tool. By default nf-core pipelines use containers and software from the [biocontainers](https://biocontainers.pro/) or [bioconda](https://bioconda.github.io/) projects. However in some cases the pipeline specified version maybe out of date. + +To use a different container from the default container or conda environment specified in a pipeline, please see the [updating tool versions](https://nf-co.re/docs/usage/configuration#updating-tool-versions) section of the nf-core website. + +### Custom Tool Arguments + +A pipeline might not always support every possible argument or option of a particular tool used in pipeline. Fortunately, nf-core pipelines provide some freedom to users to insert additional parameters that the pipeline does not include by default. + +To learn how to provide additional arguments to a particular tool of the pipeline, please see the [customising tool arguments](https://nf-co.re/docs/usage/configuration#customising-tool-arguments) section of the nf-core website. + +### nf-core/configs + +In most cases, you will only need to create a custom config as a one-off but if you and others within your organisation are likely to be running nf-core pipelines regularly and need to use the same settings regularly it may be a good idea to request that your custom config file is uploaded to the `nf-core/configs` git repository. Before you do this please can you test that the config file works with your pipeline of choice using the `-c` parameter. You can then create a pull request to the `nf-core/configs` repository with the addition of your config file, associated documentation file (see examples in [`nf-core/configs/docs`](https://github.com/nf-core/configs/tree/master/docs)), and amending [`nfcore_custom.config`](https://github.com/nf-core/configs/blob/master/nfcore_custom.config) to include your custom profile. + +See the main [Nextflow documentation](https://www.nextflow.io/docs/latest/config.html) for more information about creating your own configuration files. + +If you have any questions or issues please send us a message on [Slack](https://nf-co.re/join/slack) on the [`#configs` channel](https://nfcore.slack.com/channels/configs). + +## Running in the background + +Nextflow handles job submissions and supervises the running jobs. The Nextflow process must run until the pipeline is finished. + +The Nextflow `-bg` flag launches Nextflow in the background, detached from your terminal so that the workflow does not stop if you log out of your session. The logs are saved to a file. + +Alternatively, you can use `screen` / `tmux` or similar tool to create a detached session which you can log back into at a later time. +Some HPC setups also allow you to run nextflow within a cluster job submitted your job scheduler (from where it submits more jobs). + +## Nextflow memory requirements + +In some cases, the Nextflow Java virtual machines can start to request a large amount of memory. +We recommend adding the following line to your environment to limit this (typically in `~/.bashrc` or `~./bash_profile`): + +```bash +NXF_OPTS='-Xms1g -Xmx4g' +``` diff --git a/main.nf b/main.nf new file mode 100644 index 0000000..6a06252 --- /dev/null +++ b/main.nf @@ -0,0 +1,89 @@ +#!/usr/bin/env nextflow +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + nf-core/deepmodeloptim +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Github : https://github.com/nf-core/deepmodeloptim + Website: https://nf-co.re/deepmodeloptim + Slack : https://nfcore.slack.com/channels/deepmodeloptim +---------------------------------------------------------------------------------------- +*/ + +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + IMPORT FUNCTIONS / MODULES / SUBWORKFLOWS / WORKFLOWS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ + +include { DEEPMODELOPTIM } from './workflows/deepmodeloptim' +include { PIPELINE_INITIALISATION } from './subworkflows/local/utils_nfcore_deepmodeloptim_pipeline' +include { PIPELINE_COMPLETION } from './subworkflows/local/utils_nfcore_deepmodeloptim_pipeline' +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + NAMED WORKFLOWS FOR PIPELINE +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ + +// +// WORKFLOW: Run main analysis pipeline depending on type of input +// +workflow NFCORE_DEEPMODELOPTIM { + + take: + samplesheet // channel: samplesheet read in from --input + + main: + + // + // WORKFLOW: Run pipeline + // + DEEPMODELOPTIM ( + samplesheet + ) +} +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + RUN MAIN WORKFLOW +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ + +workflow { + + main: + // + // SUBWORKFLOW: Run initialisation tasks + // + PIPELINE_INITIALISATION ( + params.version, + params.validate_params, + params.monochrome_logs, + args, + params.outdir, + params.input + ) + + // + // WORKFLOW: Run main workflow + // + NFCORE_DEEPMODELOPTIM ( + PIPELINE_INITIALISATION.out.samplesheet + ) + // + // SUBWORKFLOW: Run completion tasks + // + PIPELINE_COMPLETION ( + params.email, + params.email_on_fail, + params.plaintext_email, + params.outdir, + params.monochrome_logs, + params.hook_url, + + ) +} + +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + THE END +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ diff --git a/modules.json b/modules.json new file mode 100644 index 0000000..fb095bf --- /dev/null +++ b/modules.json @@ -0,0 +1,31 @@ +{ + "name": "nf-core/deepmodeloptim", + "homePage": "https://github.com/nf-core/deepmodeloptim", + "repos": { + "https://github.com/nf-core/modules.git": { + "modules": { + "nf-core": { + } + }, + "subworkflows": { + "nf-core": { + "utils_nextflow_pipeline": { + "branch": "master", + "git_sha": "d20fb2a9cc3e2835e9d067d1046a63252eb17352", + "installed_by": ["subworkflows"] + }, + "utils_nfcore_pipeline": { + "branch": "master", + "git_sha": "2fdce49d30c0254f76bc0f13c55c17455c1251ab", + "installed_by": ["subworkflows"] + }, + "utils_nfschema_plugin": { + "branch": "master", + "git_sha": "bbd5a41f4535a8defafe6080e00ea74c45f4f96c", + "installed_by": ["subworkflows"] + } + } + } + } + } +} diff --git a/nextflow.config b/nextflow.config new file mode 100644 index 0000000..20d528f --- /dev/null +++ b/nextflow.config @@ -0,0 +1,254 @@ +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + nf-core/deepmodeloptim Nextflow config file +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Default config options for all compute environments +---------------------------------------------------------------------------------------- +*/ + +// Global default params, used in configs +params { + + // TODO nf-core: Specify your pipeline's command line flags + // Input options + input = null + + // Boilerplate options + outdir = null + publish_dir_mode = 'copy' + email = null + email_on_fail = null + plaintext_email = false + monochrome_logs = false + hook_url = null + help = false + help_full = false + show_hidden = false + version = false + pipelines_testdata_base_path = 'https://raw.githubusercontent.com/nf-core/test-datasets/' + // Config options + config_profile_name = null + config_profile_description = null + + custom_config_version = 'master' + custom_config_base = "https://raw.githubusercontent.com/nf-core/configs/${params.custom_config_version}" + config_profile_contact = null + config_profile_url = null + // Schema validation default options + validate_params = true + +} + +// Load base.config by default for all pipelines +includeConfig 'conf/base.config' + +profiles { + debug { + dumpHashes = true + process.beforeScript = 'echo $HOSTNAME' + cleanup = false + nextflow.enable.configProcessNamesValidation = true + } + conda { + conda.enabled = true + docker.enabled = false + singularity.enabled = false + podman.enabled = false + shifter.enabled = false + charliecloud.enabled = false + conda.channels = ['conda-forge', 'bioconda'] + apptainer.enabled = false + } + mamba { + conda.enabled = true + conda.useMamba = 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 + singularity.enabled = false + podman.enabled = false + shifter.enabled = false + charliecloud.enabled = false + apptainer.enabled = false + docker.runOptions = '-u $(id -u):$(id -g)' + } + arm { + docker.runOptions = '-u $(id -u):$(id -g) --platform=linux/amd64' + } + singularity { + singularity.enabled = true + singularity.autoMounts = true + conda.enabled = false + docker.enabled = false + podman.enabled = false + shifter.enabled = false + charliecloud.enabled = false + apptainer.enabled = false + } + podman { + podman.enabled = true + conda.enabled = false + docker.enabled = false + singularity.enabled = false + shifter.enabled = false + charliecloud.enabled = false + apptainer.enabled = false + } + shifter { + shifter.enabled = true + conda.enabled = false + docker.enabled = false + singularity.enabled = false + podman.enabled = false + charliecloud.enabled = false + apptainer.enabled = false + } + charliecloud { + charliecloud.enabled = true + conda.enabled = false + docker.enabled = false + singularity.enabled = false + podman.enabled = false + shifter.enabled = false + apptainer.enabled = false + } + apptainer { + apptainer.enabled = true + apptainer.autoMounts = true + conda.enabled = false + docker.enabled = false + singularity.enabled = false + podman.enabled = false + shifter.enabled = false + charliecloud.enabled = false + } + wave { + apptainer.ociAutoPull = true + singularity.ociAutoPull = true + wave.enabled = true + wave.freeze = true + wave.strategy = 'conda,container' + } + gitpod { + executor.name = 'local' + executor.cpus = 4 + executor.memory = 8.GB + } + 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/deepmodeloptim 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/deepmodeloptim.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 +// Set to your registry if you have a mirror of containers +apptainer.registry = 'quay.io' +docker.registry = 'quay.io' +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. + +env { + PYTHONNOUSERSITE = 1 + R_PROFILE_USER = "/.Rprofile" + R_ENVIRON_USER = "/.Renviron" + JULIA_DEPOT_PATH = "/usr/local/share/julia" +} + +// Set bash options +process.shell = """\ +bash + +set -e # Exit if a tool returns a non-zero status/exit code +set -u # Treat unset variables and parameters as an error +set -o pipefail # Returns the status of the last command to exit with a non-zero status or zero if all successfully execute +set -C # No clobber - prevent output redirection from overwriting files. +""" + +// Disable process selector warnings by default. Use debug profile to enable warnings. +nextflow.enable.configProcessNamesValidation = false + +def trace_timestamp = new java.util.Date().format( 'yyyy-MM-dd_HH-mm-ss') +timeline { + enabled = true + file = "${params.outdir}/pipeline_info/execution_timeline_${trace_timestamp}.html" +} +report { + enabled = true + file = "${params.outdir}/pipeline_info/execution_report_${trace_timestamp}.html" +} +trace { + enabled = true + file = "${params.outdir}/pipeline_info/execution_trace_${trace_timestamp}.txt" +} +dag { + enabled = true + file = "${params.outdir}/pipeline_info/pipeline_dag_${trace_timestamp}.html" +} + +manifest { + name = 'nf-core/deepmodeloptim' + author = """Mathys Grappote""" + homePage = 'https://github.com/nf-core/deepmodeloptim' + description = """nf-core/deepmodeloptim is an end-to-end nextflow based pipeline for statistically testing training procedures of machine learning models""" + mainScript = 'main.nf' + nextflowVersion = '!>=24.04.2' + version = '1.0.0dev' + doi = '' +} + +// Nextflow plugins +plugins { + id 'nf-schema@2.1.1' // Validation of pipeline parameters and creation of an input channel from a sample sheet +} + +validation { + defaultIgnoreParams = ["genomes"] + help { + enabled = true + command = "nextflow run $manifest.name -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 ${manifest.name} ${manifest.version}\033[0m +-\033[2m----------------------------------------------------\033[0m- +""" + afterText = """${manifest.doi ? "* 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/${manifest.name}/blob/master/CITATIONS.md +""" + } + summary { + beforeText = validation.help.beforeText + afterText = validation.help.afterText + } +} + +// Load modules.config for DSL2 module specific options +includeConfig 'conf/modules.config' + diff --git a/nextflow_schema.json b/nextflow_schema.json new file mode 100644 index 0000000..3f7eaba --- /dev/null +++ b/nextflow_schema.json @@ -0,0 +1,167 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/nf-core/deepmodeloptim/master/nextflow_schema.json", + "title": "nf-core/deepmodeloptim pipeline parameters", + "description": "nf-core/deepmodeloptim is an end-to-end nextflow based pipeline for statistically testing training procedures of machine learning models", + "type": "object", + "$defs": { + "input_output_options": { + "title": "Input/output options", + "type": "object", + "fa_icon": "fas fa-terminal", + "description": "Define where the pipeline should find input data and save output data.", + "required": ["input", "outdir"], + "properties": { + "input": { + "type": "string", + "format": "file-path", + "exists": true, + "schema": "assets/schema_input.json", + "mimetype": "text/csv", + "pattern": "^\\S+\\.csv$", + "description": "Path to comma-separated file containing information about the samples in the experiment.", + "help_text": "You will need to create a design file with information about the samples in your experiment before running the pipeline. Use this parameter to specify its location. It has to be a comma-separated file with 3 columns, and a header row. See [usage docs](https://nf-co.re/deepmodeloptim/usage#samplesheet-input).", + "fa_icon": "fas fa-file-csv" + }, + "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.", + "fa_icon": "fas fa-folder-open" + }, + "email": { + "type": "string", + "description": "Email address for completion summary.", + "fa_icon": "fas fa-envelope", + "help_text": "Set this parameter to your e-mail address to get a summary e-mail with details of the run sent to you when the workflow exits. If set in your user config file (`~/.nextflow/config`) then you don't need to specify this on the command line for every run.", + "pattern": "^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,5})$" + } + } + }, + "institutional_config_options": { + "title": "Institutional config options", + "type": "object", + "fa_icon": "fas fa-university", + "description": "Parameters used to describe centralised config profiles. These should not be edited.", + "help_text": "The centralised nf-core configuration profiles use a handful of pipeline parameters to describe themselves. This information is then printed to the Nextflow log when you run a pipeline. You should not need to change these values when you run a pipeline.", + "properties": { + "custom_config_version": { + "type": "string", + "description": "Git commit id for Institutional configs.", + "default": "master", + "hidden": true, + "fa_icon": "fas fa-users-cog" + }, + "custom_config_base": { + "type": "string", + "description": "Base directory for Institutional configs.", + "default": "https://raw.githubusercontent.com/nf-core/configs/master", + "hidden": true, + "help_text": "If you're running offline, Nextflow will not be able to fetch the institutional config files from the internet. If you don't need them, then this is not a problem. If you do need them, you should download the files from the repo and tell Nextflow where to find them with this parameter.", + "fa_icon": "fas fa-users-cog" + }, + "config_profile_name": { + "type": "string", + "description": "Institutional config name.", + "hidden": true, + "fa_icon": "fas fa-users-cog" + }, + "config_profile_description": { + "type": "string", + "description": "Institutional config description.", + "hidden": true, + "fa_icon": "fas fa-users-cog" + }, + "config_profile_contact": { + "type": "string", + "description": "Institutional config contact information.", + "hidden": true, + "fa_icon": "fas fa-users-cog" + }, + "config_profile_url": { + "type": "string", + "description": "Institutional config URL link.", + "hidden": true, + "fa_icon": "fas fa-users-cog" + } + } + }, + "generic_options": { + "title": "Generic options", + "type": "object", + "fa_icon": "fas fa-file-import", + "description": "Less common options for the pipeline, typically set in a config file.", + "help_text": "These options are common to all nf-core pipelines and allow you to customise some of the core preferences for how the pipeline runs.\n\nTypically these options would be set in a Nextflow config file loaded for all pipeline runs, such as `~/.nextflow/config`.", + "properties": { + "version": { + "type": "boolean", + "description": "Display version and exit.", + "fa_icon": "fas fa-question-circle", + "hidden": true + }, + "publish_dir_mode": { + "type": "string", + "default": "copy", + "description": "Method used to save pipeline results to output directory.", + "help_text": "The Nextflow `publishDir` option specifies which intermediate files should be saved to the output directory. This option tells the pipeline what method should be used to move these files. See [Nextflow docs](https://www.nextflow.io/docs/latest/process.html#publishdir) for details.", + "fa_icon": "fas fa-copy", + "enum": ["symlink", "rellink", "link", "copy", "copyNoFollow", "move"], + "hidden": true + }, + "email_on_fail": { + "type": "string", + "description": "Email address for completion summary, only when pipeline fails.", + "fa_icon": "fas fa-exclamation-triangle", + "pattern": "^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,5})$", + "help_text": "An email address to send a summary email to when the pipeline is completed - ONLY sent if the pipeline does not exit successfully.", + "hidden": true + }, + "plaintext_email": { + "type": "boolean", + "description": "Send plain-text email instead of HTML.", + "fa_icon": "fas fa-remove-format", + "hidden": true + }, + "monochrome_logs": { + "type": "boolean", + "description": "Do not use coloured log outputs.", + "fa_icon": "fas fa-palette", + "hidden": true + }, + "hook_url": { + "type": "string", + "description": "Incoming hook URL for messaging service", + "fa_icon": "fas fa-people-group", + "help_text": "Incoming hook URL for messaging service. Currently, MS Teams and Slack are supported.", + "hidden": true + }, + "validate_params": { + "type": "boolean", + "description": "Boolean whether to validate parameters against the schema at runtime", + "default": true, + "fa_icon": "fas fa-check-square", + "hidden": true + }, + "pipelines_testdata_base_path": { + "type": "string", + "fa_icon": "far fa-check-circle", + "description": "Base URL or local path to location of pipeline test dataset files", + "default": "https://raw.githubusercontent.com/nf-core/test-datasets/", + "hidden": true + } + } + } + }, + "allOf": [ + { + "$ref": "#/$defs/input_output_options" + }, + + { + "$ref": "#/$defs/institutional_config_options" + }, + { + "$ref": "#/$defs/generic_options" + } + ] +} diff --git a/subworkflows/local/utils_nfcore_deepmodeloptim_pipeline/main.nf b/subworkflows/local/utils_nfcore_deepmodeloptim_pipeline/main.nf new file mode 100644 index 0000000..23b8816 --- /dev/null +++ b/subworkflows/local/utils_nfcore_deepmodeloptim_pipeline/main.nf @@ -0,0 +1,230 @@ +// +// Subworkflow with functionality specific to the nf-core/deepmodeloptim pipeline +// + +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + IMPORT FUNCTIONS / MODULES / SUBWORKFLOWS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ + +include { UTILS_NFSCHEMA_PLUGIN } from '../../nf-core/utils_nfschema_plugin' +include { paramsSummaryMap } from 'plugin/nf-schema' +include { samplesheetToList } from 'plugin/nf-schema' +include { completionEmail } from '../../nf-core/utils_nfcore_pipeline' +include { completionSummary } from '../../nf-core/utils_nfcore_pipeline' +include { imNotification } from '../../nf-core/utils_nfcore_pipeline' +include { UTILS_NFCORE_PIPELINE } from '../../nf-core/utils_nfcore_pipeline' +include { UTILS_NEXTFLOW_PIPELINE } from '../../nf-core/utils_nextflow_pipeline' + +/* +======================================================================================== + SUBWORKFLOW TO INITIALISE PIPELINE +======================================================================================== +*/ + +workflow PIPELINE_INITIALISATION { + + take: + version // boolean: Display version and exit + validate_params // boolean: Boolean whether to validate parameters against the schema at runtime + monochrome_logs // boolean: Do not use coloured log outputs + 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 + + main: + + ch_versions = Channel.empty() + + // + // Print version and exit if required and dump pipeline parameters to JSON file + // + UTILS_NEXTFLOW_PIPELINE ( + version, + true, + outdir, + workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1 + ) + + + // + // Validate parameters and generate parameter summary to stdout + // + UTILS_NFSCHEMA_PLUGIN ( + workflow, + validate_params, + null + ) + + + // + // Check config provided to the pipeline + // + UTILS_NFCORE_PIPELINE ( + nextflow_cli_args + ) + + // + // Create channel from input file provided through params.input + // + + Channel + .fromList(samplesheetToList(params.input, "${projectDir}/assets/schema_input.json")) + .map { + meta, fastq_1, fastq_2 -> + if (!fastq_2) { + return [ meta.id, meta + [ single_end:true ], [ fastq_1 ] ] + } else { + return [ meta.id, meta + [ single_end:false ], [ fastq_1, fastq_2 ] ] + } + } + .groupTuple() + .map { samplesheet -> + validateInputSamplesheet(samplesheet) + } + .map { + meta, fastqs -> + return [ meta, fastqs.flatten() ] + } + .set { ch_samplesheet } + + emit: + samplesheet = ch_samplesheet + versions = ch_versions +} + +/* +======================================================================================== + SUBWORKFLOW FOR PIPELINE COMPLETION +======================================================================================== +*/ + +workflow PIPELINE_COMPLETION { + + take: + email // string: email address + email_on_fail // string: email address sent on pipeline failure + plaintext_email // boolean: Send plain-text email instead of HTML + + outdir // path: Path to output directory where results will be published + monochrome_logs // boolean: Disable ANSI colour codes in log output + hook_url // string: hook URL for notifications + + + main: + summary_params = paramsSummaryMap(workflow, parameters_schema: "nextflow_schema.json") + + // + // Completion email and summary + // + workflow.onComplete { + if (email || email_on_fail) { + completionEmail( + summary_params, + email, + email_on_fail, + plaintext_email, + outdir, + monochrome_logs, + [] + ) + } + + completionSummary(monochrome_logs) + if (hook_url) { + imNotification(summary_params, hook_url) + } + } + + workflow.onError { + log.error "Pipeline failed. Please refer to troubleshooting docs: https://nf-co.re/docs/usage/troubleshooting" + } +} + +/* +======================================================================================== + FUNCTIONS +======================================================================================== +*/ + +// +// Validate channels from input samplesheet +// +def validateInputSamplesheet(input) { + def (metas, fastqs) = input[1..2] + + // Check that multiple runs of the same sample are of the same datatype i.e. single-end / paired-end + def endedness_ok = metas.collect{ meta -> meta.single_end }.unique().size == 1 + if (!endedness_ok) { + error("Please check input samplesheet -> Multiple runs of a sample must be of the same datatype i.e. single-end or paired-end: ${metas[0].id}") + } + + return [ metas[0], fastqs ] +} +// +// Generate methods description for MultiQC +// +def toolCitationText() { + // TODO nf-core: Optionally add in-text citation tools to this list. + // Can use ternary operators to dynamically construct based conditions, e.g. params["run_xyz"] ? "Tool (Foo et al. 2023)" : "", + // Uncomment function in methodsDescriptionText to render in MultiQC report + def citation_text = [ + "Tools used in the workflow included:", + + + "." + ].join(' ').trim() + + return citation_text +} + +def toolBibliographyText() { + // TODO nf-core: Optionally add bibliographic entries to this list. + // Can use ternary operators to dynamically construct based conditions, e.g. params["run_xyz"] ? "
  • Author (2023) Pub name, Journal, DOI
  • " : "", + // Uncomment function in methodsDescriptionText to render in MultiQC report + def reference_text = [ + + + ].join(' ').trim() + + return reference_text +} + +def methodsDescriptionText(mqc_methods_yaml) { + // Convert to a named map so can be used as with familar NXF ${workflow} variable syntax in the MultiQC YML file + def meta = [:] + meta.workflow = workflow.toMap() + meta["manifest_map"] = workflow.manifest.toMap() + + // Pipeline DOI + if (meta.manifest_map.doi) { + // Using a loop to handle multiple DOIs + // Removing `https://doi.org/` to handle pipelines using DOIs vs DOI resolvers + // Removing ` ` since the manifest.doi is a string and not a proper list + def temp_doi_ref = "" + def manifest_doi = meta.manifest_map.doi.tokenize(",") + manifest_doi.each { doi_ref -> + temp_doi_ref += "(doi: ${doi_ref.replace("https://doi.org/", "").replace(" ", "")}), " + } + meta["doi_text"] = temp_doi_ref.substring(0, temp_doi_ref.length() - 2) + } else meta["doi_text"] = "" + meta["nodoi_text"] = meta.manifest_map.doi ? "" : "
  • If available, make sure to update the text to include the Zenodo DOI of version of the pipeline used.
  • " + + // Tool references + meta["tool_citations"] = "" + meta["tool_bibliography"] = "" + + // TODO nf-core: Only uncomment below if logic in toolCitationText/toolBibliographyText has been filled! + // meta["tool_citations"] = toolCitationText().replaceAll(", \\.", ".").replaceAll("\\. \\.", ".").replaceAll(", \\.", ".") + // meta["tool_bibliography"] = toolBibliographyText() + + + def methods_text = mqc_methods_yaml.text + + def engine = new groovy.text.SimpleTemplateEngine() + def description_html = engine.createTemplate(methods_text).make(meta) + + return description_html.toString() +} + diff --git a/subworkflows/nf-core/utils_nextflow_pipeline/main.nf b/subworkflows/nf-core/utils_nextflow_pipeline/main.nf new file mode 100644 index 0000000..28e32b2 --- /dev/null +++ b/subworkflows/nf-core/utils_nextflow_pipeline/main.nf @@ -0,0 +1,124 @@ +// +// Subworkflow with functionality that may be useful for any Nextflow pipeline +// + +/* +======================================================================================== + SUBWORKFLOW DEFINITION +======================================================================================== +*/ + +workflow UTILS_NEXTFLOW_PIPELINE { + + take: + print_version // boolean: print version + dump_parameters // boolean: dump parameters + outdir // path: base directory used to publish pipeline results + check_conda_channels // boolean: check conda channels + + main: + + // + // Print workflow version and exit on --version + // + if (print_version) { + log.info "${workflow.manifest.name} ${getWorkflowVersion()}" + System.exit(0) + } + + // + // Dump pipeline parameters to a JSON file + // + if (dump_parameters && outdir) { + dumpParametersToJSON(outdir) + } + + // + // When running with Conda, warn if channels have not been set-up appropriately + // + if (check_conda_channels) { + checkCondaChannels() + } + + emit: + dummy_emit = true +} + +/* +======================================================================================== + FUNCTIONS +======================================================================================== +*/ + +// +// Generate version string +// +def getWorkflowVersion() { + def version_string = "" as String + if (workflow.manifest.version) { + def prefix_v = workflow.manifest.version[0] != 'v' ? 'v' : '' + version_string += "${prefix_v}${workflow.manifest.version}" + } + + if (workflow.commitId) { + def git_shortsha = workflow.commitId.substring(0, 7) + version_string += "-g${git_shortsha}" + } + + return version_string +} + +// +// Dump pipeline parameters to a JSON file +// +def dumpParametersToJSON(outdir) { + def timestamp = new java.util.Date().format( 'yyyy-MM-dd_HH-mm-ss') + def filename = "params_${timestamp}.json" + def temp_pf = new File(workflow.launchDir.toString(), ".${filename}") + def jsonStr = groovy.json.JsonOutput.toJson(params) + temp_pf.text = groovy.json.JsonOutput.prettyPrint(jsonStr) + + nextflow.extension.FilesEx.copyTo(temp_pf.toPath(), "${outdir}/pipeline_info/params_${timestamp}.json") + temp_pf.delete() +} + +// +// When running with -profile conda, warn if channels have not been set-up appropriately +// +def checkCondaChannels() { + def parser = new org.yaml.snakeyaml.Yaml() + def channels = [] + try { + def config = parser.load("conda config --show channels".execute().text) + channels = config.channels + } catch(NullPointerException | IOException e) { + log.warn "Could not verify conda channel configuration." + return + } + + // Check that all channels are present + // This channel list is ordered by required channel priority. + def required_channels_in_order = ['conda-forge', 'bioconda'] + def channels_missing = ((required_channels_in_order as Set) - (channels as Set)) as Boolean + + // Check that they are in the right order + def channel_priority_violation = false + + required_channels_in_order.eachWithIndex { channel, index -> + if (index < required_channels_in_order.size() - 1) { + channel_priority_violation |= !(channels.indexOf(channel) < channels.indexOf(required_channels_in_order[index+1])) + } + } + + if (channels_missing | channel_priority_violation) { + log.warn "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + + " There is a problem with your Conda configuration!\n\n" + + " You will need to set-up the conda-forge and bioconda channels correctly.\n" + + " Please refer to https://bioconda.github.io/\n" + + " The observed channel order is \n" + + " ${channels}\n" + + " but the following channel order is required:\n" + + " ${required_channels_in_order}\n" + + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" + } +} diff --git a/subworkflows/nf-core/utils_nextflow_pipeline/meta.yml b/subworkflows/nf-core/utils_nextflow_pipeline/meta.yml new file mode 100644 index 0000000..e5c3a0a --- /dev/null +++ b/subworkflows/nf-core/utils_nextflow_pipeline/meta.yml @@ -0,0 +1,38 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/subworkflows/yaml-schema.json +name: "UTILS_NEXTFLOW_PIPELINE" +description: Subworkflow with functionality that may be useful for any Nextflow pipeline +keywords: + - utility + - pipeline + - initialise + - version +components: [] +input: + - print_version: + type: boolean + description: | + Print the version of the pipeline and exit + - dump_parameters: + type: boolean + description: | + Dump the parameters of the pipeline to a JSON file + - output_directory: + type: directory + description: Path to output dir to write JSON file to. + pattern: "results/" + - check_conda_channel: + type: boolean + description: | + Check if the conda channel priority is correct. +output: + - dummy_emit: + type: boolean + description: | + Dummy emit to make nf-core subworkflows lint happy +authors: + - "@adamrtalbot" + - "@drpatelh" +maintainers: + - "@adamrtalbot" + - "@drpatelh" + - "@maxulysse" diff --git a/subworkflows/nf-core/utils_nextflow_pipeline/tests/main.function.nf.test b/subworkflows/nf-core/utils_nextflow_pipeline/tests/main.function.nf.test new file mode 100644 index 0000000..68718e4 --- /dev/null +++ b/subworkflows/nf-core/utils_nextflow_pipeline/tests/main.function.nf.test @@ -0,0 +1,54 @@ + +nextflow_function { + + name "Test Functions" + script "subworkflows/nf-core/utils_nextflow_pipeline/main.nf" + config "subworkflows/nf-core/utils_nextflow_pipeline/tests/nextflow.config" + tag 'subworkflows' + tag 'utils_nextflow_pipeline' + tag 'subworkflows/utils_nextflow_pipeline' + + test("Test Function getWorkflowVersion") { + + function "getWorkflowVersion" + + then { + assertAll( + { assert function.success }, + { assert snapshot(function.result).match() } + ) + } + } + + test("Test Function dumpParametersToJSON") { + + function "dumpParametersToJSON" + + when { + function { + """ + // define inputs of the function here. Example: + input[0] = "$outputDir" + """.stripIndent() + } + } + + then { + assertAll( + { assert function.success } + ) + } + } + + test("Test Function checkCondaChannels") { + + function "checkCondaChannels" + + then { + assertAll( + { assert function.success }, + { assert snapshot(function.result).match() } + ) + } + } +} diff --git a/subworkflows/nf-core/utils_nextflow_pipeline/tests/main.function.nf.test.snap b/subworkflows/nf-core/utils_nextflow_pipeline/tests/main.function.nf.test.snap new file mode 100644 index 0000000..e3f0baf --- /dev/null +++ b/subworkflows/nf-core/utils_nextflow_pipeline/tests/main.function.nf.test.snap @@ -0,0 +1,20 @@ +{ + "Test Function getWorkflowVersion": { + "content": [ + "v9.9.9" + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-28T12:02:05.308243" + }, + "Test Function checkCondaChannels": { + "content": null, + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-28T12:02:12.425833" + } +} \ No newline at end of file diff --git a/subworkflows/nf-core/utils_nextflow_pipeline/tests/main.workflow.nf.test b/subworkflows/nf-core/utils_nextflow_pipeline/tests/main.workflow.nf.test new file mode 100644 index 0000000..ca964ce --- /dev/null +++ b/subworkflows/nf-core/utils_nextflow_pipeline/tests/main.workflow.nf.test @@ -0,0 +1,111 @@ +nextflow_workflow { + + name "Test Workflow UTILS_NEXTFLOW_PIPELINE" + script "../main.nf" + config "subworkflows/nf-core/utils_nextflow_pipeline/tests/nextflow.config" + workflow "UTILS_NEXTFLOW_PIPELINE" + tag 'subworkflows' + tag 'utils_nextflow_pipeline' + tag 'subworkflows/utils_nextflow_pipeline' + + test("Should run no inputs") { + + when { + workflow { + """ + print_version = false + dump_parameters = false + outdir = null + check_conda_channels = false + + input[0] = print_version + input[1] = dump_parameters + input[2] = outdir + input[3] = check_conda_channels + """ + } + } + + then { + assertAll( + { assert workflow.success } + ) + } + } + + test("Should print version") { + + when { + workflow { + """ + print_version = true + dump_parameters = false + outdir = null + check_conda_channels = false + + input[0] = print_version + input[1] = dump_parameters + input[2] = outdir + input[3] = check_conda_channels + """ + } + } + + then { + assertAll( + { assert workflow.success }, + { assert workflow.stdout.contains("nextflow_workflow v9.9.9") } + ) + } + } + + test("Should dump params") { + + when { + workflow { + """ + print_version = false + dump_parameters = true + outdir = 'results' + check_conda_channels = false + + input[0] = false + input[1] = true + input[2] = outdir + input[3] = false + """ + } + } + + then { + assertAll( + { assert workflow.success } + ) + } + } + + test("Should not create params JSON if no output directory") { + + when { + workflow { + """ + print_version = false + dump_parameters = true + outdir = null + check_conda_channels = false + + input[0] = false + input[1] = true + input[2] = outdir + input[3] = false + """ + } + } + + then { + assertAll( + { assert workflow.success } + ) + } + } +} diff --git a/subworkflows/nf-core/utils_nextflow_pipeline/tests/nextflow.config b/subworkflows/nf-core/utils_nextflow_pipeline/tests/nextflow.config new file mode 100644 index 0000000..a09572e --- /dev/null +++ b/subworkflows/nf-core/utils_nextflow_pipeline/tests/nextflow.config @@ -0,0 +1,9 @@ +manifest { + name = 'nextflow_workflow' + author = """nf-core""" + homePage = 'https://127.0.0.1' + description = """Dummy pipeline""" + nextflowVersion = '!>=23.04.0' + version = '9.9.9' + doi = 'https://doi.org/10.5281/zenodo.5070524' +} diff --git a/subworkflows/nf-core/utils_nextflow_pipeline/tests/tags.yml b/subworkflows/nf-core/utils_nextflow_pipeline/tests/tags.yml new file mode 100644 index 0000000..f847611 --- /dev/null +++ b/subworkflows/nf-core/utils_nextflow_pipeline/tests/tags.yml @@ -0,0 +1,2 @@ +subworkflows/utils_nextflow_pipeline: + - subworkflows/nf-core/utils_nextflow_pipeline/** diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/main.nf b/subworkflows/nf-core/utils_nfcore_pipeline/main.nf new file mode 100644 index 0000000..cbd8495 --- /dev/null +++ b/subworkflows/nf-core/utils_nfcore_pipeline/main.nf @@ -0,0 +1,445 @@ +// +// Subworkflow with utility functions specific to the nf-core pipeline template +// + +/* +======================================================================================== + SUBWORKFLOW DEFINITION +======================================================================================== +*/ + +workflow UTILS_NFCORE_PIPELINE { + + take: + nextflow_cli_args + + main: + valid_config = checkConfigProvided() + checkProfileProvided(nextflow_cli_args) + + emit: + valid_config +} + +/* +======================================================================================== + FUNCTIONS +======================================================================================== +*/ + +// +// Warn if a -profile or Nextflow config has not been provided to run the pipeline +// +def checkConfigProvided() { + def valid_config = true as Boolean + if (workflow.profile == 'standard' && workflow.configFiles.size() <= 1) { + log.warn "[$workflow.manifest.name] You are attempting to run the pipeline without any custom configuration!\n\n" + + "This will be dependent on your local compute environment but can be achieved via one or more of the following:\n" + + " (1) Using an existing pipeline profile e.g. `-profile docker` or `-profile singularity`\n" + + " (2) Using an existing nf-core/configs for your Institution e.g. `-profile crick` or `-profile uppmax`\n" + + " (3) Using your own local custom config e.g. `-c /path/to/your/custom.config`\n\n" + + "Please refer to the quick start section and usage docs for the pipeline.\n " + valid_config = false + } + return valid_config +} + +// +// Exit pipeline if --profile contains spaces +// +def checkProfileProvided(nextflow_cli_args) { + if (workflow.profile.endsWith(',')) { + error "The `-profile` option cannot end with a trailing comma, please remove it and re-run the pipeline!\n" + + "HINT: A common mistake is to provide multiple values separated by spaces e.g. `-profile test, docker`.\n" + } + if (nextflow_cli_args[0]) { + log.warn "nf-core pipelines do not accept positional arguments. The positional argument `${nextflow_cli_args[0]}` has been detected.\n" + + "HINT: A common mistake is to provide multiple values separated by spaces e.g. `-profile test, docker`.\n" + } +} + +// +// Citation string for pipeline +// +def workflowCitation() { + def temp_doi_ref = "" + def manifest_doi = workflow.manifest.doi.tokenize(",") + // Using a loop to handle multiple DOIs + // Removing `https://doi.org/` to handle pipelines using DOIs vs DOI resolvers + // Removing ` ` since the manifest.doi is a string and not a proper list + manifest_doi.each { doi_ref -> + temp_doi_ref += " https://doi.org/${doi_ref.replace('https://doi.org/', '').replace(' ', '')}\n" + } + return "If you use ${workflow.manifest.name} for your analysis please cite:\n\n" + + "* The pipeline\n" + + temp_doi_ref + "\n" + + "* The nf-core framework\n" + + " https://doi.org/10.1038/s41587-020-0439-x\n\n" + + "* Software dependencies\n" + + " https://github.com/${workflow.manifest.name}/blob/master/CITATIONS.md" +} + +// +// Generate workflow version string +// +def getWorkflowVersion() { + def version_string = "" as String + if (workflow.manifest.version) { + def prefix_v = workflow.manifest.version[0] != 'v' ? 'v' : '' + version_string += "${prefix_v}${workflow.manifest.version}" + } + + if (workflow.commitId) { + def git_shortsha = workflow.commitId.substring(0, 7) + version_string += "-g${git_shortsha}" + } + + return version_string +} + +// +// Get software versions for pipeline +// +def processVersionsFromYAML(yaml_file) { + def yaml = new org.yaml.snakeyaml.Yaml() + def versions = yaml.load(yaml_file).collectEntries { k, v -> [ k.tokenize(':')[-1], v ] } + return yaml.dumpAsMap(versions).trim() +} + +// +// Get workflow version for pipeline +// +def workflowVersionToYAML() { + return """ + Workflow: + $workflow.manifest.name: ${getWorkflowVersion()} + Nextflow: $workflow.nextflow.version + """.stripIndent().trim() +} + +// +// 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())) +} + +// +// Get workflow summary for MultiQC +// +def paramsSummaryMultiqc(summary_params) { + def summary_section = '' + summary_params.keySet().each { group -> + def group_params = summary_params.get(group) // This gets the parameters of that particular group + if (group_params) { + summary_section += "

    $group

    \n" + summary_section += "
    \n" + group_params.keySet().sort().each { param -> + summary_section += "
    $param
    ${group_params.get(param) ?: 'N/A'}
    \n" + } + summary_section += "
    \n" + } + } + + def yaml_file_text = "id: '${workflow.manifest.name.replace('/','-')}-summary'\n" as String + yaml_file_text += "description: ' - this information is collected when the pipeline is started.'\n" + yaml_file_text += "section_name: '${workflow.manifest.name} Workflow Summary'\n" + yaml_file_text += "section_href: 'https://github.com/${workflow.manifest.name}'\n" + yaml_file_text += "plot_type: 'html'\n" + yaml_file_text += "data: |\n" + yaml_file_text += "${summary_section}" + + return yaml_file_text +} + +// +// nf-core logo +// +def nfCoreLogo(monochrome_logs=true) { + def colors = logColours(monochrome_logs) as Map + String.format( + """\n + ${dashedLine(monochrome_logs)} + ${colors.green},--.${colors.black}/${colors.green},-.${colors.reset} + ${colors.blue} ___ __ __ __ ___ ${colors.green}/,-._.--~\'${colors.reset} + ${colors.blue} |\\ | |__ __ / ` / \\ |__) |__ ${colors.yellow}} {${colors.reset} + ${colors.blue} | \\| | \\__, \\__/ | \\ |___ ${colors.green}\\`-._,-`-,${colors.reset} + ${colors.green}`._,._,\'${colors.reset} + ${colors.purple} ${workflow.manifest.name} ${getWorkflowVersion()}${colors.reset} + ${dashedLine(monochrome_logs)} + """.stripIndent() + ) +} + +// +// Return dashed line +// +def dashedLine(monochrome_logs=true) { + def colors = logColours(monochrome_logs) as Map + return "-${colors.dim}----------------------------------------------------${colors.reset}-" +} + +// +// ANSII colours used for terminal logging +// +def logColours(monochrome_logs=true) { + def colorcodes = [:] as Map + + // Reset / Meta + colorcodes['reset'] = monochrome_logs ? '' : "\033[0m" + colorcodes['bold'] = monochrome_logs ? '' : "\033[1m" + colorcodes['dim'] = monochrome_logs ? '' : "\033[2m" + colorcodes['underlined'] = monochrome_logs ? '' : "\033[4m" + colorcodes['blink'] = monochrome_logs ? '' : "\033[5m" + colorcodes['reverse'] = monochrome_logs ? '' : "\033[7m" + colorcodes['hidden'] = monochrome_logs ? '' : "\033[8m" + + // Regular Colors + colorcodes['black'] = monochrome_logs ? '' : "\033[0;30m" + colorcodes['red'] = monochrome_logs ? '' : "\033[0;31m" + colorcodes['green'] = monochrome_logs ? '' : "\033[0;32m" + colorcodes['yellow'] = monochrome_logs ? '' : "\033[0;33m" + colorcodes['blue'] = monochrome_logs ? '' : "\033[0;34m" + colorcodes['purple'] = monochrome_logs ? '' : "\033[0;35m" + colorcodes['cyan'] = monochrome_logs ? '' : "\033[0;36m" + colorcodes['white'] = monochrome_logs ? '' : "\033[0;37m" + + // Bold + colorcodes['bblack'] = monochrome_logs ? '' : "\033[1;30m" + colorcodes['bred'] = monochrome_logs ? '' : "\033[1;31m" + colorcodes['bgreen'] = monochrome_logs ? '' : "\033[1;32m" + colorcodes['byellow'] = monochrome_logs ? '' : "\033[1;33m" + colorcodes['bblue'] = monochrome_logs ? '' : "\033[1;34m" + colorcodes['bpurple'] = monochrome_logs ? '' : "\033[1;35m" + colorcodes['bcyan'] = monochrome_logs ? '' : "\033[1;36m" + colorcodes['bwhite'] = monochrome_logs ? '' : "\033[1;37m" + + // Underline + colorcodes['ublack'] = monochrome_logs ? '' : "\033[4;30m" + colorcodes['ured'] = monochrome_logs ? '' : "\033[4;31m" + colorcodes['ugreen'] = monochrome_logs ? '' : "\033[4;32m" + colorcodes['uyellow'] = monochrome_logs ? '' : "\033[4;33m" + colorcodes['ublue'] = monochrome_logs ? '' : "\033[4;34m" + colorcodes['upurple'] = monochrome_logs ? '' : "\033[4;35m" + colorcodes['ucyan'] = monochrome_logs ? '' : "\033[4;36m" + colorcodes['uwhite'] = monochrome_logs ? '' : "\033[4;37m" + + // High Intensity + colorcodes['iblack'] = monochrome_logs ? '' : "\033[0;90m" + colorcodes['ired'] = monochrome_logs ? '' : "\033[0;91m" + colorcodes['igreen'] = monochrome_logs ? '' : "\033[0;92m" + colorcodes['iyellow'] = monochrome_logs ? '' : "\033[0;93m" + colorcodes['iblue'] = monochrome_logs ? '' : "\033[0;94m" + colorcodes['ipurple'] = monochrome_logs ? '' : "\033[0;95m" + colorcodes['icyan'] = monochrome_logs ? '' : "\033[0;96m" + colorcodes['iwhite'] = monochrome_logs ? '' : "\033[0;97m" + + // Bold High Intensity + colorcodes['biblack'] = monochrome_logs ? '' : "\033[1;90m" + colorcodes['bired'] = monochrome_logs ? '' : "\033[1;91m" + colorcodes['bigreen'] = monochrome_logs ? '' : "\033[1;92m" + colorcodes['biyellow'] = monochrome_logs ? '' : "\033[1;93m" + colorcodes['biblue'] = monochrome_logs ? '' : "\033[1;94m" + colorcodes['bipurple'] = monochrome_logs ? '' : "\033[1;95m" + colorcodes['bicyan'] = monochrome_logs ? '' : "\033[1;96m" + colorcodes['biwhite'] = monochrome_logs ? '' : "\033[1;97m" + + return colorcodes +} + +// +// Attach the multiqc report to email +// +def attachMultiqcReport(multiqc_report) { + def mqc_report = null + try { + if (workflow.success) { + mqc_report = multiqc_report.getVal() + if (mqc_report.getClass() == ArrayList && mqc_report.size() >= 1) { + if (mqc_report.size() > 1) { + log.warn "[$workflow.manifest.name] Found multiple reports from process 'MULTIQC', will use only one" + } + mqc_report = mqc_report[0] + } + } + } catch (all) { + if (multiqc_report) { + log.warn "[$workflow.manifest.name] Could not attach MultiQC report to summary email" + } + } + return mqc_report +} + +// +// Construct and send completion email +// +def completionEmail(summary_params, email, email_on_fail, plaintext_email, outdir, monochrome_logs=true, multiqc_report=null) { + + // Set up the e-mail variables + def subject = "[$workflow.manifest.name] Successful: $workflow.runName" + if (!workflow.success) { + subject = "[$workflow.manifest.name] FAILED: $workflow.runName" + } + + def summary = [:] + summary_params.keySet().sort().each { group -> + summary << summary_params[group] + } + + def misc_fields = [:] + misc_fields['Date Started'] = workflow.start + misc_fields['Date Completed'] = workflow.complete + misc_fields['Pipeline script file path'] = workflow.scriptFile + misc_fields['Pipeline script hash ID'] = workflow.scriptId + if (workflow.repository) misc_fields['Pipeline repository Git URL'] = workflow.repository + if (workflow.commitId) misc_fields['Pipeline repository Git Commit'] = workflow.commitId + if (workflow.revision) misc_fields['Pipeline Git branch/tag'] = workflow.revision + misc_fields['Nextflow Version'] = workflow.nextflow.version + misc_fields['Nextflow Build'] = workflow.nextflow.build + misc_fields['Nextflow Compile Timestamp'] = workflow.nextflow.timestamp + + def email_fields = [:] + email_fields['version'] = getWorkflowVersion() + email_fields['runName'] = workflow.runName + email_fields['success'] = workflow.success + email_fields['dateComplete'] = workflow.complete + email_fields['duration'] = workflow.duration + email_fields['exitStatus'] = workflow.exitStatus + email_fields['errorMessage'] = (workflow.errorMessage ?: 'None') + email_fields['errorReport'] = (workflow.errorReport ?: 'None') + email_fields['commandLine'] = workflow.commandLine + email_fields['projectDir'] = workflow.projectDir + email_fields['summary'] = summary << misc_fields + + // On success try attach the multiqc report + def mqc_report = attachMultiqcReport(multiqc_report) + + // Check if we are only sending emails on failure + def email_address = email + if (!email && email_on_fail && !workflow.success) { + email_address = email_on_fail + } + + // Render the TXT template + def engine = new groovy.text.GStringTemplateEngine() + def tf = new File("${workflow.projectDir}/assets/email_template.txt") + def txt_template = engine.createTemplate(tf).make(email_fields) + def email_txt = txt_template.toString() + + // Render the HTML template + def hf = new File("${workflow.projectDir}/assets/email_template.html") + def html_template = engine.createTemplate(hf).make(email_fields) + def email_html = html_template.toString() + + // Render the sendmail template + def max_multiqc_email_size = (params.containsKey('max_multiqc_email_size') ? params.max_multiqc_email_size : 0) as nextflow.util.MemoryUnit + def smail_fields = [ email: email_address, subject: subject, email_txt: email_txt, email_html: email_html, projectDir: "${workflow.projectDir}", mqcFile: mqc_report, mqcMaxSize: max_multiqc_email_size.toBytes() ] + def sf = new File("${workflow.projectDir}/assets/sendmail_template.txt") + def sendmail_template = engine.createTemplate(sf).make(smail_fields) + def sendmail_html = sendmail_template.toString() + + // Send the HTML e-mail + def colors = logColours(monochrome_logs) as Map + if (email_address) { + try { + if (plaintext_email) { throw new org.codehaus.groovy.GroovyException('Send plaintext e-mail, not HTML') } + // Try to send HTML e-mail using sendmail + def sendmail_tf = new File(workflow.launchDir.toString(), ".sendmail_tmp.html") + sendmail_tf.withWriter { w -> w << sendmail_html } + [ 'sendmail', '-t' ].execute() << sendmail_html + log.info "-${colors.purple}[$workflow.manifest.name]${colors.green} Sent summary e-mail to $email_address (sendmail)-" + } catch (all) { + // Catch failures and try with plaintext + def mail_cmd = [ 'mail', '-s', subject, '--content-type=text/html', email_address ] + mail_cmd.execute() << email_html + log.info "-${colors.purple}[$workflow.manifest.name]${colors.green} Sent summary e-mail to $email_address (mail)-" + } + } + + // Write summary e-mail HTML to a file + def output_hf = new File(workflow.launchDir.toString(), ".pipeline_report.html") + output_hf.withWriter { w -> w << email_html } + nextflow.extension.FilesEx.copyTo(output_hf.toPath(), "${outdir}/pipeline_info/pipeline_report.html"); + output_hf.delete() + + // Write summary e-mail TXT to a file + def output_tf = new File(workflow.launchDir.toString(), ".pipeline_report.txt") + output_tf.withWriter { w -> w << email_txt } + nextflow.extension.FilesEx.copyTo(output_tf.toPath(), "${outdir}/pipeline_info/pipeline_report.txt"); + output_tf.delete() +} + +// +// Print pipeline summary on completion +// +def completionSummary(monochrome_logs=true) { + def colors = logColours(monochrome_logs) as Map + if (workflow.success) { + if (workflow.stats.ignoredCount == 0) { + log.info "-${colors.purple}[$workflow.manifest.name]${colors.green} Pipeline completed successfully${colors.reset}-" + } else { + log.info "-${colors.purple}[$workflow.manifest.name]${colors.yellow} Pipeline completed successfully, but with errored process(es) ${colors.reset}-" + } + } else { + log.info "-${colors.purple}[$workflow.manifest.name]${colors.red} Pipeline completed with errors${colors.reset}-" + } +} + +// +// Construct and send a notification to a web server as JSON e.g. Microsoft Teams and Slack +// +def imNotification(summary_params, hook_url) { + def summary = [:] + summary_params.keySet().sort().each { group -> + summary << summary_params[group] + } + + def misc_fields = [:] + misc_fields['start'] = workflow.start + misc_fields['complete'] = workflow.complete + misc_fields['scriptfile'] = workflow.scriptFile + misc_fields['scriptid'] = workflow.scriptId + if (workflow.repository) misc_fields['repository'] = workflow.repository + if (workflow.commitId) misc_fields['commitid'] = workflow.commitId + if (workflow.revision) misc_fields['revision'] = workflow.revision + misc_fields['nxf_version'] = workflow.nextflow.version + misc_fields['nxf_build'] = workflow.nextflow.build + misc_fields['nxf_timestamp'] = workflow.nextflow.timestamp + + def msg_fields = [:] + msg_fields['version'] = getWorkflowVersion() + msg_fields['runName'] = workflow.runName + msg_fields['success'] = workflow.success + msg_fields['dateComplete'] = workflow.complete + msg_fields['duration'] = workflow.duration + msg_fields['exitStatus'] = workflow.exitStatus + msg_fields['errorMessage'] = (workflow.errorMessage ?: 'None') + msg_fields['errorReport'] = (workflow.errorReport ?: 'None') + msg_fields['commandLine'] = workflow.commandLine.replaceFirst(/ +--hook_url +[^ ]+/, "") + msg_fields['projectDir'] = workflow.projectDir + msg_fields['summary'] = summary << misc_fields + + // Render the JSON template + def engine = new groovy.text.GStringTemplateEngine() + // Different JSON depending on the service provider + // Defaults to "Adaptive Cards" (https://adaptivecards.io), except Slack which has its own format + def json_path = hook_url.contains("hooks.slack.com") ? "slackreport.json" : "adaptivecard.json" + def hf = new File("${workflow.projectDir}/assets/${json_path}") + def json_template = engine.createTemplate(hf).make(msg_fields) + def json_message = json_template.toString() + + // POST + def post = new URL(hook_url).openConnection(); + post.setRequestMethod("POST") + post.setDoOutput(true) + post.setRequestProperty("Content-Type", "application/json") + post.getOutputStream().write(json_message.getBytes("UTF-8")); + def postRC = post.getResponseCode(); + if (! postRC.equals(200)) { + log.warn(post.getErrorStream().getText()); + } +} diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/meta.yml b/subworkflows/nf-core/utils_nfcore_pipeline/meta.yml new file mode 100644 index 0000000..d08d243 --- /dev/null +++ b/subworkflows/nf-core/utils_nfcore_pipeline/meta.yml @@ -0,0 +1,24 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/subworkflows/yaml-schema.json +name: "UTILS_NFCORE_PIPELINE" +description: Subworkflow with utility functions specific to the nf-core pipeline template +keywords: + - utility + - pipeline + - initialise + - version +components: [] +input: + - nextflow_cli_args: + type: list + description: | + Nextflow CLI positional arguments +output: + - success: + type: boolean + description: | + Dummy output to indicate success +authors: + - "@adamrtalbot" +maintainers: + - "@adamrtalbot" + - "@maxulysse" diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.function.nf.test b/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.function.nf.test new file mode 100644 index 0000000..1dc317f --- /dev/null +++ b/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.function.nf.test @@ -0,0 +1,134 @@ + +nextflow_function { + + name "Test Functions" + script "../main.nf" + config "subworkflows/nf-core/utils_nfcore_pipeline/tests/nextflow.config" + tag "subworkflows" + tag "subworkflows_nfcore" + tag "utils_nfcore_pipeline" + tag "subworkflows/utils_nfcore_pipeline" + + test("Test Function checkConfigProvided") { + + function "checkConfigProvided" + + then { + assertAll( + { assert function.success }, + { assert snapshot(function.result).match() } + ) + } + } + + test("Test Function checkProfileProvided") { + + function "checkProfileProvided" + + when { + function { + """ + input[0] = [] + """ + } + } + + then { + assertAll( + { assert function.success }, + { assert snapshot(function.result).match() } + ) + } + } + + test("Test Function workflowCitation") { + + function "workflowCitation" + + then { + assertAll( + { assert function.success }, + { assert snapshot(function.result).match() } + ) + } + } + + test("Test Function nfCoreLogo") { + + function "nfCoreLogo" + + when { + function { + """ + input[0] = false + """ + } + } + + then { + assertAll( + { assert function.success }, + { assert snapshot(function.result).match() } + ) + } + } + + test("Test Function dashedLine") { + + function "dashedLine" + + when { + function { + """ + input[0] = false + """ + } + } + + then { + assertAll( + { assert function.success }, + { assert snapshot(function.result).match() } + ) + } + } + + test("Test Function without logColours") { + + function "logColours" + + when { + function { + """ + input[0] = true + """ + } + } + + then { + assertAll( + { assert function.success }, + { assert snapshot(function.result).match() } + ) + } + } + + test("Test Function with logColours") { + function "logColours" + + when { + function { + """ + input[0] = false + """ + } + } + + then { + assertAll( + { assert function.success }, + { assert snapshot(function.result).match() } + ) + } + } +} diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.function.nf.test.snap b/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.function.nf.test.snap new file mode 100644 index 0000000..1037232 --- /dev/null +++ b/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.function.nf.test.snap @@ -0,0 +1,166 @@ +{ + "Test Function checkProfileProvided": { + "content": null, + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-28T12:03:03.360873" + }, + "Test Function checkConfigProvided": { + "content": [ + true + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-28T12:02:59.729647" + }, + "Test Function nfCoreLogo": { + "content": [ + "\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 nextflow_workflow v9.9.9\u001b[0m\n-\u001b[2m----------------------------------------------------\u001b[0m-\n" + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-28T12:03:10.562934" + }, + "Test Function workflowCitation": { + "content": [ + "If you use nextflow_workflow for your analysis please cite:\n\n* The pipeline\n https://doi.org/10.5281/zenodo.5070524\n\n* The nf-core framework\n https://doi.org/10.1038/s41587-020-0439-x\n\n* Software dependencies\n https://github.com/nextflow_workflow/blob/master/CITATIONS.md" + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-28T12:03:07.019761" + }, + "Test Function without logColours": { + "content": [ + { + "reset": "", + "bold": "", + "dim": "", + "underlined": "", + "blink": "", + "reverse": "", + "hidden": "", + "black": "", + "red": "", + "green": "", + "yellow": "", + "blue": "", + "purple": "", + "cyan": "", + "white": "", + "bblack": "", + "bred": "", + "bgreen": "", + "byellow": "", + "bblue": "", + "bpurple": "", + "bcyan": "", + "bwhite": "", + "ublack": "", + "ured": "", + "ugreen": "", + "uyellow": "", + "ublue": "", + "upurple": "", + "ucyan": "", + "uwhite": "", + "iblack": "", + "ired": "", + "igreen": "", + "iyellow": "", + "iblue": "", + "ipurple": "", + "icyan": "", + "iwhite": "", + "biblack": "", + "bired": "", + "bigreen": "", + "biyellow": "", + "biblue": "", + "bipurple": "", + "bicyan": "", + "biwhite": "" + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-28T12:03:17.969323" + }, + "Test Function dashedLine": { + "content": [ + "-\u001b[2m----------------------------------------------------\u001b[0m-" + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-28T12:03:14.366181" + }, + "Test Function with logColours": { + "content": [ + { + "reset": "\u001b[0m", + "bold": "\u001b[1m", + "dim": "\u001b[2m", + "underlined": "\u001b[4m", + "blink": "\u001b[5m", + "reverse": "\u001b[7m", + "hidden": "\u001b[8m", + "black": "\u001b[0;30m", + "red": "\u001b[0;31m", + "green": "\u001b[0;32m", + "yellow": "\u001b[0;33m", + "blue": "\u001b[0;34m", + "purple": "\u001b[0;35m", + "cyan": "\u001b[0;36m", + "white": "\u001b[0;37m", + "bblack": "\u001b[1;30m", + "bred": "\u001b[1;31m", + "bgreen": "\u001b[1;32m", + "byellow": "\u001b[1;33m", + "bblue": "\u001b[1;34m", + "bpurple": "\u001b[1;35m", + "bcyan": "\u001b[1;36m", + "bwhite": "\u001b[1;37m", + "ublack": "\u001b[4;30m", + "ured": "\u001b[4;31m", + "ugreen": "\u001b[4;32m", + "uyellow": "\u001b[4;33m", + "ublue": "\u001b[4;34m", + "upurple": "\u001b[4;35m", + "ucyan": "\u001b[4;36m", + "uwhite": "\u001b[4;37m", + "iblack": "\u001b[0;90m", + "ired": "\u001b[0;91m", + "igreen": "\u001b[0;92m", + "iyellow": "\u001b[0;93m", + "iblue": "\u001b[0;94m", + "ipurple": "\u001b[0;95m", + "icyan": "\u001b[0;96m", + "iwhite": "\u001b[0;97m", + "biblack": "\u001b[1;90m", + "bired": "\u001b[1;91m", + "bigreen": "\u001b[1;92m", + "biyellow": "\u001b[1;93m", + "biblue": "\u001b[1;94m", + "bipurple": "\u001b[1;95m", + "bicyan": "\u001b[1;96m", + "biwhite": "\u001b[1;97m" + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-28T12:03:21.714424" + } +} \ No newline at end of file diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.workflow.nf.test b/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.workflow.nf.test new file mode 100644 index 0000000..8940d32 --- /dev/null +++ b/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.workflow.nf.test @@ -0,0 +1,29 @@ +nextflow_workflow { + + name "Test Workflow UTILS_NFCORE_PIPELINE" + script "../main.nf" + config "subworkflows/nf-core/utils_nfcore_pipeline/tests/nextflow.config" + workflow "UTILS_NFCORE_PIPELINE" + tag "subworkflows" + tag "subworkflows_nfcore" + tag "utils_nfcore_pipeline" + tag "subworkflows/utils_nfcore_pipeline" + + test("Should run without failures") { + + when { + workflow { + """ + input[0] = [] + """ + } + } + + then { + assertAll( + { assert workflow.success }, + { assert snapshot(workflow.out).match() } + ) + } + } +} diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.workflow.nf.test.snap b/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.workflow.nf.test.snap new file mode 100644 index 0000000..859d103 --- /dev/null +++ b/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.workflow.nf.test.snap @@ -0,0 +1,19 @@ +{ + "Should run without failures": { + "content": [ + { + "0": [ + true + ], + "valid_config": [ + true + ] + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-02-28T12:03:25.726491" + } +} \ No newline at end of file diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/tests/nextflow.config b/subworkflows/nf-core/utils_nfcore_pipeline/tests/nextflow.config new file mode 100644 index 0000000..d0a926b --- /dev/null +++ b/subworkflows/nf-core/utils_nfcore_pipeline/tests/nextflow.config @@ -0,0 +1,9 @@ +manifest { + name = 'nextflow_workflow' + author = """nf-core""" + homePage = 'https://127.0.0.1' + description = """Dummy pipeline""" + nextflowVersion = '!>=23.04.0' + version = '9.9.9' + doi = 'https://doi.org/10.5281/zenodo.5070524' +} diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/tests/tags.yml b/subworkflows/nf-core/utils_nfcore_pipeline/tests/tags.yml new file mode 100644 index 0000000..ac8523c --- /dev/null +++ b/subworkflows/nf-core/utils_nfcore_pipeline/tests/tags.yml @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000..4994303 --- /dev/null +++ b/subworkflows/nf-core/utils_nfschema_plugin/main.nf @@ -0,0 +1,46 @@ +// +// Subworkflow that uses the nf-schema plugin to validate parameters and render the parameter summary +// + +include { paramsSummaryLog } from 'plugin/nf-schema' +include { validateParameters } from 'plugin/nf-schema' + +workflow UTILS_NFSCHEMA_PLUGIN { + + take: + input_workflow // workflow: the workflow object used by nf-schema to get metadata from the workflow + validate_params // boolean: validate the parameters + parameters_schema // string: path to the parameters JSON schema. + // this has to be the same as the schema given to `validation.parametersSchema` + // 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 + + main: + + // + // Print parameter summary to stdout. This will display the parameters + // that differ from the default given in the JSON schema + // + if(parameters_schema) { + log.info paramsSummaryLog(input_workflow, parameters_schema:parameters_schema) + } else { + log.info paramsSummaryLog(input_workflow) + } + + // + // Validate the parameters using nextflow_schema.json or the schema + // given via the validation.parametersSchema configuration option + // + if(validate_params) { + if(parameters_schema) { + validateParameters(parameters_schema:parameters_schema) + } else { + validateParameters() + } + } + + emit: + dummy_emit = true +} + diff --git a/subworkflows/nf-core/utils_nfschema_plugin/meta.yml b/subworkflows/nf-core/utils_nfschema_plugin/meta.yml new file mode 100644 index 0000000..f7d9f02 --- /dev/null +++ b/subworkflows/nf-core/utils_nfschema_plugin/meta.yml @@ -0,0 +1,35 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/subworkflows/yaml-schema.json +name: "utils_nfschema_plugin" +description: Run nf-schema to validate parameters and create a summary of changed parameters +keywords: + - validation + - JSON schema + - plugin + - parameters + - summary +components: [] +input: + - input_workflow: + type: object + description: | + The workflow object of the used pipeline. + This object contains meta data used to create the params summary log + - validate_params: + type: boolean + description: Validate the parameters and error if invalid. + - parameters_schema: + type: string + description: | + Path to the parameters JSON schema. + This has to be the same as the schema given to the `validation.parametersSchema` config + option. When this input is empty it will automatically use the configured schema or + "${projectDir}/nextflow_schema.json" as default. The schema should not be given in this way + for meta pipelines. +output: + - dummy_emit: + type: boolean + description: Dummy emit to make nf-core subworkflows lint happy +authors: + - "@nvnieuwk" +maintainers: + - "@nvnieuwk" diff --git a/subworkflows/nf-core/utils_nfschema_plugin/tests/main.nf.test b/subworkflows/nf-core/utils_nfschema_plugin/tests/main.nf.test new file mode 100644 index 0000000..842dc43 --- /dev/null +++ b/subworkflows/nf-core/utils_nfschema_plugin/tests/main.nf.test @@ -0,0 +1,117 @@ +nextflow_workflow { + + name "Test Subworkflow UTILS_NFSCHEMA_PLUGIN" + script "../main.nf" + workflow "UTILS_NFSCHEMA_PLUGIN" + + tag "subworkflows" + tag "subworkflows_nfcore" + tag "subworkflows/utils_nfschema_plugin" + tag "plugin/nf-schema" + + config "./nextflow.config" + + test("Should run nothing") { + + when { + + params { + test_data = '' + } + + workflow { + """ + validate_params = false + input[0] = workflow + input[1] = validate_params + input[2] = "" + """ + } + } + + then { + assertAll( + { assert workflow.success } + ) + } + } + + test("Should validate params") { + + when { + + params { + test_data = '' + outdir = 1 + } + + workflow { + """ + validate_params = true + input[0] = workflow + input[1] = validate_params + input[2] = "" + """ + } + } + + then { + assertAll( + { assert workflow.failed }, + { assert workflow.stdout.any { it.contains('ERROR ~ Validation of pipeline parameters failed!') } } + ) + } + } + + test("Should run nothing - custom schema") { + + when { + + params { + test_data = '' + } + + workflow { + """ + validate_params = false + input[0] = workflow + input[1] = validate_params + input[2] = "${projectDir}/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow_schema.json" + """ + } + } + + then { + assertAll( + { assert workflow.success } + ) + } + } + + test("Should validate params - custom schema") { + + when { + + params { + test_data = '' + outdir = 1 + } + + workflow { + """ + validate_params = true + input[0] = workflow + input[1] = validate_params + input[2] = "${projectDir}/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow_schema.json" + """ + } + } + + then { + assertAll( + { assert workflow.failed }, + { assert workflow.stdout.any { it.contains('ERROR ~ Validation of pipeline parameters failed!') } } + ) + } + } +} diff --git a/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow.config b/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow.config new file mode 100644 index 0000000..0907ac5 --- /dev/null +++ b/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow.config @@ -0,0 +1,8 @@ +plugins { + id "nf-schema@2.1.0" +} + +validation { + parametersSchema = "${projectDir}/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow_schema.json" + monochromeLogs = true +} \ No newline at end of file diff --git a/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow_schema.json b/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow_schema.json new file mode 100644 index 0000000..331e0d2 --- /dev/null +++ b/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow_schema.json @@ -0,0 +1,96 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/./master/nextflow_schema.json", + "title": ". pipeline parameters", + "description": "", + "type": "object", + "$defs": { + "input_output_options": { + "title": "Input/output options", + "type": "object", + "fa_icon": "fas fa-terminal", + "description": "Define where the pipeline should find input data and save output data.", + "required": ["outdir"], + "properties": { + "validate_params": { + "type": "boolean", + "description": "Validate parameters?", + "default": true, + "hidden": true + }, + "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.", + "fa_icon": "fas fa-folder-open" + }, + "test_data_base": { + "type": "string", + "default": "https://raw.githubusercontent.com/nf-core/test-datasets/modules", + "description": "Base for test data directory", + "hidden": true + }, + "test_data": { + "type": "string", + "description": "Fake test data param", + "hidden": true + } + } + }, + "generic_options": { + "title": "Generic options", + "type": "object", + "fa_icon": "fas fa-file-import", + "description": "Less common options for the pipeline, typically set in a config file.", + "help_text": "These options are common to all nf-core pipelines and allow you to customise some of the core preferences for how the pipeline runs.\n\nTypically these options would be set in a Nextflow config file loaded for all pipeline runs, such as `~/.nextflow/config`.", + "properties": { + "help": { + "type": "boolean", + "description": "Display help text.", + "fa_icon": "fas fa-question-circle", + "hidden": true + }, + "version": { + "type": "boolean", + "description": "Display version and exit.", + "fa_icon": "fas fa-question-circle", + "hidden": true + }, + "logo": { + "type": "boolean", + "default": true, + "description": "Display nf-core logo in console output.", + "fa_icon": "fas fa-image", + "hidden": true + }, + "singularity_pull_docker_container": { + "type": "boolean", + "description": "Pull Singularity container from Docker?", + "hidden": true + }, + "publish_dir_mode": { + "type": "string", + "default": "copy", + "description": "Method used to save pipeline results to output directory.", + "help_text": "The Nextflow `publishDir` option specifies which intermediate files should be saved to the output directory. This option tells the pipeline what method should be used to move these files. See [Nextflow docs](https://www.nextflow.io/docs/latest/process.html#publishdir) for details.", + "fa_icon": "fas fa-copy", + "enum": ["symlink", "rellink", "link", "copy", "copyNoFollow", "move"], + "hidden": true + }, + "monochrome_logs": { + "type": "boolean", + "description": "Use monochrome_logs", + "hidden": true + } + } + } + }, + "allOf": [ + { + "$ref": "#/$defs/input_output_options" + }, + { + "$ref": "#/$defs/generic_options" + } + ] +} diff --git a/tower.yml b/tower.yml new file mode 100644 index 0000000..c61323c --- /dev/null +++ b/tower.yml @@ -0,0 +1,3 @@ +reports: + samplesheet.csv: + display: "Auto-created samplesheet with collated metadata and FASTQ paths" diff --git a/workflows/deepmodeloptim.nf b/workflows/deepmodeloptim.nf new file mode 100644 index 0000000..2d073ec --- /dev/null +++ b/workflows/deepmodeloptim.nf @@ -0,0 +1,49 @@ +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + IMPORT MODULES / SUBWORKFLOWS / FUNCTIONS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ + + +include { paramsSummaryMap } from 'plugin/nf-schema' + +include { softwareVersionsToYAML } from '../subworkflows/nf-core/utils_nfcore_pipeline' +include { methodsDescriptionText } from '../subworkflows/local/utils_nfcore_deepmodeloptim_pipeline' + +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + RUN MAIN WORKFLOW +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ + +workflow DEEPMODELOPTIM { + + take: + ch_samplesheet // channel: samplesheet read in from --input + main: + + ch_versions = Channel.empty() + + + // + // Collate and save software versions + // + softwareVersionsToYAML(ch_versions) + .collectFile( + storeDir: "${params.outdir}/pipeline_info", + name: 'nf_core_' + 'pipeline_software_' + '' + 'versions.yml', + sort: true, + newLine: true + ).set { ch_collated_versions } + + + emit: + versions = ch_versions // channel: [ path(versions.yml) ] + +} + +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + THE END +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ From 40f80d548c138a31c6f905154766fa93a2d4a31e Mon Sep 17 00:00:00 2001 From: JoseEspinosa Date: Thu, 17 Oct 2024 22:34:27 +0200 Subject: [PATCH 02/25] Add todos --- tests/handle_data.nf.test | 9 +++++---- tests/handle_tune.nf.test | 5 ++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/handle_data.nf.test b/tests/handle_data.nf.test index b0eb9c5..2131c7c 100644 --- a/tests/handle_data.nf.test +++ b/tests/handle_data.nf.test @@ -9,10 +9,11 @@ nextflow_workflow { when { - // make so that nf-test will see a singularity cache dir where is usally set to, again interpretation of projectdir has to be done here and not rely on an external config. - params { - singularity_cache_dir = "${projectDir}/singularity_cache" - } + // TODO Check that continues working with $NXF_SINGULARITY_CACHEDIR + // // make so that nf-test will see a singularity cache dir where is usally set to, again interpretation of projectdir has to be done here and not rely on an external config. + // params { + // singularity_cache_dir = "${projectDir}/singularity_cache" + // } // repeating in input[0-1] the same content of params.csv and parma.exp_conf present in the test.config because nf-test does not interpret the ${projectDir} correctly otherwise workflow { diff --git a/tests/handle_tune.nf.test b/tests/handle_tune.nf.test index ea97be9..45fae18 100644 --- a/tests/handle_tune.nf.test +++ b/tests/handle_tune.nf.test @@ -23,13 +23,12 @@ nextflow_workflow { // make so that nf-test will see a singularity cache dir where is usally set to, again interpretation of projectdir has to be done here and not rely on an external config. params { - singularity_cache_dir = "${projectDir}/singularity_cache" + // TODO Check that continues working with $NXF_SINGULARITY_CACHEDIR + // singularity_cache_dir = "${projectDir}/singularity_cache" debug_mode = true - max_gpus = 0 - } // repeating in input[0-1] the same content params.model of params.tune_conf present in the test.config because nf-test does not interpret the ${projectDir} correctly otherwise From 21ff6ec72cc043ce817bce9b0482580050dfbe10 Mon Sep 17 00:00:00 2001 From: JoseEspinosa Date: Thu, 17 Oct 2024 23:01:19 +0200 Subject: [PATCH 03/25] Move workflow logic from main to deepmodeloptim --- main.nf | 63 ------------------------------------- workflows/deepmodeloptim.nf | 54 ++++++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 71 deletions(-) diff --git a/main.nf b/main.nf index 8d4a202..bd68d0d 100644 --- a/main.nf +++ b/main.nf @@ -9,27 +9,6 @@ ---------------------------------------------------------------------------------------- */ -/* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - VALIDATE & PRINT PARAMETER SUMMARY -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -*/ - - - -/* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - input handling -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -*/ - - - -/* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - NAMED WORKFLOW FOR PIPELINE -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -*/ include { CHECK_MODEL } from './subworkflows/check_model.nf' include { HANDLE_DATA } from './workflows/handle_data.nf' @@ -42,48 +21,6 @@ include { HANDLE_ANALYSIS } from './workflows/handle_analysis.nf' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ - -workflow { - - CHECK_MODEL ( - params.csv, - params.exp_conf, - params.model, - params.tune_conf, - params.initial_weights - ) - completion_message = CHECK_MODEL.out.completion_message - - HANDLE_DATA( - params.csv, - params.exp_conf, - completion_message - ) - prepared_data = HANDLE_DATA.out.data - //HANDLE_DATA.out.data.view() - - HANDLE_TUNE( - params.model, - params.tune_conf, - prepared_data, - params.initial_weights - ) - //HANDLE_TUNE.out.model.view() - //HANDLE_TUNE.out.tune_out.view() - - // this part works, but the docker container is not updated with matplotlib yet - HANDLE_ANALYSIS( - HANDLE_TUNE.out.tune_out, - HANDLE_TUNE.out.model - ) - -} - -/* -======================= -*/ - - include { DEEPMODELOPTIM } from './workflows/deepmodeloptim' include { PIPELINE_INITIALISATION } from './subworkflows/local/utils_nfcore_deepmodeloptim_pipeline' include { PIPELINE_COMPLETION } from './subworkflows/local/utils_nfcore_deepmodeloptim_pipeline' diff --git a/workflows/deepmodeloptim.nf b/workflows/deepmodeloptim.nf index 2d073ec..cab68a8 100644 --- a/workflows/deepmodeloptim.nf +++ b/workflows/deepmodeloptim.nf @@ -19,22 +19,60 @@ include { methodsDescriptionText } from '../subworkflows/local/utils_nfcore_deep workflow DEEPMODELOPTIM { take: - ch_samplesheet // channel: samplesheet read in from --input + // ch_samplesheet, // channel: samplesheet read in from --input + ch_csv, + ch_exp_conf, + ch_model, + ch_tune_conf, + ch_initial_weights + main: ch_versions = Channel.empty() + CHECK_MODEL ( + params.csv, + params.exp_conf, + params.model, + params.tune_conf, + params.initial_weights + ) + completion_message = CHECK_MODEL.out.completion_message + + HANDLE_DATA( + params.csv, + params.exp_conf, + completion_message + ) + prepared_data = HANDLE_DATA.out.data + //HANDLE_DATA.out.data.view() + + HANDLE_TUNE( + params.model, + params.tune_conf, + prepared_data, + params.initial_weights + ) + //HANDLE_TUNE.out.model.view() + //HANDLE_TUNE.out.tune_out.view() + + // this part works, but the docker container is not updated with matplotlib yet + HANDLE_ANALYSIS( + HANDLE_TUNE.out.tune_out, + HANDLE_TUNE.out.model + ) // // Collate and save software versions // - softwareVersionsToYAML(ch_versions) - .collectFile( - storeDir: "${params.outdir}/pipeline_info", - name: 'nf_core_' + 'pipeline_software_' + '' + 'versions.yml', - sort: true, - newLine: true - ).set { ch_collated_versions } + // TODO: collect software versions + // softwareVersionsToYAML(ch_versions) + // .collectFile( + // storeDir: "${params.outdir}/pipeline_info", + // name: 'nf_core_' + 'pipeline_software_' + '' + 'versions.yml', + // sort: true, + // newLine: true + // ).set { ch_collated_versions } emit: From 763c4b8232b71f477ae3a51ecd5ee1ef2f0f7ff7 Mon Sep 17 00:00:00 2001 From: JoseEspinosa Date: Fri, 18 Oct 2024 09:21:25 +0200 Subject: [PATCH 04/25] Get rid of check_max --- conf/crg.config | 18 +++++++++--------- conf/crg_slurm.config | 18 +++++++++--------- conf/local.config | 18 +++++++++--------- conf/test_learn.config | 20 ++++++++++---------- 4 files changed, 37 insertions(+), 37 deletions(-) diff --git a/conf/crg.config b/conf/crg.config index a28efd5..868b661 100644 --- a/conf/crg.config +++ b/conf/crg.config @@ -15,21 +15,21 @@ process { withLabel:process_low { queue = 'cn-el7,short-centos79' - cpus = { check_max( 1 , 'cpus' ) } - memory = { check_max( 4.GB * task.attempt, 'memory' ) } - time = { check_max( 1.h * task.attempt, 'time' ) } + cpus = { 1 } + memory = { 4.GB * task.attempt } + time = { 1.h * task.attempt } } withLabel:process_medium{ queue = 'cn-el7,short-centos79' - cpus = { check_max( 4 , 'cpus' ) } - memory = { check_max( 10.GB * task.attempt, 'memory' ) } - time = { check_max( 6.h * task.attempt, 'time' ) } + cpus = { 4 } + memory = { 10.GB * task.attempt } + time = { 6.h * task.attempt } } withLabel:process_high { queue = 'cn-el7,long-centos79' - cpus = { check_max( 12 , 'cpus' ) } - memory = { check_max( 20.GB * task.attempt, 'memory' ) } - time = { check_max( 12.h * task.attempt, 'time' ) } + cpus = { 12 } + memory = { 20.GB * task.attempt } + time = { 12.h * task.attempt } } } diff --git a/conf/crg_slurm.config b/conf/crg_slurm.config index 724f979..a9f3043 100644 --- a/conf/crg_slurm.config +++ b/conf/crg_slurm.config @@ -14,21 +14,21 @@ process { withLabel:process_low { clusterOptions = '--qos=shorter' - cpus = { check_max( 1 , 'cpus' ) } - memory = { check_max( 4.GB * task.attempt, 'memory' ) } - time = { check_max( 1.h * task.attempt, 'time' ) } + cpus = { 1 } + memory = { 4.GB * task.attempt } + time = { 1.h * task.attempt } } withLabel:process_medium{ clusterOptions = '--qos=short' - cpus = { check_max( 4 , 'cpus' ) } - memory = { check_max( 10.GB * task.attempt, 'memory' ) } - time = { check_max( 6.h * task.attempt, 'time' ) } + cpus = { 4 } + memory = { 10.GB * task.attempt } + time = { 6.h * task.attempt } } withLabel:process_high { clusterOptions = '--qos=normal' - cpus = { check_max( 12 , 'cpus' ) } - memory = { check_max( 20.GB * task.attempt, 'memory' ) } - time = { check_max( 12.h * task.attempt, 'time' ) } + cpus = { 12 } + memory = { 20.GB * task.attempt } + time = { 12.h * task.attempt } } } diff --git a/conf/local.config b/conf/local.config index ee813e3..67d2330 100644 --- a/conf/local.config +++ b/conf/local.config @@ -10,18 +10,18 @@ process { errorStrategy = params.err_start withLabel:process_low { - cpus = { check_max( 1 , 'cpus' ) } - memory = { check_max( 4.GB * task.attempt, 'memory' ) } - time = { check_max( 1.h * task.attempt, 'time' ) } + cpus = { 1 } + memory = { 4.GB * task.attempt } + time = { 1.h * task.attempt } } withLabel:process_medium{ - cpus = { check_max( 4 , 'cpus' ) } - memory = { check_max( 10.GB * task.attempt, 'memory' ) } - time = { check_max( 6.h * task.attempt, 'time' ) } + cpus = { 4 } + memory = { 10.GB * task.attempt } + time = { 6.h * task.attempt } } withLabel:process_medium_high { - cpus = { check_max( 12 , 'cpus' ) } - memory = { check_max( 50.GB * task.attempt, 'memory' ) } - time = { check_max( 12.h * task.attempt, 'time' ) } + cpus = { 12 } + memory = { 50.GB * task.attempt } + time = { 12.h * task.attempt } } } diff --git a/conf/test_learn.config b/conf/test_learn.config index 0f2df49..7651199 100644 --- a/conf/test_learn.config +++ b/conf/test_learn.config @@ -19,24 +19,24 @@ params { } -// Limit resources so that this can run on GitHub Actions or take benefit from the check_max function +// Limit resources so that this can run on GitHub Actions process { maxRetries = params.max_retries errorStrategy = params.err_start withLabel:process_low { - cpus = { check_max( 1 , 'cpus' ) } - memory = { check_max( 4.GB * task.attempt, 'memory' ) } - time = { check_max( 10.m * task.attempt, 'time' ) } + cpus = { 1 } + memory = { 4.GB * task.attempt } + time = { 10.m * task.attempt } } withLabel:process_medium{ - cpus = { check_max( 2 , 'cpus' ) } - memory = { check_max( 6.GB * task.attempt, 'memory' ) } - time = { check_max( 30.m * task.attempt, 'time' ) } + cpus = { 2 } + memory = { 6.GB * task.attempt } + time = { 30.m * task.attempt } } withLabel:process_high { - cpus = { check_max( 4 , 'cpus' ) } - memory = { check_max( 8.GB * task.attempt, 'memory' ) } - time = { check_max( 1.h * task.attempt, 'time' ) } + cpus = { 4 } + memory = { 8.GB * task.attempt } + time = { 1.h * task.attempt } } } From ffc139a3a307d4662a279a1a54d254e7607962e8 Mon Sep 17 00:00:00 2001 From: JoseEspinosa Date: Fri, 18 Oct 2024 10:21:21 +0200 Subject: [PATCH 05/25] Bring back max_gpus parameter and fix resource limit in configs --- conf/crg.config | 10 +++-- conf/crg_slurm.config | 10 +++-- conf/test_learn.config | 11 ++--- conf/test_stub.config | 12 +++--- main.nf | 15 ++++--- nextflow.config | 9 ++-- nextflow_schema.json | 14 ++++++- subworkflows/check_model.nf | 1 - .../main.nf | 41 ++++++++++--------- workflows/deepmodeloptim.nf | 8 ++-- 10 files changed, 76 insertions(+), 55 deletions(-) diff --git a/conf/crg.config b/conf/crg.config index 868b661..bac7b3c 100644 --- a/conf/crg.config +++ b/conf/crg.config @@ -1,14 +1,16 @@ params { config_profile_name = 'CRG profile' config_profile_description = 'Configuration to run on CRG cluster' - - max_cpus = 64 - max_memory = 100.GB - max_time = 48.h } process { + resourceLimits = [ + cpus: 64, + memory: 100.GB, + time: 48.h + ] + executor = 'crg' maxRetries = params.max_retries errorStrategy = params.err_start diff --git a/conf/crg_slurm.config b/conf/crg_slurm.config index a9f3043..5e23bce 100644 --- a/conf/crg_slurm.config +++ b/conf/crg_slurm.config @@ -2,12 +2,16 @@ params { config_profile_name = 'CRG profile - slurm new cluster' config_profile_description = 'Configuration to run on CRG new cluster' - max_cpus = 64 - max_memory = 100.GB - max_time = 48.h } process { + + resourceLimits = [ + cpus: 64, + memory: 100.GB, + time: 48.h + ] + executor = "slurm" maxRetries = params.max_retries errorStrategy = params.err_start diff --git a/conf/test_learn.config b/conf/test_learn.config index 7651199..f615990 100644 --- a/conf/test_learn.config +++ b/conf/test_learn.config @@ -5,11 +5,6 @@ params { config_profile_name = 'Test Learn profile' config_profile_description = 'Minimal test dataset to check if a model that should learn actually does' - - // Limit resources so that this can run on GitHub Actions - max_cpus = 2 - max_memory = '8.GB' - max_time = '6.h' // Input data csv = "${projectDir}/bin/tests/test_data/titanic/titanic_stimulus.csv" @@ -21,6 +16,12 @@ params { // Limit resources so that this can run on GitHub Actions process { + resourceLimits = [ + cpus: 2, + memory: 8.GB, + time: 6.h + ] + maxRetries = params.max_retries errorStrategy = params.err_start diff --git a/conf/test_stub.config b/conf/test_stub.config index befeb1d..861f494 100644 --- a/conf/test_stub.config +++ b/conf/test_stub.config @@ -6,11 +6,6 @@ stubRun = true params { config_profile_name = 'Test stub profile' config_profile_description = 'stub run to check pipeline function' - - // Limit resources so that this can run on GitHub Actions - max_cpus = 2 - max_memory = '6.GB' - max_time = '6.h' // Input data csv = "${projectDir}/bin/tests/test_data/dna_experiment/test.csv" @@ -20,6 +15,13 @@ params { } process { + // Limit resources so that this can run on GitHub Actions + resourceLimits = [ + cpus: 2, + memory: 6.GB, + time: 6.h + ] + withName: 'INTERPRET_JSON|STIMULUS_NOISE_CSV|STIMULUS_SHUFFLE_CSV|STIMULUS_SPLIT_CSV|TORCH_TRAIN' { container = 'quay.io/biocontainers/gawk:5.1.0' } diff --git a/main.nf b/main.nf index bd68d0d..2d5b0c0 100644 --- a/main.nf +++ b/main.nf @@ -10,9 +10,9 @@ */ -include { CHECK_MODEL } from './subworkflows/check_model.nf' -include { HANDLE_DATA } from './workflows/handle_data.nf' -include { HANDLE_TUNE } from './workflows/handle_tune.nf' +include { CHECK_MODEL } from './subworkflows/check_model.nf' +include { HANDLE_DATA } from './workflows/handle_data.nf' +include { HANDLE_TUNE } from './workflows/handle_tune.nf' include { HANDLE_ANALYSIS } from './workflows/handle_analysis.nf' /* @@ -39,9 +39,9 @@ workflow NFCORE_DEEPMODELOPTIM { take: // samplesheet // channel: samplesheet read in from --input csv - exp_conf, - model, - tune_conf, + exp_conf + model + tune_conf initial_weights main: @@ -101,8 +101,7 @@ workflow { params.plaintext_email, params.outdir, params.monochrome_logs, - params.hook_url, - + params.hook_url ) } diff --git a/nextflow.config b/nextflow.config index 2f17f1f..e749ea8 100644 --- a/nextflow.config +++ b/nextflow.config @@ -23,11 +23,12 @@ params { outdir = "./results/" // the outdir has to be the one the user specify _ the unique name of the run _ the time so that multiple runs will not overlap publish_dir_mode = "copy" - // Computational resources - max_cpus = 12 // this flasg and the following are for regulating resources, profiles can overwrite these. + // Computational resources max_gpus = 1 // requesting the gpus for the tuning steps. - max_memory = 32.GB - max_time = "72.h" + // TODO this should not be in the nextflow config but in each config + // max_cpus = 12 // this flasg and the following are for regulating resources, profiles can overwrite these. + // max_memory = 32.GB + // max_time = "72.h" // Error options max_retries = 0 diff --git a/nextflow_schema.json b/nextflow_schema.json index 0787dbb..1af8907 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -75,6 +75,15 @@ } } }, + "resources_options": { + "max_gpus": { + "type": "integer", + "default": 1, + "minimum": 0, + "help_text": "requesting the gpus for the tuning steps.", + "description": "set maximum GPU limit" + } + }, "on_error_options": { "title": "On error options", "type": "object", @@ -257,11 +266,14 @@ }, "allOf": [ { - "$ref": "#/$defs/input_files_options" + "$ref": "#/$defs/input_output_options" }, { "$ref": "#/$defs/optional_inputs" }, + { + "$ref": "#/$defs/resources_options" + }, { "$ref": "#/$defs/on_error_options" }, diff --git a/subworkflows/check_model.nf b/subworkflows/check_model.nf index ff555db..d805c49 100644 --- a/subworkflows/check_model.nf +++ b/subworkflows/check_model.nf @@ -60,7 +60,6 @@ workflow CHECK_MODEL { } - /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ THE END diff --git a/subworkflows/local/utils_nfcore_deepmodeloptim_pipeline/main.nf b/subworkflows/local/utils_nfcore_deepmodeloptim_pipeline/main.nf index 23b8816..9cd4f29 100644 --- a/subworkflows/local/utils_nfcore_deepmodeloptim_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_deepmodeloptim_pipeline/main.nf @@ -31,7 +31,7 @@ workflow PIPELINE_INITIALISATION { monochrome_logs // boolean: Do not use coloured log outputs 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 + //input // string: Path to input samplesheet main: @@ -69,25 +69,26 @@ workflow PIPELINE_INITIALISATION { // Create channel from input file provided through params.input // - Channel - .fromList(samplesheetToList(params.input, "${projectDir}/assets/schema_input.json")) - .map { - meta, fastq_1, fastq_2 -> - if (!fastq_2) { - return [ meta.id, meta + [ single_end:true ], [ fastq_1 ] ] - } else { - return [ meta.id, meta + [ single_end:false ], [ fastq_1, fastq_2 ] ] - } - } - .groupTuple() - .map { samplesheet -> - validateInputSamplesheet(samplesheet) - } - .map { - meta, fastqs -> - return [ meta, fastqs.flatten() ] - } - .set { ch_samplesheet } + // TODO: Adequate it to our input if `input` is implemented + // Channel + // .fromList(samplesheetToList(params.input, "${projectDir}/assets/schema_input.json")) + // .map { + // meta, fastq_1, fastq_2 -> + // if (!fastq_2) { + // return [ meta.id, meta + [ single_end:true ], [ fastq_1 ] ] + // } else { + // return [ meta.id, meta + [ single_end:false ], [ fastq_1, fastq_2 ] ] + // } + // } + // .groupTuple() + // .map { samplesheet -> + // validateInputSamplesheet(samplesheet) + // } + // .map { + // meta, fastqs -> + // return [ meta, fastqs.flatten() ] + // } + // .set { ch_samplesheet } emit: samplesheet = ch_samplesheet diff --git a/workflows/deepmodeloptim.nf b/workflows/deepmodeloptim.nf index cab68a8..87ae0d3 100644 --- a/workflows/deepmodeloptim.nf +++ b/workflows/deepmodeloptim.nf @@ -20,10 +20,10 @@ workflow DEEPMODELOPTIM { take: // ch_samplesheet, // channel: samplesheet read in from --input - ch_csv, - ch_exp_conf, - ch_model, - ch_tune_conf, + ch_csv + ch_exp_conf + ch_model + ch_tune_conf ch_initial_weights main: From 96ee230b3e48b6fc73b98fdb9042acef9a096ccf Mon Sep 17 00:00:00 2001 From: JoseEspinosa Date: Fri, 18 Oct 2024 16:52:12 +0200 Subject: [PATCH 06/25] Refactor pipeline to nf-core structure --- main.nf | 6 ------ .../{check_model.nf => local/check_model/main.nf} | 2 +- .../local/handle_analysis/main.nf | 2 +- .../local/handle_data/main.nf | 9 ++++----- .../local/handle_tune/main.nf | 2 +- .../{shuffle_csv.nf => local/shuffle_csv/main.nf} | 2 +- subworkflows/{split_csv.nf => local/split_csv/main.nf} | 2 +- .../{transform_csv.nf => local/transform_csv/main.nf} | 2 +- .../local/utils_nfcore_deepmodeloptim_pipeline/main.nf | 2 +- workflows/deepmodeloptim.nf | 9 +++++++-- 10 files changed, 18 insertions(+), 20 deletions(-) rename subworkflows/{check_model.nf => local/check_model/main.nf} (96%) rename workflows/handle_analysis.nf => subworkflows/local/handle_analysis/main.nf (91%) rename workflows/handle_data.nf => subworkflows/local/handle_data/main.nf (93%) rename workflows/handle_tune.nf => subworkflows/local/handle_tune/main.nf (96%) rename subworkflows/{shuffle_csv.nf => local/shuffle_csv/main.nf} (90%) rename subworkflows/{split_csv.nf => local/split_csv/main.nf} (92%) rename subworkflows/{transform_csv.nf => local/transform_csv/main.nf} (92%) diff --git a/main.nf b/main.nf index 2d5b0c0..28def03 100644 --- a/main.nf +++ b/main.nf @@ -9,12 +9,6 @@ ---------------------------------------------------------------------------------------- */ - -include { CHECK_MODEL } from './subworkflows/check_model.nf' -include { HANDLE_DATA } from './workflows/handle_data.nf' -include { HANDLE_TUNE } from './workflows/handle_tune.nf' -include { HANDLE_ANALYSIS } from './workflows/handle_analysis.nf' - /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ RUN ALL WORKFLOWS diff --git a/subworkflows/check_model.nf b/subworkflows/local/check_model/main.nf similarity index 96% rename from subworkflows/check_model.nf rename to subworkflows/local/check_model/main.nf index d805c49..d55dc33 100644 --- a/subworkflows/check_model.nf +++ b/subworkflows/local/check_model/main.nf @@ -4,7 +4,7 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -include { CHECK_TORCH_MODEL } from '../modules/local/check_torch_model.nf' +include { CHECK_TORCH_MODEL } from '../../../modules/local/check_torch_model.nf' /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/workflows/handle_analysis.nf b/subworkflows/local/handle_analysis/main.nf similarity index 91% rename from workflows/handle_analysis.nf rename to subworkflows/local/handle_analysis/main.nf index 0a8800e..db3aee4 100644 --- a/workflows/handle_analysis.nf +++ b/subworkflows/local/handle_analysis/main.nf @@ -4,7 +4,7 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -include { STIMULUS_ANALYSIS_DEFAULT } from '../modules/local/stimulus_analysis_default' +include { STIMULUS_ANALYSIS_DEFAULT } from '../../../modules/local/stimulus_analysis_default' /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/workflows/handle_data.nf b/subworkflows/local/handle_data/main.nf similarity index 93% rename from workflows/handle_data.nf rename to subworkflows/local/handle_data/main.nf index 6f01043..472bd3f 100644 --- a/workflows/handle_data.nf +++ b/subworkflows/local/handle_data/main.nf @@ -4,11 +4,10 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -include { INTERPRET_JSON } from '../modules/local/interpret_json.nf' -include { SPLIT_CSV } from '../subworkflows/split_csv.nf' -include { TRANSFORM_CSV } from '../subworkflows/transform_csv.nf' -include { SHUFFLE_CSV } from '../subworkflows/shuffle_csv.nf' - +include { INTERPRET_JSON } from '../../../modules/local/interpret_json.nf' +include { SPLIT_CSV } from '../../../subworkflows/local/split_csv' +include { TRANSFORM_CSV } from '../../../subworkflows/local/transform_csv' +include { SHUFFLE_CSV } from '../../../subworkflows/local/shuffle_csv' /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/workflows/handle_tune.nf b/subworkflows/local/handle_tune/main.nf similarity index 96% rename from workflows/handle_tune.nf rename to subworkflows/local/handle_tune/main.nf index 6914bc5..edb722a 100644 --- a/workflows/handle_tune.nf +++ b/subworkflows/local/handle_tune/main.nf @@ -4,7 +4,7 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -include { TORCH_TUNE } from '../modules/local/torch_tune.nf' +include { TORCH_TUNE } from '../../../modules/local/torch_tune.nf' /* diff --git a/subworkflows/shuffle_csv.nf b/subworkflows/local/shuffle_csv/main.nf similarity index 90% rename from subworkflows/shuffle_csv.nf rename to subworkflows/local/shuffle_csv/main.nf index c15091a..5736ec9 100644 --- a/subworkflows/shuffle_csv.nf +++ b/subworkflows/local/shuffle_csv/main.nf @@ -4,7 +4,7 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -include { STIMULUS_SHUFFLE_CSV } from '../modules/local/stimulus_shuffle_csv.nf' +include { STIMULUS_SHUFFLE_CSV } from '../../../modules/local/stimulus_shuffle_csv.nf' /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/subworkflows/split_csv.nf b/subworkflows/local/split_csv/main.nf similarity index 92% rename from subworkflows/split_csv.nf rename to subworkflows/local/split_csv/main.nf index d3e1ef4..8bf6037 100644 --- a/subworkflows/split_csv.nf +++ b/subworkflows/local/split_csv/main.nf @@ -4,7 +4,7 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -include { STIMULUS_SPLIT_CSV } from '../modules/local/stimulus_split_csv.nf' +include { STIMULUS_SPLIT_CSV } from '../../../modules/local/stimulus_split_csv.nf' /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/subworkflows/transform_csv.nf b/subworkflows/local/transform_csv/main.nf similarity index 92% rename from subworkflows/transform_csv.nf rename to subworkflows/local/transform_csv/main.nf index dd1f84f..2549ac6 100644 --- a/subworkflows/transform_csv.nf +++ b/subworkflows/local/transform_csv/main.nf @@ -4,7 +4,7 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -include { STIMULUS_TRANSFORM_CSV } from '../modules/local/stimulus_transform_csv.nf' +include { STIMULUS_TRANSFORM_CSV } from '../../../modules/local/stimulus_transform_csv.nf' /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/subworkflows/local/utils_nfcore_deepmodeloptim_pipeline/main.nf b/subworkflows/local/utils_nfcore_deepmodeloptim_pipeline/main.nf index 9cd4f29..c39df6b 100644 --- a/subworkflows/local/utils_nfcore_deepmodeloptim_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_deepmodeloptim_pipeline/main.nf @@ -91,7 +91,7 @@ workflow PIPELINE_INITIALISATION { // .set { ch_samplesheet } emit: - samplesheet = ch_samplesheet + // samplesheet = ch_samplesheet versions = ch_versions } diff --git a/workflows/deepmodeloptim.nf b/workflows/deepmodeloptim.nf index 87ae0d3..6f90e8e 100644 --- a/workflows/deepmodeloptim.nf +++ b/workflows/deepmodeloptim.nf @@ -4,11 +4,16 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ - +// +// SUBWORKFLOW: Consisting of a mix of local and nf-core/modules +// include { paramsSummaryMap } from 'plugin/nf-schema' - include { softwareVersionsToYAML } from '../subworkflows/nf-core/utils_nfcore_pipeline' include { methodsDescriptionText } from '../subworkflows/local/utils_nfcore_deepmodeloptim_pipeline' +include { CHECK_MODEL } from '../subworkflows/local/check_model' +include { HANDLE_DATA } from '../subworkflows/local/handle_data' +include { HANDLE_TUNE } from '../subworkflows/local/handle_tune' +include { HANDLE_ANALYSIS } from '../subworkflows/local/handle_analysis' /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From ba86448d6d9b68c396b6fd3aff391560b1f2cc17 Mon Sep 17 00:00:00 2001 From: JoseEspinosa Date: Fri, 18 Oct 2024 16:52:30 +0200 Subject: [PATCH 07/25] Ping correctly stimulus image --- modules/local/check_torch_model.nf | 3 ++- modules/local/interpret_json.nf | 3 ++- modules/local/stimulus_analysis_default.nf | 3 ++- modules/local/stimulus_shuffle_csv.nf | 3 ++- modules/local/stimulus_split_csv.nf | 3 ++- modules/local/stimulus_transform_csv.nf | 3 ++- modules/local/torch_tune.nf | 3 ++- 7 files changed, 14 insertions(+), 7 deletions(-) diff --git a/modules/local/check_torch_model.nf b/modules/local/check_torch_model.nf index 929623f..aad9f67 100644 --- a/modules/local/check_torch_model.nf +++ b/modules/local/check_torch_model.nf @@ -3,7 +3,8 @@ process CHECK_TORCH_MODEL { tag "$experiment_config - $original_csv" label 'process_medium' - container "mathysgrapotte/stimulus-py:latest" + // TODO: push image to nf-core quay.io + container "docker.io/mathysgrapotte/stimulus-py:latest" input: tuple path(original_csv), path(model), path(experiment_config), path(ray_tune_config), path(initial_weights) diff --git a/modules/local/interpret_json.nf b/modules/local/interpret_json.nf index 464d3cc..daad7d9 100644 --- a/modules/local/interpret_json.nf +++ b/modules/local/interpret_json.nf @@ -3,7 +3,8 @@ process INTERPRET_JSON { tag "$user_json" label 'process_low' - container "mathysgrapotte/stimulus-py:latest" + // TODO: push image to nf-core quay.io + container "docker.io/mathysgrapotte/stimulus-py:latest" input: path user_json diff --git a/modules/local/stimulus_analysis_default.nf b/modules/local/stimulus_analysis_default.nf index a2ecf11..619ee00 100644 --- a/modules/local/stimulus_analysis_default.nf +++ b/modules/local/stimulus_analysis_default.nf @@ -3,7 +3,8 @@ process STIMULUS_ANALYSIS_DEFAULT { tag "${model} - ${split_transform_key}" label 'process_medium' - container "mathysgrapotte/stimulus-py:latest" + // TODO: push image to nf-core quay.io + container "docker.io/mathysgrapotte/stimulus-py:latest" input: tuple val(split_transform_key), \ diff --git a/modules/local/stimulus_shuffle_csv.nf b/modules/local/stimulus_shuffle_csv.nf index c8c433a..e1f8ad0 100644 --- a/modules/local/stimulus_shuffle_csv.nf +++ b/modules/local/stimulus_shuffle_csv.nf @@ -3,7 +3,8 @@ process STIMULUS_SHUFFLE_CSV { tag "${original_csv} - shuffles" label 'process_medium' - container "mathysgrapotte/stimulus-py:latest" + // TODO: push image to nf-core quay.io + container "docker.io/mathysgrapotte/stimulus-py:latest" input: tuple val(split_transform_key), path(splitted_csv), path(split_json), path(original_csv) diff --git a/modules/local/stimulus_split_csv.nf b/modules/local/stimulus_split_csv.nf index 0d4accd..d505668 100644 --- a/modules/local/stimulus_split_csv.nf +++ b/modules/local/stimulus_split_csv.nf @@ -3,7 +3,8 @@ process STIMULUS_SPLIT_CSV { tag "${original_csv} - ${split_transform_key}" label 'process_low' - container "mathysgrapotte/stimulus-py:latest" + // TODO: push image to nf-core quay.io + container "docker.io/mathysgrapotte/stimulus-py:latest" input: tuple val(split_transform_key), path(split_json), path(original_csv) diff --git a/modules/local/stimulus_transform_csv.nf b/modules/local/stimulus_transform_csv.nf index 007d201..994e045 100644 --- a/modules/local/stimulus_transform_csv.nf +++ b/modules/local/stimulus_transform_csv.nf @@ -3,7 +3,8 @@ process STIMULUS_TRANSFORM_CSV { tag "${original_csv} - ${combination_key}" label 'process_medium' - container "mathysgrapotte/stimulus-py:latest" + // TODO: push image to nf-core quay.io + container "docker.io/mathysgrapotte/stimulus-py:latest" input: tuple val(split_transform_key), val(combination_key), path(transform_json), path(splitted_csv), path(split_json), path(original_csv) diff --git a/modules/local/torch_tune.nf b/modules/local/torch_tune.nf index 783f597..bcad1d8 100644 --- a/modules/local/torch_tune.nf +++ b/modules/local/torch_tune.nf @@ -3,7 +3,8 @@ process TORCH_TUNE { tag "${model} - ${combination_key}" label 'process_high' - container "mathysgrapotte/stimulus-py:latest" + // TODO: push image to nf-core quay.io + container "docker.io/mathysgrapotte/stimulus-py:latest" input: tuple val(combination_key), val(split_transform_key), path(ray_tune_config), path(model), path(data_csv), path(experiment_config), path(initial_weights) From a2e253cfdc5664f984402f8c4809d6345cc4f51f Mon Sep 17 00:00:00 2001 From: JoseEspinosa Date: Fri, 18 Oct 2024 18:36:41 +0200 Subject: [PATCH 08/25] Fix resources options entry in schema --- nextflow_schema.json | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/nextflow_schema.json b/nextflow_schema.json index 1af8907..ee59e31 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -76,13 +76,19 @@ } }, "resources_options": { - "max_gpus": { - "type": "integer", - "default": 1, - "minimum": 0, - "help_text": "requesting the gpus for the tuning steps.", - "description": "set maximum GPU limit" - } + "title": "Resources options", + "type": "object", + "description": "Specify maximun processes resources", + "default": "", + "properties": { + "max_gpus": { + "type": "integer", + "default": 1, + "minimum": 0, + "help_text": "requesting the gpus for the tuning steps.", + "description": "set maximum GPU limit" + } + } }, "on_error_options": { "title": "On error options", From 84f8372d3b4feaa66464e04e6db256a4897779ae Mon Sep 17 00:00:00 2001 From: JoseEspinosa Date: Mon, 21 Oct 2024 10:55:17 +0200 Subject: [PATCH 09/25] Make prettier happy --- .github/CONTRIBUTING.md | 2 +- .github/workflows/download_pipeline.yml | 1 - .nf-core.yml | 26 +- CITATIONS.md | 5 - README.md | 53 ++- docs/output.md | 7 +- docs/usage.md | 2 +- examples/pipeline_generated.json | 22 +- examples/test.json | 6 +- examples/user_given.json | 45 +- modules.json | 3 +- nextflow_schema.json | 568 ++++++++++++------------ 12 files changed, 366 insertions(+), 374 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 2cfa080..8a3519a 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -25,6 +25,7 @@ If you'd like to write some code for nf-core/deepmodeloptim, the standard workfl If you're not used to this workflow with git, you can start with some [docs from GitHub](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests) or even their [excellent `git` resources](https://try.github.io/). ## Tests + You have the option to test your changes locally by running the pipeline. For receiving warnings about process selectors and other `debug` information, it is recommended to use the debug profile. Execute all the tests with the following command: ```bash @@ -120,4 +121,3 @@ To get started: Devcontainer specs: - [DevContainer config](.devcontainer/devcontainer.json) - diff --git a/.github/workflows/download_pipeline.yml b/.github/workflows/download_pipeline.yml index 63f7dcc..713dc3e 100644 --- a/.github/workflows/download_pipeline.yml +++ b/.github/workflows/download_pipeline.yml @@ -117,4 +117,3 @@ jobs: else echo "The pipeline can be downloaded successfully!" fi - diff --git a/.nf-core.yml b/.nf-core.yml index 1ef8d01..1f17dd3 100644 --- a/.nf-core.yml +++ b/.nf-core.yml @@ -1,17 +1,17 @@ bump_version: null lint: files_exist: - - conf/igenomes.config - - conf/igenomes_ignored.config - - assets/multiqc_config.yml - - conf/igenomes.config - - conf/igenomes_ignored.config - - assets/multiqc_config.yml + - conf/igenomes.config + - conf/igenomes_ignored.config + - assets/multiqc_config.yml + - conf/igenomes.config + - conf/igenomes_ignored.config + - assets/multiqc_config.yml files_unchanged: - - .github/CONTRIBUTING.md - - assets/sendmail_template.txt - - .github/CONTRIBUTING.md - - assets/sendmail_template.txt + - .github/CONTRIBUTING.md + - assets/sendmail_template.txt + - .github/CONTRIBUTING.md + - assets/sendmail_template.txt multiqc_config: false nf_core_version: 3.0.0 org_path: null @@ -26,8 +26,8 @@ template: org: nf-core outdir: . skip_features: - - igenomes - - multiqc - - fastqc + - igenomes + - multiqc + - fastqc version: 1.0.0dev update: null diff --git a/CITATIONS.md b/CITATIONS.md index fd9fb93..f3cf692 100644 --- a/CITATIONS.md +++ b/CITATIONS.md @@ -10,10 +10,6 @@ ## Pipeline tools - - - - ## Software packaging/containerisation tools - [Anaconda](https://anaconda.com) @@ -35,4 +31,3 @@ - [Singularity](https://pubmed.ncbi.nlm.nih.gov/28494014/) > Kurtzer GM, Sochat V, Bauer MW. Singularity: Scientific containers for mobility of compute. PLoS One. 2017 May 11;12(5):e0177459. doi: 10.1371/journal.pone.0177459. eCollection 2017. PubMed PMID: 28494014; PubMed Central PMCID: PMC5426675. - > diff --git a/README.md b/README.md index d15c3bf..1cd09e5 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,13 @@ ## Introduction -**nf-core/deepmodeloptim** is a bioinformatics end-to-end pipeline that performs statistically testing training procedures of machine learning models. +**nf-core/deepmodeloptim** is a bioinformatics end-to-end pipeline that performs statistically testing training procedures of machine learning models. Deep learning model development in natural science is an empirical and costly process. Users must define a pre-processing pipeline, an architecture, find the best parameters for said architecture and iterate over this process. Leveraging the power of Nextflow (polyglotism, container integration, scalable on the cloud), we propose STIMULUS, an open-source software built to automatize deep learning model development for genomics. -STIMULUS takes as input a user defined PyTorch model, a dataset, a configuration file to describe the pre-processing steps to be performed, and a range of parameters for the PyTorch model. It then transforms the data according to all possible pre-processing steps, finds the best architecture parameters for each of the transformed datasets, performs sanity checks on the models and train a minimal deep learning version for each dataset/architecture. +STIMULUS takes as input a user defined PyTorch model, a dataset, a configuration file to describe the pre-processing steps to be performed, and a range of parameters for the PyTorch model. It then transforms the data according to all possible pre-processing steps, finds the best architecture parameters for each of the transformed datasets, performs sanity checks on the models and train a minimal deep learning version for each dataset/architecture. Those experiments are then compiled into an intuitive report, making it easier for scientists to pick the best design choice to be sent to large scale training. @@ -74,7 +74,7 @@ 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/deepmodeloptim/output). - @@ -84,37 +84,36 @@ For more details about the output files and reports, please refer to the The data is provided as a csv where the header columns are in the following format : name:type:class -*name* is user given (note that it has an impact on experiment definition). +_name_ is user given (note that it has an impact on experiment definition). -*type* is either "input", "meta", or "label". "input" types are fed into the mode, "meta" types are registered but not transformed nor fed into the models and "label" is used as a training label. +_type_ is either "input", "meta", or "label". "input" types are fed into the mode, "meta" types are registered but not transformed nor fed into the models and "label" is used as a training label. -*class* is a supported class of data for which encoding methods have been created, please raise an issue on github or contribute a PR if a class of your interest is not implemented +_class_ is a supported class of data for which encoding methods have been created, please raise an issue on github or contribute a PR if a class of your interest is not implemented #### csv general example | input1:input:input_type | input2:input:input_type | meta1:meta:meta_type | label1:label:label_type | label2:label:label_type | -|-------------------------|-------------------------|----------------------|-------------------------|-------------------------| +| ----------------------- | ----------------------- | -------------------- | ----------------------- | ----------------------- | | sample1 input1 | sample1 input2 | sample1 meta1 | sample1 label1 | sample1 label2 | | sample2 input1 | sample2 input2 | sample2 meta1 | sample2 label1 | sample2 label2 | | sample3 input1 | sample3 input2 | sample3 meta1 | sample3 label1 | sample3 label2 | - #### csv specific example +| mouse_dna:input:dna | mouse_rnaseq:label:float | +| ------------------- | ------------------------ | +| ACTAGGCATGCTAGTCG | 0.53 | +| ACTGGGGCTAGTCGAA | 0.23 | +| GATGTTCTGATGCT | 0.98 | -| mouse_dna:input:dna | mouse_rnaseq:label:float| -|-------------------------|-------------------------| -| ACTAGGCATGCTAGTCG | 0.53 | -| ACTGGGGCTAGTCGAA | 0.23 | -| GATGTTCTGATGCT | 0.98 | - -### Model +### Model In STIMULUS, users input a .py file containing a model written in pytorch (see examples in bin/tests/models) Said models should obey to minor standards: 1. The model class you want to train should start with "Model", there should be exactly one class starting with "Model". + ```python import torch @@ -152,14 +151,14 @@ class ModelClass(nn.Module): # your model definition here pass - def forward(self, mouse_dna): + def forward(self, mouse_dna): output = model_layers(mouse_dna) ``` -3. The model should include a **batch** named function that takes as input a dictionary of input "x", a dictionary of labels "y", a Callable loss function and a callable optimizer. +3. The model should include a **batch** named function that takes as input a dictionary of input "x", a dictionary of labels "y", a Callable loss function and a callable optimizer. -In order to allow **batch** to take as input a Callable loss, we define an extra compute_loss function that parses the correct output to the correct loss class. +In order to allow **batch** to take as input a Callable loss, we define an extra compute_loss function that parses the correct output to the correct loss class. ```python @@ -176,7 +175,7 @@ class ModelClass(nn.Module): # your model definition here pass - def forward(self, mouse_dna): + def forward(self, mouse_dna): output = model_layers(mouse_dna) def compute_loss_mouse_rnaseq(self, output: torch.Tensor, mouse_rnaseq: torch.Tensor, loss_fn: Callable) -> torch.Tensor: @@ -186,10 +185,10 @@ class ModelClass(nn.Module): `mouse_rnaseq` is the target tensor -> label column name. `loss_fn` is the loss function to be used. - IMPORTANT : the input variable "mouse_rnaseq" has the same name as the label defined in the csv above. + IMPORTANT : the input variable "mouse_rnaseq" has the same name as the label defined in the csv above. """ return loss_fn(output, mouse_rnaseq) - + def batch(self, x: dict, y: dict, loss_fn: Callable, optimizer: Optional[Callable] = None) -> Tuple[torch.Tensor, dict]: """ Perform one batch step. @@ -227,9 +226,9 @@ class ModelClass(nn.Module): # your model definition here pass - def forward(self, mouse_dna): + def forward(self, mouse_dna): output = model_layers(mouse_dna) - + def batch(self, x: dict, y: dict, optimizer: Optional[Callable] = None) -> Tuple[torch.Tensor, dict]: """ Perform one batch step. @@ -254,17 +253,15 @@ class ModelClass(nn.Module): ### Experiment design -The file in which all information about how to handle the data before tuning is called an `experiment_config`. This file in `.json` format for now but it will be soon moved to `.yaml`. So this section could vary in the future. +The file in which all information about how to handle the data before tuning is called an `experiment_config`. This file in `.json` format for now but it will be soon moved to `.yaml`. So this section could vary in the future. -The `experiment_config` is a mandatory input for the pipeline and can be passed with the flag `--exp_conf` followed by the `PATH` of the file you want to use. Two examples of `experiment_config` can be found in the `examples` directory. +The `experiment_config` is a mandatory input for the pipeline and can be passed with the flag `--exp_conf` followed by the `PATH` of the file you want to use. Two examples of `experiment_config` can be found in the `examples` directory. ### Experiment config content description. - - ## Credits - diff --git a/docs/output.md b/docs/output.md index c3673d0..4a4b659 100644 --- a/docs/output.md +++ b/docs/output.md @@ -2,7 +2,7 @@ ## Introduction -This document describes the output produced by 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. @@ -12,9 +12,6 @@ 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: - - - - [Pipeline information](#pipeline-information) - Report metrics generated during the workflow execution ### Pipeline information @@ -24,7 +21,7 @@ The pipeline is built using [Nextflow](https://www.nextflow.io/) and processes d - `pipeline_info/` - Reports generated by Nextflow: `execution_report.html`, `execution_timeline.html`, `execution_trace.txt` and `pipeline_dag.dot`/`pipeline_dag.svg`. - - Reports generated by the pipeline: `pipeline_report.html`, `pipeline_report.txt` and `software_versions.yml`. The `pipeline_report*` files will only be present if the `--email` / `--email_on_fail` parameter's are used when running the pipeline. + - Reports generated by the pipeline: `pipeline_report.html`, `pipeline_report.txt` and `software_versions.yml`. The `pipeline_report*` files will only be present if the `--email` / `--email_on_fail` parameter's are used when running the pipeline. - Reformatted samplesheet files used as input to the pipeline: `samplesheet.valid.csv`. - Parameters used by the pipeline run: `params.json`. diff --git a/docs/usage.md b/docs/usage.md index b32fbfa..42ec1b6 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -110,7 +110,7 @@ It is a good idea to specify a pipeline version when running the pipeline on you First, go to the [nf-core/deepmodeloptim releases page](https://github.com/nf-core/deepmodeloptim/releases) and find the latest pipeline version - numeric only (eg. `1.3.1`). Then specify this when running the pipeline with `-r` (one hyphen) - eg. `-r 1.3.1`. Of course, you can switch to another version by changing the number after the `-r` flag. -This version number will be logged in reports when you run the pipeline, so that you'll know what you used when you look back in the future. +This version number will be logged in reports when you run the pipeline, so that you'll know what you used when you look back in the future. To further assist in reproducbility, you can use share and re-use [parameter files](#running-the-pipeline) to repeat pipeline runs with the same settings without having to write out a command with every single parameter. diff --git a/examples/pipeline_generated.json b/examples/pipeline_generated.json index 7531ac7..fede8da 100644 --- a/examples/pipeline_generated.json +++ b/examples/pipeline_generated.json @@ -3,30 +3,26 @@ "transform": [ [ { - "column_name": "hello:input:dna", "name": "UniformTextMasker", - "params": {"probability": 0.1} + "params": { "probability": 0.1 } }, { - "column_name": "bonjour:input:prot", "name": "UniformTextMasker", - "params": {"probability": 0.4} + "params": { "probability": 0.4 } } ], [ { "column_name": "hola:label:float", "name": "GaussianNoise", - "params": {"mean": 0.5, "std": 0.1} + "params": { "mean": 0.5, "std": 0.1 } } - ] + ] ], - "split": - { - "name": "RandomSplitter", - "params": {"split": [0.6, 0.2, 0.2]} - } - -} \ No newline at end of file + "split": { + "name": "RandomSplitter", + "params": { "split": [0.6, 0.2, 0.2] } + } +} diff --git a/examples/test.json b/examples/test.json index 8aa39c7..868e233 100644 --- a/examples/test.json +++ b/examples/test.json @@ -1,18 +1,18 @@ { "experiment": "DnaToFloatExperiment", - "interpret_params_mode": "column_wise", + "interpret_params_mode": "column_wise", "transform": [ { "column_name": "hello:input:dna", "name": ["UniformTextMasker", "ReverseComplement"], - "params": [{"probability": [0.2], "seed": [42]}, "default"] + "params": [{ "probability": [0.2], "seed": [42] }, "default"] }, null ], "split": [ { "name": "RandomSplitter", - "params": [{"split": [[0.34, 0.33, 0.33]], "seed": [42]}] + "params": [{ "split": [[0.34, 0.33, 0.33]], "seed": [42] }] }, null ] diff --git a/examples/user_given.json b/examples/user_given.json index 82e5a85..e671480 100644 --- a/examples/user_given.json +++ b/examples/user_given.json @@ -1,11 +1,15 @@ { "experiment": "DnaToFloatExperiment", - "interpret_parmas_mode": "culumn_wise", + "interpret_parmas_mode": "culumn_wise", "transform": [ { "column_name": "hello:input1:dna", "name": ["UniformTextMasker", "AnotherNoiser", "AnotherNoiser"], - "params": [{"probability": [0.1, 0.2, 0.3]}, {"probability": [0.11, 0.21, 0.31]}, {"probability": [0.12, 0.22, 0.32]}] + "params": [ + { "probability": [0.1, 0.2, 0.3] }, + { "probability": [0.11, 0.21, 0.31] }, + { "probability": [0.12, 0.22, 0.32] } + ] }, { "column_name": "hello:input2:prot", @@ -15,13 +19,20 @@ { "column_name": "hola:label:float", "name": "GaussianNoise", - "params": [{"mean": [0.5, 0.6, 0.7], "std": [0.1, 0.2, 0.3]}] + "params": [{ "mean": [0.5, 0.6, 0.7], "std": [0.1, 0.2, 0.3] }] } ], "split": [ { "name": "RandomSplitter", - "params": [{"split": [[0.6, 0.2, 0.2], [0.7, 0.15, 0.15]]}] + "params": [ + { + "split": [ + [0.6, 0.2, 0.2], + [0.7, 0.15, 0.15] + ] + } + ] }, { "name": "SomeSplitter", @@ -34,33 +45,31 @@ { "column_name": "hello:input:dna", "name": "UniformTextMasker", - "params": {"probability": 0.1} + "params": { "probability": 0.1 } }, { "column_name": "hola:label:float", "name": "GaussianNoise", - "params": {"mean": 0.4, "std": 0.15} + "params": { "mean": 0.4, "std": 0.15 } } ], - "split": - { - "name": "RandomSplitter", - "params": {"split": [0.6, 0.4, 0]} - } + "split": { + "name": "RandomSplitter", + "params": { "split": [0.6, 0.4, 0] } + } }, { "transform": [ { "column_name": "hello:input:dna", "name": "UniformTextMasker", - "params": {"probability": 0.1} + "params": { "probability": 0.1 } } ], - "split": - { - "name": "RandomSplitter", - "params": {"split": [0.6, 0.3, 0.1]} - } + "split": { + "name": "RandomSplitter", + "params": { "split": [0.6, 0.3, 0.1] } + } } ] -} \ No newline at end of file +} diff --git a/modules.json b/modules.json index fb095bf..6d9e8dc 100644 --- a/modules.json +++ b/modules.json @@ -4,8 +4,7 @@ "repos": { "https://github.com/nf-core/modules.git": { "modules": { - "nf-core": { - } + "nf-core": {} }, "subworkflows": { "nf-core": { diff --git a/nextflow_schema.json b/nextflow_schema.json index ee59e31..4484212 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -1,299 +1,299 @@ { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/nf-core/deepmodeloptim/master/nextflow_schema.json", - "title": "nf-core/deepmodeloptim pipeline parameters", - "description": "nf-core/deepmodeloptim is an end-to-end nextflow based pipeline for statistically testing training procedures of machine learning models", - "required": ["csv", "exp_conf", "model", "tune_conf", "outdir"], - "type": "object", - "$defs": { - "input_output_options": { - "title": "Input files options", - "type": "object", - "fa_icon": "fas fa-terminal", - "description": "Define where the pipeline should find input data.", - "properties": { - "csv": { - "type": "string", - "format": "file-path", - "description": "Test data as CSV file", - "fa_icon": "fas fa-folder-open", - "mimetype": "tesxt/csv", - "help_text": "the input file containing all input data" + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/nf-core/deepmodeloptim/master/nextflow_schema.json", + "title": "nf-core/deepmodeloptim pipeline parameters", + "description": "nf-core/deepmodeloptim is an end-to-end nextflow based pipeline for statistically testing training procedures of machine learning models", + "required": ["csv", "exp_conf", "model", "tune_conf", "outdir"], + "type": "object", + "$defs": { + "input_output_options": { + "title": "Input files options", + "type": "object", + "fa_icon": "fas fa-terminal", + "description": "Define where the pipeline should find input data.", + "properties": { + "csv": { + "type": "string", + "format": "file-path", + "description": "Test data as CSV file", + "fa_icon": "fas fa-folder-open", + "mimetype": "tesxt/csv", + "help_text": "the input file containing all input data" + }, + "exp_conf": { + "type": "string", + "format": "file-path", + "description": "Experiment config as JSON format", + "fa_icon": "fas fa-folder-open", + "help_text": "the json config file that specifies all the parameters relative to the data manipulation." + }, + "model": { + "type": "string", + "format": "file-path", + "description": "Model file in Python", + "fa_icon": "fas fa-folder-open", + "mimetype": "text/py", + "help_text": "the model file in python, the model that will be tested by this pipeline." + }, + "tune_conf": { + "type": "string", + "format": "file-path", + "description": "Tuning config in yaml format", + "fa_icon": "fas fa-folder-open", + "mimetype": "text/yaml", + "help_text": "the config file with all the hyperparameter directives (choiches) and all ray tune specs." + }, + "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.", + "fa_icon": "fas fa-folder-open", + "help_text": "The directory will contain a subdirectory with a name unique to each stimulus pipeline run." + }, + "email": { + "type": "string", + "description": "Email address for completion summary.", + "fa_icon": "fas fa-envelope", + "help_text": "Set this parameter to your e-mail address to get a summary e-mail with details of the run sent to you when the workflow exits. If set in your user config file (`~/.nextflow/config`) then you don't need to specify this on the command line for every run.", + "pattern": "^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,5})$" + } + } + }, + "optional_inputs": { + "title": "Optional inputs", + "type": "object", + "description": "Files that can be provided as optional input or omitted.", + "default": "", + "fa_icon": "fas fa-terminal", + "properties": { + "initial_weights": { + "type": "string", + "fa_icon": "fas fa-folder-open", + "help_text": "The initial weights of the model. These files can be used to start the training instead of random initialization. One can provide several files, each of them will be used for a different run.", + "description": "File to be used to initialize the model in tuning", + "format": "path" + } + } }, - "exp_conf": { - "type": "string", - "format": "file-path", - "description": "Experiment config as JSON format", - "fa_icon": "fas fa-folder-open", - "help_text": "the json config file that specifies all the parameters relative to the data manipulation." + "resources_options": { + "title": "Resources options", + "type": "object", + "description": "Specify maximun processes resources", + "default": "", + "properties": { + "max_gpus": { + "type": "integer", + "default": 1, + "minimum": 0, + "help_text": "requesting the gpus for the tuning steps.", + "description": "set maximum GPU limit" + } + } }, - "model": { - "type": "string", - "format": "file-path", - "description": "Model file in Python", - "fa_icon": "fas fa-folder-open", - "mimetype": "text/py", - "help_text": "the model file in python, the model that will be tested by this pipeline." + "on_error_options": { + "title": "On error options", + "type": "object", + "description": "What to do and how to handle errors", + "default": "", + "fa_icon": "fas fa-terminal", + "properties": { + "err_start": { + "type": "string", + "default": "finish", + "description": "Tells the pipeline how to behave on error", + "help_text": "refer to nextflow errorStrategy documentation for more details." + }, + "max_retries": { + "type": "integer", + "default": 0, + "description": "number of time to retry if err_strat is\u00a0set to retry", + "help_text": "this also acts as a multiplier for recources request. If it failed for lack of resources it automaticly asks more the second time. take a look at test.conf for more details." + } + } }, - "tune_conf": { - "type": "string", - "format": "file-path", - "description": "Tuning config in yaml format", - "fa_icon": "fas fa-folder-open", - "mimetype": "text/yaml", - "help_text": "the config file with all the hyperparameter directives (choiches) and all ray tune specs." + "skip_options": { + "title": "Skip options", + "type": "object", + "description": "options to skip or change bhaviour of pipeline", + "default": "", + "fa_icon": "fas fa-terminal", + "properties": { + "check_model": { + "type": "boolean", + "default": true, + "description": "checks if all input are comatible and the model can be tuned.", + "help_text": "flag to tell whether to check or not if the model can be tuned and trained. It does one call of the batch function, (predicting), of the model importing and using everything needed for that. Default run such a check." + }, + "check_model_num_samples": { + "type": "string", + "description": "optional flag to do a more/less extensive check during check_model.", + "help_text": "This will override user given num_sample value for the tune run. This will give the user control on how extensive it wants the check to be. by default is going to be set to 3." + }, + "shuffle": { + "type": "boolean", + "default": true, + "description": "run the shuffle sanity check", + "help_text": "flag to tell wether to shuffle or not the data and run a train on it. Sanity check always run on default. " + }, + "debug_mode": { + "type": "boolean", + "description": "developer flag", + "help_text": "flag used to switch to debug mode for the pipeline. more verbose outputs." + } + } }, - "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.", - "fa_icon": "fas fa-folder-open", - "help_text": "The directory will contain a subdirectory with a name unique to each stimulus pipeline run." + "general_options": { + "title": "General options", + "type": "object", + "description": "generic options", + "default": "", + "fa_icon": "fas fa-terminal", + "properties": { + "validate_params": { + "type": "boolean", + "description": "to validate or not the input params", + "default": true + } + } }, - "email": { + "institutional_config_options": { + "title": "Institutional config options", + "type": "object", + "fa_icon": "fas fa-university", + "description": "Parameters used to describe centralised config profiles. These should not be edited.", + "help_text": "The centralised nf-core configuration profiles use a handful of pipeline parameters to describe themselves. This information is then printed to the Nextflow log when you run a pipeline. You should not need to change these values when you run a pipeline.", + "properties": { + "custom_config_version": { "type": "string", - "description": "Email address for completion summary.", - "fa_icon": "fas fa-envelope", - "help_text": "Set this parameter to your e-mail address to get a summary e-mail with details of the run sent to you when the workflow exits. If set in your user config file (`~/.nextflow/config`) then you don't need to specify this on the command line for every run.", - "pattern": "^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,5})$" - } - } - }, - "optional_inputs": { - "title": "Optional inputs", - "type": "object", - "description": "Files that can be provided as optional input or omitted.", - "default": "", - "fa_icon": "fas fa-terminal", - "properties": { - "initial_weights": { - "type": "string", - "fa_icon": "fas fa-folder-open", - "help_text": "The initial weights of the model. These files can be used to start the training instead of random initialization. One can provide several files, each of them will be used for a different run.", - "description": "File to be used to initialize the model in tuning", - "format": "path" - } - } - }, - "resources_options": { - "title": "Resources options", - "type": "object", - "description": "Specify maximun processes resources", - "default": "", - "properties": { - "max_gpus": { - "type": "integer", - "default": 1, - "minimum": 0, - "help_text": "requesting the gpus for the tuning steps.", - "description": "set maximum GPU limit" - } - } - }, - "on_error_options": { - "title": "On error options", - "type": "object", - "description": "What to do and how to handle errors", - "default": "", - "fa_icon": "fas fa-terminal", - "properties": { - "err_start": { - "type": "string", - "default": "finish", - "description": "Tells the pipeline how to behave on error", - "help_text": "refer to nextflow errorStrategy documentation for more details." + "description": "Git commit id for Institutional configs.", + "default": "master", + "hidden": true, + "fa_icon": "fas fa-users-cog" + }, + "custom_config_base": { + "type": "string", + "description": "Base directory for Institutional configs.", + "default": "https://raw.githubusercontent.com/nf-core/configs/master", + "hidden": true, + "help_text": "If you're running offline, Nextflow will not be able to fetch the institutional config files from the internet. If you don't need them, then this is not a problem. If you do need them, you should download the files from the repo and tell Nextflow where to find them with this parameter.", + "fa_icon": "fas fa-users-cog" + }, + "config_profile_name": { + "type": "string", + "description": "Institutional config name.", + "hidden": true, + "fa_icon": "fas fa-users-cog" + }, + "config_profile_description": { + "type": "string", + "description": "Institutional config description.", + "hidden": true, + "fa_icon": "fas fa-users-cog" + }, + "config_profile_contact": { + "type": "string", + "description": "Institutional config contact information.", + "hidden": true, + "fa_icon": "fas fa-users-cog" + }, + "config_profile_url": { + "type": "string", + "description": "Institutional config URL link.", + "hidden": true, + "fa_icon": "fas fa-users-cog" + } + } }, - "max_retries": { - "type": "integer", - "default": 0, - "description": "number of time to retry if err_strat is\u00a0set to retry", - "help_text": "this also acts as a multiplier for recources request. If it failed for lack of resources it automaticly asks more the second time. take a look at test.conf for more details." + "generic_options": { + "title": "Generic options", + "type": "object", + "fa_icon": "fas fa-file-import", + "description": "Less common options for the pipeline, typically set in a config file.", + "help_text": "These options are common to all nf-core pipelines and allow you to customise some of the core preferences for how the pipeline runs.\n\nTypically these options would be set in a Nextflow config file loaded for all pipeline runs, such as `~/.nextflow/config`.", + "properties": { + "version": { + "type": "boolean", + "description": "Display version and exit.", + "fa_icon": "fas fa-question-circle", + "hidden": true + }, + "publish_dir_mode": { + "type": "string", + "default": "copy", + "description": "Method used to save pipeline results to output directory.", + "help_text": "The Nextflow `publishDir` option specifies which intermediate files should be saved to the output directory. This option tells the pipeline what method should be used to move these files. See [Nextflow docs](https://www.nextflow.io/docs/latest/process.html#publishdir) for details.", + "fa_icon": "fas fa-copy", + "enum": ["symlink", "rellink", "link", "copy", "copyNoFollow", "move"], + "hidden": true + }, + "email_on_fail": { + "type": "string", + "description": "Email address for completion summary, only when pipeline fails.", + "fa_icon": "fas fa-exclamation-triangle", + "pattern": "^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,5})$", + "help_text": "An email address to send a summary email to when the pipeline is completed - ONLY sent if the pipeline does not exit successfully.", + "hidden": true + }, + "plaintext_email": { + "type": "boolean", + "description": "Send plain-text email instead of HTML.", + "fa_icon": "fas fa-remove-format", + "hidden": true + }, + "monochrome_logs": { + "type": "boolean", + "description": "Do not use coloured log outputs.", + "fa_icon": "fas fa-palette", + "hidden": true + }, + "hook_url": { + "type": "string", + "description": "Incoming hook URL for messaging service", + "fa_icon": "fas fa-people-group", + "help_text": "Incoming hook URL for messaging service. Currently, MS Teams and Slack are supported.", + "hidden": true + }, + "validate_params": { + "type": "boolean", + "description": "Boolean whether to validate parameters against the schema at runtime", + "default": true, + "fa_icon": "fas fa-check-square", + "hidden": true + }, + "pipelines_testdata_base_path": { + "type": "string", + "fa_icon": "far fa-check-circle", + "description": "Base URL or local path to location of pipeline test dataset files", + "default": "https://raw.githubusercontent.com/nf-core/test-datasets/", + "hidden": true + } + } } - } }, - "skip_options": { - "title": "Skip options", - "type": "object", - "description": "options to skip or change bhaviour of pipeline", - "default": "", - "fa_icon": "fas fa-terminal", - "properties": { - "check_model": { - "type": "boolean", - "default": true, - "description": "checks if all input are comatible and the model can be tuned.", - "help_text": "flag to tell whether to check or not if the model can be tuned and trained. It does one call of the batch function, (predicting), of the model importing and using everything needed for that. Default run such a check." + "allOf": [ + { + "$ref": "#/$defs/input_output_options" }, - "check_model_num_samples": { - "type": "string", - "description": "optional flag to do a more/less extensive check during check_model.", - "help_text": "This will override user given num_sample value for the tune run. This will give the user control on how extensive it wants the check to be. by default is going to be set to 3." + { + "$ref": "#/$defs/optional_inputs" }, - "shuffle": { - "type": "boolean", - "default": true, - "description": "run the shuffle sanity check", - "help_text": "flag to tell wether to shuffle or not the data and run a train on it. Sanity check always run on default. " + { + "$ref": "#/$defs/resources_options" }, - "debug_mode": { - "type": "boolean", - "description": "developer flag", - "help_text": "flag used to switch to debug mode for the pipeline. more verbose outputs." - } - } - }, - "general_options": { - "title": "General options", - "type": "object", - "description": "generic options", - "default": "", - "fa_icon": "fas fa-terminal", - "properties": { - "validate_params": { - "type": "boolean", - "description": "to validate or not the input params", - "default": true + { + "$ref": "#/$defs/on_error_options" + }, + { + "$ref": "#/$defs/skip_options" + }, + { + "$ref": "#/$defs/general_options" + }, + { + "$ref": "#/$defs/institutional_config_options" + }, + { + "$ref": "#/$defs/generic_options" } - } - }, - "institutional_config_options": { - "title": "Institutional config options", - "type": "object", - "fa_icon": "fas fa-university", - "description": "Parameters used to describe centralised config profiles. These should not be edited.", - "help_text": "The centralised nf-core configuration profiles use a handful of pipeline parameters to describe themselves. This information is then printed to the Nextflow log when you run a pipeline. You should not need to change these values when you run a pipeline.", - "properties": { - "custom_config_version": { - "type": "string", - "description": "Git commit id for Institutional configs.", - "default": "master", - "hidden": true, - "fa_icon": "fas fa-users-cog" - }, - "custom_config_base": { - "type": "string", - "description": "Base directory for Institutional configs.", - "default": "https://raw.githubusercontent.com/nf-core/configs/master", - "hidden": true, - "help_text": "If you're running offline, Nextflow will not be able to fetch the institutional config files from the internet. If you don't need them, then this is not a problem. If you do need them, you should download the files from the repo and tell Nextflow where to find them with this parameter.", - "fa_icon": "fas fa-users-cog" - }, - "config_profile_name": { - "type": "string", - "description": "Institutional config name.", - "hidden": true, - "fa_icon": "fas fa-users-cog" - }, - "config_profile_description": { - "type": "string", - "description": "Institutional config description.", - "hidden": true, - "fa_icon": "fas fa-users-cog" - }, - "config_profile_contact": { - "type": "string", - "description": "Institutional config contact information.", - "hidden": true, - "fa_icon": "fas fa-users-cog" - }, - "config_profile_url": { - "type": "string", - "description": "Institutional config URL link.", - "hidden": true, - "fa_icon": "fas fa-users-cog" - } - } - }, - "generic_options": { - "title": "Generic options", - "type": "object", - "fa_icon": "fas fa-file-import", - "description": "Less common options for the pipeline, typically set in a config file.", - "help_text": "These options are common to all nf-core pipelines and allow you to customise some of the core preferences for how the pipeline runs.\n\nTypically these options would be set in a Nextflow config file loaded for all pipeline runs, such as `~/.nextflow/config`.", - "properties": { - "version": { - "type": "boolean", - "description": "Display version and exit.", - "fa_icon": "fas fa-question-circle", - "hidden": true - }, - "publish_dir_mode": { - "type": "string", - "default": "copy", - "description": "Method used to save pipeline results to output directory.", - "help_text": "The Nextflow `publishDir` option specifies which intermediate files should be saved to the output directory. This option tells the pipeline what method should be used to move these files. See [Nextflow docs](https://www.nextflow.io/docs/latest/process.html#publishdir) for details.", - "fa_icon": "fas fa-copy", - "enum": ["symlink", "rellink", "link", "copy", "copyNoFollow", "move"], - "hidden": true - }, - "email_on_fail": { - "type": "string", - "description": "Email address for completion summary, only when pipeline fails.", - "fa_icon": "fas fa-exclamation-triangle", - "pattern": "^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,5})$", - "help_text": "An email address to send a summary email to when the pipeline is completed - ONLY sent if the pipeline does not exit successfully.", - "hidden": true - }, - "plaintext_email": { - "type": "boolean", - "description": "Send plain-text email instead of HTML.", - "fa_icon": "fas fa-remove-format", - "hidden": true - }, - "monochrome_logs": { - "type": "boolean", - "description": "Do not use coloured log outputs.", - "fa_icon": "fas fa-palette", - "hidden": true - }, - "hook_url": { - "type": "string", - "description": "Incoming hook URL for messaging service", - "fa_icon": "fas fa-people-group", - "help_text": "Incoming hook URL for messaging service. Currently, MS Teams and Slack are supported.", - "hidden": true - }, - "validate_params": { - "type": "boolean", - "description": "Boolean whether to validate parameters against the schema at runtime", - "default": true, - "fa_icon": "fas fa-check-square", - "hidden": true - }, - "pipelines_testdata_base_path": { - "type": "string", - "fa_icon": "far fa-check-circle", - "description": "Base URL or local path to location of pipeline test dataset files", - "default": "https://raw.githubusercontent.com/nf-core/test-datasets/", - "hidden": true - } - } - } - }, - "allOf": [ - { - "$ref": "#/$defs/input_output_options" - }, - { - "$ref": "#/$defs/optional_inputs" - }, - { - "$ref": "#/$defs/resources_options" - }, - { - "$ref": "#/$defs/on_error_options" - }, - { - "$ref": "#/$defs/skip_options" - }, - { - "$ref": "#/$defs/general_options" - }, - { - "$ref": "#/$defs/institutional_config_options" - }, - { - "$ref": "#/$defs/generic_options" - } - ] + ] } From a677c873da9dac6a59bb95797ad68ab5fc15f199 Mon Sep 17 00:00:00 2001 From: JoseEspinosa Date: Mon, 21 Oct 2024 11:47:07 +0200 Subject: [PATCH 10/25] Make lint happy --- Dockerfiles/ps/Dockerfile | 2 +- Dockerfiles/stimulus/Dockerfile | 2 +- bin/tests/test_data/dna_experiment/test.csv | 2 +- .../test_data/dna_experiment/test_config.json | 2 +- .../dna_experiment/test_unequal_dna_float.csv | 2 +- .../dna_experiment/test_with_split.csv | 2 +- .../test_data/prot_dna_experiment/test.csv | 2 +- .../prot_dna_experiment/test_config.json | 2 +- .../prot_dna_experiment/test_with_split.csv | 2 +- .../titanic/process_titanic_to_stimulus.py | 4 +-- .../test_data/titanic/titanic_stimulus.json | 2 +- bin/tests/test_model/dnatofloat_model.py | 22 +++++++------- bin/tests/test_model/titanic_model.py | 4 +-- conf/crg.config | 20 ++++++------- conf/crg_slurm.config | 22 +++++++------- conf/local.config | 26 ++++++++--------- conf/modules.config | 10 +++---- conf/test_learn.config | 2 +- main.nf | 2 +- modules/local/check_torch_model.nf | 2 +- modules/local/interpret_json.nf | 4 +-- modules/local/stimulus_analysis_default.nf | 14 ++++----- modules/local/stimulus_split_csv.nf | 2 +- modules/local/torch_tune.nf | 29 +++++++++---------- nextflow.config | 18 ++++++------ subworkflows/local/check_model/main.nf | 15 +++++----- subworkflows/local/handle_analysis/main.nf | 8 ++--- subworkflows/local/handle_data/main.nf | 14 ++++----- subworkflows/local/handle_tune/main.nf | 10 +++---- subworkflows/local/shuffle_csv/main.nf | 4 +-- subworkflows/local/transform_csv/main.nf | 2 +- .../main.nf | 16 +++++----- tests/handle_tune.nf.test | 4 +-- tests/nextflow.config | 2 +- workflows/deepmodeloptim.nf | 10 +++---- 35 files changed, 141 insertions(+), 145 deletions(-) diff --git a/Dockerfiles/ps/Dockerfile b/Dockerfiles/ps/Dockerfile index 349d802..54e6acb 100644 --- a/Dockerfiles/ps/Dockerfile +++ b/Dockerfiles/ps/Dockerfile @@ -12,4 +12,4 @@ RUN micromamba install -y -n base -c defaults -c bioconda -c conda-forge \ && micromamba clean -a -y ENV PATH="$MAMBA_ROOT_PREFIX/bin:$PATH" -USER root \ No newline at end of file +USER root diff --git a/Dockerfiles/stimulus/Dockerfile b/Dockerfiles/stimulus/Dockerfile index df3353e..625715d 100644 --- a/Dockerfiles/stimulus/Dockerfile +++ b/Dockerfiles/stimulus/Dockerfile @@ -18,4 +18,4 @@ RUN pip install \ ray[train,tune,default]==2.23.0 \ matplotlib==3.9.0 \ pandas==2.2.0 \ - safetensors==0.4.5 + safetensors==0.4.5 diff --git a/bin/tests/test_data/dna_experiment/test.csv b/bin/tests/test_data/dna_experiment/test.csv index bcf7c9c..7e089cd 100644 --- a/bin/tests/test_data/dna_experiment/test.csv +++ b/bin/tests/test_data/dna_experiment/test.csv @@ -1,3 +1,3 @@ hello:input:dna,hola:label:float,pet:meta:str ACTGACTGATCGATGC,12,cat -ACTGACTGATCGATGC,12,dog \ No newline at end of file +ACTGACTGATCGATGC,12,dog diff --git a/bin/tests/test_data/dna_experiment/test_config.json b/bin/tests/test_data/dna_experiment/test_config.json index 352e25e..27fb044 100644 --- a/bin/tests/test_data/dna_experiment/test_config.json +++ b/bin/tests/test_data/dna_experiment/test_config.json @@ -15,7 +15,7 @@ "column_name": "hello:input:dna", "name": "ReverseComplement", "params": {} - } + } ], "split": { "name": "RandomSplitter", diff --git a/bin/tests/test_data/dna_experiment/test_unequal_dna_float.csv b/bin/tests/test_data/dna_experiment/test_unequal_dna_float.csv index f253bef..d1bbea4 100644 --- a/bin/tests/test_data/dna_experiment/test_unequal_dna_float.csv +++ b/bin/tests/test_data/dna_experiment/test_unequal_dna_float.csv @@ -2,4 +2,4 @@ hello:input:dna,hola:label:float ACTGACTGATCGATGC,5 ACTGACTGATCGATGC,5 ATCAGTCAG,2.3 -aTCnndhaksdhjtcgaysdgyagctaggat,10 \ No newline at end of file +aTCnndhaksdhjtcgaysdgyagctaggat,10 diff --git a/bin/tests/test_data/dna_experiment/test_with_split.csv b/bin/tests/test_data/dna_experiment/test_with_split.csv index 9de3a6a..718bbd2 100644 --- a/bin/tests/test_data/dna_experiment/test_with_split.csv +++ b/bin/tests/test_data/dna_experiment/test_with_split.csv @@ -46,4 +46,4 @@ CGGTAGTTCACTGAC,1,2,bird CCGGAAGTTCACTGA,1,2,bird TCCGTAAGTTCACTG,1,2,bird ATCGGTAAGTTCACT,1,2,bird -ATCCGGTAAGTTCAC,1,2,bird \ No newline at end of file +ATCCGGTAAGTTCAC,1,2,bird diff --git a/bin/tests/test_data/prot_dna_experiment/test.csv b/bin/tests/test_data/prot_dna_experiment/test.csv index 2157549..d6cb3b5 100644 --- a/bin/tests/test_data/prot_dna_experiment/test.csv +++ b/bin/tests/test_data/prot_dna_experiment/test.csv @@ -1,3 +1,3 @@ hello:input:dna,bonjour:input:prot,hola:label:float,pet:meta:str ACTGACTGATCGATGC,GPRTTIKAKQLETLK,12,cat -ACTGACTGATCGATGC,GPRTTIKAKQLETLK,12,dog \ No newline at end of file +ACTGACTGATCGATGC,GPRTTIKAKQLETLK,12,dog diff --git a/bin/tests/test_data/prot_dna_experiment/test_config.json b/bin/tests/test_data/prot_dna_experiment/test_config.json index 1a04e20..9979a5c 100644 --- a/bin/tests/test_data/prot_dna_experiment/test_config.json +++ b/bin/tests/test_data/prot_dna_experiment/test_config.json @@ -20,7 +20,7 @@ "column_name": "hello:input:dna", "name": "ReverseComplement", "params": {} - } + } ], "split": { "name": "RandomSplitter", diff --git a/bin/tests/test_data/prot_dna_experiment/test_with_split.csv b/bin/tests/test_data/prot_dna_experiment/test_with_split.csv index ab546e9..a6a1fd5 100644 --- a/bin/tests/test_data/prot_dna_experiment/test_with_split.csv +++ b/bin/tests/test_data/prot_dna_experiment/test_with_split.csv @@ -1,4 +1,4 @@ hello:input:dna,bonjour:input:prot,hola:label:float,split:split:int,pet:meta:str ACTGACTGATCGATGC,GPRTTIKAKQLETLK,12,0,cat ACTGACTGATCGATGC,GPRTTIKAKQLETLK,12,1,dog -ACTGACTGATCGATGC,GPRTTIKAKQLETLK,12,2,bird \ No newline at end of file +ACTGACTGATCGATGC,GPRTTIKAKQLETLK,12,2,bird diff --git a/bin/tests/test_data/titanic/process_titanic_to_stimulus.py b/bin/tests/test_data/titanic/process_titanic_to_stimulus.py index 2c96788..211ad63 100644 --- a/bin/tests/test_data/titanic/process_titanic_to_stimulus.py +++ b/bin/tests/test_data/titanic/process_titanic_to_stimulus.py @@ -5,7 +5,7 @@ def arg_parser(): parser = argparse.ArgumentParser(description="Process Titanic dataset to stimulus format") parser.add_argument("--input", type=str, help="Path to input csv file, should be identical to Kaggle download of the Titanic dataset, see : https://www.kaggle.com/c/titanic/data", required=True) parser.add_argument("--output", type=str, help="Path to output csv file", default="titanic_stimulus.csv", required=False) - return parser.parse_args() + return parser.parse_args() def main(): args = arg_parser() @@ -41,4 +41,4 @@ def main(): df.write_csv(args.output) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/bin/tests/test_data/titanic/titanic_stimulus.json b/bin/tests/test_data/titanic/titanic_stimulus.json index 92e4608..0d3b5f2 100644 --- a/bin/tests/test_data/titanic/titanic_stimulus.json +++ b/bin/tests/test_data/titanic/titanic_stimulus.json @@ -1,6 +1,6 @@ { "experiment": "TitanicExperiment", - "interpret_params_mode": "column_wise", + "interpret_params_mode": "column_wise", "split": [ { "name": "RandomSplitter", diff --git a/bin/tests/test_model/dnatofloat_model.py b/bin/tests/test_model/dnatofloat_model.py index 3f48ae9..fed761e 100644 --- a/bin/tests/test_model/dnatofloat_model.py +++ b/bin/tests/test_model/dnatofloat_model.py @@ -1,14 +1,14 @@ -import torch +import torch import torch.nn as nn from typing import Callable, Optional, Tuple - + class ModelSimple(torch.nn.Module): """ A simple model example. - It takes as input a 1D tensor of any size, - apply some convolutional layer and + It takes as input a 1D tensor of any size, + apply some convolutional layer and outputs a single value using a maxpooling layer and a softmax function. - + All functions `forward`, `compute_loss` and `batch` need to be implemented for any new model. """ def __init__(self, kernel_size: int = 3, pool_size: int = 2): @@ -18,20 +18,20 @@ def __init__(self, kernel_size: int = 3, pool_size: int = 2): self.softmax = nn.Softmax(dim=1) # had to change to 6 because dna sequence is shoprter self.linear = nn.Linear(6, 1) - + def forward(self, hello: torch.Tensor) -> dict: """ Forward pass of the model. It should return the output as a dictionary, with the same keys as `y`. """ - x = hello.permute(0, 2, 1).to(torch.float32) # permute the two last dimensions of hello + x = hello.permute(0, 2, 1).to(torch.float32) # permute the two last dimensions of hello x = self.conv1(x) x = self.pool(x) x = self.softmax(x) x = self.linear(x) x = x.squeeze() return x - + def compute_loss(self, output: torch.Tensor, hola: torch.Tensor, loss_fn: Callable) -> torch.Tensor: """ Compute the loss. @@ -40,14 +40,14 @@ def compute_loss(self, output: torch.Tensor, hola: torch.Tensor, loss_fn: Callab `loss_fn` is the loss function to be used. """ return loss_fn(output, hola.to(torch.float32)) - + def batch(self, x: dict, y: dict, loss_fn1: Callable, loss_fn2: Callable, optimizer: Optional[Callable] = None) -> Tuple[torch.Tensor, dict]: """ Perform one batch step. `x` is a dictionary with the input tensors. `y` is a dictionary with the target tensors. `loss_fn1` and `loss_fn2` are the loss function to be used. - + If `optimizer` is passed, it will perform the optimization step -> training step Otherwise, only return the forward pass output and loss -> evaluation step @@ -62,4 +62,4 @@ def batch(self, x: dict, y: dict, loss_fn1: Callable, loss_fn2: Callable, optimi loss1.backward(retain_graph=True) loss2.backward(retain_graph=True) optimizer.step() - return loss1, output \ No newline at end of file + return loss1, output diff --git a/bin/tests/test_model/titanic_model.py b/bin/tests/test_model/titanic_model.py index 8a5f4d8..426a4ba 100644 --- a/bin/tests/test_model/titanic_model.py +++ b/bin/tests/test_model/titanic_model.py @@ -28,7 +28,7 @@ def forward(self, pclass: torch.Tensor, sex: torch.Tensor, age: torch.Tensor, si x = self.relu(layer(x)) x = self.softmax(self.output_layer(x)) return x - + def compute_loss(self, output: torch.Tensor, survived: torch.Tensor, loss_fn: Callable) -> torch.Tensor: """ Compute the loss. @@ -37,7 +37,7 @@ def compute_loss(self, output: torch.Tensor, survived: torch.Tensor, loss_fn: Ca `loss_fn` is the loss function to be used. """ return loss_fn(output, survived) - + def batch(self, x: dict, y: dict, loss_fn: Callable, optimizer: Optional[Callable] = None) -> Tuple[torch.Tensor, dict]: """ Perform one batch step. diff --git a/conf/crg.config b/conf/crg.config index bac7b3c..4910b93 100644 --- a/conf/crg.config +++ b/conf/crg.config @@ -1,6 +1,6 @@ params { - config_profile_name = 'CRG profile' - config_profile_description = 'Configuration to run on CRG cluster' + config_profile_name = 'CRG profile' + config_profile_description = 'Configuration to run on CRG cluster' } @@ -16,16 +16,16 @@ process { errorStrategy = params.err_start withLabel:process_low { - queue = 'cn-el7,short-centos79' - cpus = { 1 } - memory = { 4.GB * task.attempt } - time = { 1.h * task.attempt } + queue = 'cn-el7,short-centos79' + cpus = { 1 } + memory = { 4.GB * task.attempt } + time = { 1.h * task.attempt } } withLabel:process_medium{ - queue = 'cn-el7,short-centos79' - cpus = { 4 } - memory = { 10.GB * task.attempt } - time = { 6.h * task.attempt } + queue = 'cn-el7,short-centos79' + cpus = { 4 } + memory = { 10.GB * task.attempt } + time = { 6.h * task.attempt } } withLabel:process_high { queue = 'cn-el7,long-centos79' diff --git a/conf/crg_slurm.config b/conf/crg_slurm.config index 5e23bce..be803d9 100644 --- a/conf/crg_slurm.config +++ b/conf/crg_slurm.config @@ -1,6 +1,6 @@ params { - config_profile_name = 'CRG profile - slurm new cluster' - config_profile_description = 'Configuration to run on CRG new cluster' + config_profile_name = 'CRG profile - slurm new cluster' + config_profile_description = 'Configuration to run on CRG new cluster' } @@ -15,18 +15,18 @@ process { executor = "slurm" maxRetries = params.max_retries errorStrategy = params.err_start - + withLabel:process_low { - clusterOptions = '--qos=shorter' - cpus = { 1 } - memory = { 4.GB * task.attempt } - time = { 1.h * task.attempt } + clusterOptions = '--qos=shorter' + cpus = { 1 } + memory = { 4.GB * task.attempt } + time = { 1.h * task.attempt } } withLabel:process_medium{ - clusterOptions = '--qos=short' - cpus = { 4 } - memory = { 10.GB * task.attempt } - time = { 6.h * task.attempt } + clusterOptions = '--qos=short' + cpus = { 4 } + memory = { 10.GB * task.attempt } + time = { 6.h * task.attempt } } withLabel:process_high { clusterOptions = '--qos=normal' diff --git a/conf/local.config b/conf/local.config index 67d2330..b1bf94e 100644 --- a/conf/local.config +++ b/conf/local.config @@ -1,27 +1,27 @@ params { - config_profile_name = 'Local profile' - config_profile_description = 'Configuration to run on local machine' + config_profile_name = 'Local profile' + config_profile_description = 'Configuration to run on local machine' } process { - maxRetries = params.max_retries +maxRetries = params.max_retries errorStrategy = params.err_start withLabel:process_low { - cpus = { 1 } - memory = { 4.GB * task.attempt } - time = { 1.h * task.attempt } - } + cpus = { 1 } + memory = { 4.GB * task.attempt } + time = { 1.h * task.attempt } + } withLabel:process_medium{ - cpus = { 4 } - memory = { 10.GB * task.attempt } - time = { 6.h * task.attempt } + cpus = { 4 } + memory = { 10.GB * task.attempt } + time = { 6.h * task.attempt } } withLabel:process_medium_high { - cpus = { 12 } - memory = { 50.GB * task.attempt } - time = { 12.h * task.attempt } + cpus = { 12 } + memory = { 50.GB * task.attempt } + time = { 12.h * task.attempt } } } diff --git a/conf/modules.config b/conf/modules.config index 11c16c9..92efa83 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -38,9 +38,9 @@ process { // the prefix is the input data csv filename without last extantion ( all other dots are changed to underscores) + the specific experimental config json name (dynamically created during interpret json step) ext.prefix = { "${combination_key}".split(' ')[0].split('\\.')[0..-2].join("_") + "-" + "${combination_key}".split(' ')[2] } - + // the output directory is called by the unique name of the run + the time of launch. - // all subidrs of the output dir are called with the same schema of the prefix above, which makes them process instance specific (aka unique). + // all subidrs of the output dir are called with the same schema of the prefix above, which makes them process instance specific (aka unique). // at the time of publishing, files are copied and then renamed to a standard naming convention based on their extention and sffixes. This is done through saveAs mode. The files need to be coopied because the original file unrenamed will be further used by the pipeline and it's name has to stay unique (thanks to the prefix), otherwise there wil be filename confilcts. publishDir = [ path: { "${params.outdir}/${workflow.runName}_" + "${workflow.start}/".replaceAll('[-:]', '_').split('\\.')[0] + "/" + "${combination_key}".split(' ')[0].split('\\.')[0..-2].join("_") + "-" + "${combination_key}".split(' ')[2] }, @@ -53,15 +53,15 @@ process { else if (filename.endsWith(".csv")) "training_data.csv" else if (filename.endsWith(".json")) "experiment_config.json" else filename - } + } ] } withName: "STIMULUS_ANALYSIS_DEFAULT" { // the output directory is called by the unique name of the run + the time of launch. - // it has subdirs that identify the class of analysises like -> /analysis_default/. - // the subsubdirs have the name of the split informstion used to create the test set. So conceptually all analyses carried on the same test set are under the same subsubdir. + // it has subdirs that identify the class of analysises like -> /analysis_default/. + // the subsubdirs have the name of the split informstion used to create the test set. So conceptually all analyses carried on the same test set are under the same subsubdir. publishDir = [ path: { "${params.outdir}/${workflow.runName}_" + "${workflow.start}/".replaceAll('[-:]', '_').split('\\.')[0] + "/analysis_default/" + "${split_transform_key}" }, mode: params.publish_dir_mode, diff --git a/conf/test_learn.config b/conf/test_learn.config index f615990..31f7217 100644 --- a/conf/test_learn.config +++ b/conf/test_learn.config @@ -5,7 +5,7 @@ params { config_profile_name = 'Test Learn profile' config_profile_description = 'Minimal test dataset to check if a model that should learn actually does' - + // Input data csv = "${projectDir}/bin/tests/test_data/titanic/titanic_stimulus.csv" exp_conf = "${projectDir}/bin/tests/test_data/titanic/titanic_stimulus.json" // TODO make the data transformations available to titanic data types diff --git a/main.nf b/main.nf index 28def03..f8a4eb8 100644 --- a/main.nf +++ b/main.nf @@ -73,7 +73,7 @@ workflow { params.outdir //, // params.input ) - + // // WORKFLOW: Run main workflow // diff --git a/modules/local/check_torch_model.nf b/modules/local/check_torch_model.nf index aad9f67..75caac7 100644 --- a/modules/local/check_torch_model.nf +++ b/modules/local/check_torch_model.nf @@ -5,7 +5,7 @@ process CHECK_TORCH_MODEL { label 'process_medium' // TODO: push image to nf-core quay.io container "docker.io/mathysgrapotte/stimulus-py:latest" - + input: tuple path(original_csv), path(model), path(experiment_config), path(ray_tune_config), path(initial_weights) diff --git a/modules/local/interpret_json.nf b/modules/local/interpret_json.nf index daad7d9..e2fac07 100644 --- a/modules/local/interpret_json.nf +++ b/modules/local/interpret_json.nf @@ -5,7 +5,7 @@ process INTERPRET_JSON { label 'process_low' // TODO: push image to nf-core quay.io container "docker.io/mathysgrapotte/stimulus-py:latest" - + input: path user_json val message_from_check_model // only here to ensure that this module waits for check_model module to actually run @@ -17,7 +17,7 @@ process INTERPRET_JSON { script: """ - stimulus-interpret-json -j ${user_json} + stimulus-interpret-json -j ${user_json} """ stub: diff --git a/modules/local/stimulus_analysis_default.nf b/modules/local/stimulus_analysis_default.nf index 619ee00..1ca0793 100644 --- a/modules/local/stimulus_analysis_default.nf +++ b/modules/local/stimulus_analysis_default.nf @@ -8,13 +8,13 @@ process STIMULUS_ANALYSIS_DEFAULT { input: tuple val(split_transform_key), \ - val(combination_key), \ - path(data), \ - path(experiment_config), \ - path(model_config), \ - path(weights), \ - path(optimizer), \ - path(metrics) + val(combination_key), \ + path(data), \ + path(experiment_config), \ + path(model_config), \ + path(weights), \ + path(optimizer), \ + path(metrics) path(model) output: diff --git a/modules/local/stimulus_split_csv.nf b/modules/local/stimulus_split_csv.nf index d505668..a9148e2 100644 --- a/modules/local/stimulus_split_csv.nf +++ b/modules/local/stimulus_split_csv.nf @@ -1,6 +1,6 @@ process STIMULUS_SPLIT_CSV { - + tag "${original_csv} - ${split_transform_key}" label 'process_low' // TODO: push image to nf-core quay.io diff --git a/modules/local/torch_tune.nf b/modules/local/torch_tune.nf index bcad1d8..123e5dd 100644 --- a/modules/local/torch_tune.nf +++ b/modules/local/torch_tune.nf @@ -11,23 +11,22 @@ process TORCH_TUNE { output: tuple val(split_transform_key), - val(combination_key), - path(data_csv), - path(experiment_config), - path("*-config.json"), - path("*-model.safetensors"), - path("*-optimizer.pt"), - path("*-metrics.csv"), - path(initial_weights), - emit: tune_specs + val(combination_key), + path(data_csv), + path(experiment_config), + path("*-config.json"), + path("*-model.safetensors"), + path("*-optimizer.pt"), + path("*-metrics.csv"), + path(initial_weights), + emit: tune_specs // output the debug files if they are present, making this an optional channel tuple val("debug_${prefix}"), - path("ray_results/*/debug/best_model_*.txt"), - path("ray_results/*/debug/worker_with_seed_*/model.safetensors"), - path("ray_results/*/debug/worker_with_seed_*/seeds.txt"), - emit: debug, - optional: true - + path("ray_results/*/debug/best_model_*.txt"), + path("ray_results/*/debug/worker_with_seed_*/model.safetensors"), + path("ray_results/*/debug/worker_with_seed_*/seeds.txt"), + emit: debug, + optional: true script: // prefix should be global so that is seen in the output section diff --git a/nextflow.config b/nextflow.config index e749ea8..d433729 100644 --- a/nextflow.config +++ b/nextflow.config @@ -15,7 +15,7 @@ params { model = null // the model file in python, the model that will be tested by this pipeline exp_conf = null // the json config file that specifies all the parameters relative to the data manipulation tune_conf = null // the config file with all the hyperparameter directives (choiches) and all ray tune specs - + // Optional inputs initial_weights = null // the initial weights of the model. These files can be used to start the training instead of random initialization. One can provide several files, each of them will be used for a different run. @@ -23,10 +23,10 @@ params { outdir = "./results/" // the outdir has to be the one the user specify _ the unique name of the run _ the time so that multiple runs will not overlap publish_dir_mode = "copy" - // Computational resources - max_gpus = 1 // requesting the gpus for the tuning steps. + // Computational resources + max_gpus = 1 // requesting the gpus for the tuning steps. // TODO this should not be in the nextflow config but in each config - // max_cpus = 12 // this flasg and the following are for regulating resources, profiles can overwrite these. + // max_cpus = 12 // this flasg and the following are for regulating resources, profiles can overwrite these. // max_memory = 32.GB // max_time = "72.h" @@ -39,7 +39,7 @@ params { check_model_num_samples = null // optional flag to do a more extensive check during check_model. This will override user given num_sample value for the tune run. This will give the user control on how extensive it wants the check to be. shuffle = true // flag to tell wether to shuffle or not the data and run a train on it. Sanity check always run on default. (If the way we think at shuffle change maybe is better to remove this flag and make it into a parameter of the user given json for noise nad split) debug_mode = false // flag used to switch to debug mode for the pipeline. - + // General help = false validate_params = true // tells wether or not to validate input values using nf-schema. @@ -67,7 +67,7 @@ params { config_profile_url = null // Schema validation default options validate_params = true - + } // Load base.config by default for all pipelines @@ -178,7 +178,7 @@ profiles { test_learn { includeConfig "conf/test_learn.config" } test_stub { includeConfig "conf/test_stub.config" } local { includeConfig "conf/local.config" } - test_full { includeConfig 'conf/test_full.config' } + test_full { includeConfig 'conf/test_full.config' } } // Load nf-core custom profiles from different Institutions @@ -273,10 +273,10 @@ validation { """ afterText = """${manifest.doi ? "* 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 + https://doi.org/10.1038/s41587-020-0439-x * Software dependencies - https://github.com/${manifest.name}/blob/master/CITATIONS.md + https://github.com/${manifest.name}/blob/master/CITATIONS.md """ } summary { diff --git a/subworkflows/local/check_model/main.nf b/subworkflows/local/check_model/main.nf index d55dc33..9079db0 100644 --- a/subworkflows/local/check_model/main.nf +++ b/subworkflows/local/check_model/main.nf @@ -20,20 +20,19 @@ workflow CHECK_MODEL { input_model input_tune_config input_initial_weights - + main: completion_message = "\n###\nThe check of the model has been skipped.\n###\n" - // It can be still skipped, but by default is run. + // It can be still skipped, but by default is run. if ( params.check_model ) { - + // put the files in channels csv = Channel.fromPath( input_csv ) model = Channel.fromPath( input_model ).first() // TODO implement a check and complain if more that one file is pased as model - json = Channel.fromPath( input_json ) + json = Channel.fromPath( input_json ) tune_config = Channel.fromPath( input_tune_config ) - // combine everything toghether in a all vs all manner model_tuple = csv.combine(model) @@ -47,11 +46,11 @@ workflow CHECK_MODEL { initial_weights = Channel.fromPath( input_initial_weights ) model_tuple = model_tuple.combine(initial_weights) } - + // launch the check using torch. TODO put selection of module based on type: torc, tensorflow ecc.. - CHECK_TORCH_MODEL( model_tuple ) + CHECK_TORCH_MODEL( model_tuple ) completion_message = CHECK_TORCH_MODEL.out.standardout - + } // to enforce that the second run workflow depends on this one diff --git a/subworkflows/local/handle_analysis/main.nf b/subworkflows/local/handle_analysis/main.nf index db3aee4..544be47 100644 --- a/subworkflows/local/handle_analysis/main.nf +++ b/subworkflows/local/handle_analysis/main.nf @@ -19,19 +19,19 @@ workflow HANDLE_ANALYSIS { input_model main: - + // 1. Run the default analysis for all models and data together. group them by the same test set -> aka same split process input_tune_out .map{it -> [it[0], it[1], it[2], it[3], it[4], it[5], it[6], it[7]]} // cannot work with empty initial_weights now. TODO fix this in the future .groupTuple() .set{ ch2default } ch2default.view() - + STIMULUS_ANALYSIS_DEFAULT( ch2default, input_model ) // 2. Run the motif discovery block - -} \ No newline at end of file + +} diff --git a/subworkflows/local/handle_data/main.nf b/subworkflows/local/handle_data/main.nf index 472bd3f..04417c9 100644 --- a/subworkflows/local/handle_data/main.nf +++ b/subworkflows/local/handle_data/main.nf @@ -24,8 +24,8 @@ workflow HANDLE_DATA { main: - - // print the message from the check_model subworkflow + + // print the message from the check_model subworkflow message_from_check.view() // put the files in channels @@ -53,7 +53,7 @@ workflow HANDLE_DATA { // run split with json that only contains experiment name and split information. It runs only the necessary times, all unique ways to split + default split (random split) or column split (already present in data). SPLIT_CSV( csv, split_json ) - // assign to each splitted data the associated ransform information based on the split_transform_key generated in the interpret step. + // assign to each splitted data the associated ransform information based on the split_transform_key generated in the interpret step. transform_split_tuple = transform_json.combine( SPLIT_CSV.out.split_data, by: 0 ) // launch the actual noise subworkflow @@ -64,7 +64,7 @@ workflow HANDLE_DATA { tmp = experiment_json.combine( TRANSFORM_CSV.out.transformed_data, by: 0 ).map{ it -> ["${it[6].name} - ${it[0]}", it[2], it[1], it[4]] } - + // Launch the shuffle, (always happening on default) and disjointed from noise. Data are taken from the no-split option of split module. Which means that is either randomly splitted with default values or using the column present in the data. // It can be still skipped but by default is run. shuffle is set to true in nextflow.config data = tmp @@ -77,11 +77,11 @@ workflow HANDLE_DATA { // merge output of shuffle to the output of noise data = tmp.concat( SHUFFLE_CSV.out.shuffle_data ) } - + emit: - data - + data + } diff --git a/subworkflows/local/handle_tune/main.nf b/subworkflows/local/handle_tune/main.nf index edb722a..8908cff 100644 --- a/subworkflows/local/handle_tune/main.nf +++ b/subworkflows/local/handle_tune/main.nf @@ -6,7 +6,6 @@ include { TORCH_TUNE } from '../../../modules/local/torch_tune.nf' - /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ RUN MAIN WORKFLOW @@ -22,13 +21,13 @@ workflow HANDLE_TUNE { input_initial_weights main: - // put the files in channels, + // put the files in channels, model = Channel.fromPath( input_model ).first() // TODO implement a check and complain if more that one file is pased as model tune_config = Channel.fromPath( input_tune_config ) // assign a model and a TUNE_config to each data model_conf_pair = model.combine(tune_config) - model_conf_data = model_conf_pair.combine(data).map{ + model_conf_data = model_conf_pair.combine(data).map{ it -> [it[2], it[3], it[1], it[0], it[5], it[4]] } // just reordering according to the inputs of the launch_tuning.py @@ -53,12 +52,11 @@ workflow HANDLE_TUNE { tune_out = TORCH_TUNE.out.tune_specs model debug - -} +} /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ THE END ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -*/ \ No newline at end of file +*/ diff --git a/subworkflows/local/shuffle_csv/main.nf b/subworkflows/local/shuffle_csv/main.nf index 5736ec9..34748f0 100644 --- a/subworkflows/local/shuffle_csv/main.nf +++ b/subworkflows/local/shuffle_csv/main.nf @@ -16,10 +16,10 @@ workflow SHUFFLE_CSV { take: csv_json_pairs - + main: - + STIMULUS_SHUFFLE_CSV(csv_json_pairs) diff --git a/subworkflows/local/transform_csv/main.nf b/subworkflows/local/transform_csv/main.nf index 2549ac6..14c0a0f 100644 --- a/subworkflows/local/transform_csv/main.nf +++ b/subworkflows/local/transform_csv/main.nf @@ -16,7 +16,7 @@ workflow TRANSFORM_CSV { take: csv_json_pairs - + main: // TODO add strategy for handling the launch of stimulus noiser as well as NF-core and other modules diff --git a/subworkflows/local/utils_nfcore_deepmodeloptim_pipeline/main.nf b/subworkflows/local/utils_nfcore_deepmodeloptim_pipeline/main.nf index c39df6b..dd954e4 100644 --- a/subworkflows/local/utils_nfcore_deepmodeloptim_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_deepmodeloptim_pipeline/main.nf @@ -47,7 +47,7 @@ workflow PIPELINE_INITIALISATION { workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1 ) - + // // Validate parameters and generate parameter summary to stdout // @@ -56,7 +56,7 @@ workflow PIPELINE_INITIALISATION { validate_params, null ) - + // // Check config provided to the pipeline @@ -107,11 +107,11 @@ workflow PIPELINE_COMPLETION { email // string: email address email_on_fail // string: email address sent on pipeline failure plaintext_email // boolean: Send plain-text email instead of HTML - + outdir // path: Path to output directory where results will be published monochrome_logs // boolean: Disable ANSI colour codes in log output hook_url // string: hook URL for notifications - + main: summary_params = paramsSummaryMap(workflow, parameters_schema: "nextflow_schema.json") @@ -172,8 +172,8 @@ def toolCitationText() { // Uncomment function in methodsDescriptionText to render in MultiQC report def citation_text = [ "Tools used in the workflow included:", - - + + "." ].join(' ').trim() @@ -185,8 +185,8 @@ 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 = [ - - + + ].join(' ').trim() return reference_text diff --git a/tests/handle_tune.nf.test b/tests/handle_tune.nf.test index 45fae18..0195c38 100644 --- a/tests/handle_tune.nf.test +++ b/tests/handle_tune.nf.test @@ -20,7 +20,7 @@ nextflow_workflow { } when { - + // make so that nf-test will see a singularity cache dir where is usally set to, again interpretation of projectdir has to be done here and not rely on an external config. params { // TODO Check that continues working with $NXF_SINGULARITY_CACHEDIR @@ -47,7 +47,7 @@ nextflow_workflow { // first chech that the model initialization is always identical // then check that the seeds used are the same through randomly picked numbers assertAll( - { assert workflow.success }, + { assert workflow.success }, { assert snapshot(workflow.out.debug).match() } ) } diff --git a/tests/nextflow.config b/tests/nextflow.config index f4281d5..f7b650f 100644 --- a/tests/nextflow.config +++ b/tests/nextflow.config @@ -33,4 +33,4 @@ manifest { timeline { enabled = false } report { enabled = false } trace { enabled = false } -dag { enabled = false } \ No newline at end of file +dag { enabled = false } diff --git a/workflows/deepmodeloptim.nf b/workflows/deepmodeloptim.nf index 6f90e8e..1f8def8 100644 --- a/workflows/deepmodeloptim.nf +++ b/workflows/deepmodeloptim.nf @@ -34,7 +34,7 @@ workflow DEEPMODELOPTIM { main: ch_versions = Channel.empty() - + CHECK_MODEL ( params.csv, params.exp_conf, @@ -44,14 +44,14 @@ workflow DEEPMODELOPTIM { ) completion_message = CHECK_MODEL.out.completion_message - HANDLE_DATA( + HANDLE_DATA( params.csv, params.exp_conf, completion_message ) prepared_data = HANDLE_DATA.out.data //HANDLE_DATA.out.data.view() - + HANDLE_TUNE( params.model, params.tune_conf, @@ -60,12 +60,12 @@ workflow DEEPMODELOPTIM { ) //HANDLE_TUNE.out.model.view() //HANDLE_TUNE.out.tune_out.view() - + // this part works, but the docker container is not updated with matplotlib yet HANDLE_ANALYSIS( HANDLE_TUNE.out.tune_out, HANDLE_TUNE.out.model - ) + ) // // Collate and save software versions From d54d43c8b854965d7f042f84d7d2385508b1e3d7 Mon Sep 17 00:00:00 2001 From: JoseEspinosa Date: Mon, 21 Oct 2024 11:51:14 +0200 Subject: [PATCH 11/25] Fix schema --- nextflow_schema.json | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/nextflow_schema.json b/nextflow_schema.json index 4484212..6091de6 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -142,20 +142,6 @@ } } }, - "general_options": { - "title": "General options", - "type": "object", - "description": "generic options", - "default": "", - "fa_icon": "fas fa-terminal", - "properties": { - "validate_params": { - "type": "boolean", - "description": "to validate or not the input params", - "default": true - } - } - }, "institutional_config_options": { "title": "Institutional config options", "type": "object", @@ -286,9 +272,6 @@ { "$ref": "#/$defs/skip_options" }, - { - "$ref": "#/$defs/general_options" - }, { "$ref": "#/$defs/institutional_config_options" }, From c5df7a108b879a6be9dceddb9c138211e3246a29 Mon Sep 17 00:00:00 2001 From: JoseEspinosa Date: Mon, 21 Oct 2024 12:49:45 +0200 Subject: [PATCH 12/25] nf-core lint does not like multiline channels --- modules/local/stimulus_analysis_default.nf | 9 +-------- modules/local/torch_tune.nf | 1 + 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/modules/local/stimulus_analysis_default.nf b/modules/local/stimulus_analysis_default.nf index 1ca0793..5b9e59c 100644 --- a/modules/local/stimulus_analysis_default.nf +++ b/modules/local/stimulus_analysis_default.nf @@ -7,14 +7,7 @@ process STIMULUS_ANALYSIS_DEFAULT { container "docker.io/mathysgrapotte/stimulus-py:latest" input: - tuple val(split_transform_key), \ - val(combination_key), \ - path(data), \ - path(experiment_config), \ - path(model_config), \ - path(weights), \ - path(optimizer), \ - path(metrics) + tuple val(split_transform_key), val(combination_key), path(data), path(experiment_config), path(model_config), path(weights), path(optimizer), path(metrics) path(model) output: diff --git a/modules/local/torch_tune.nf b/modules/local/torch_tune.nf index 123e5dd..acaea66 100644 --- a/modules/local/torch_tune.nf +++ b/modules/local/torch_tune.nf @@ -20,6 +20,7 @@ process TORCH_TUNE { path("*-metrics.csv"), path(initial_weights), emit: tune_specs + // output the debug files if they are present, making this an optional channel tuple val("debug_${prefix}"), path("ray_results/*/debug/best_model_*.txt"), From 8df9314f6e5111bfbf938aa86f8ef844ca6c0d06 Mon Sep 17 00:00:00 2001 From: JoseEspinosa Date: Mon, 21 Oct 2024 12:49:55 +0200 Subject: [PATCH 13/25] nf-core lint needs input parameter --- nextflow.config | 2 +- nextflow_schema.json | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/nextflow.config b/nextflow.config index d433729..c3189ff 100644 --- a/nextflow.config +++ b/nextflow.config @@ -10,7 +10,7 @@ params { // Input options - // input = null + input = null // TODO either change one of the params above by input or create a samplesheet containing the info of them csv = null // the input file containing all input data model = null // the model file in python, the model that will be tested by this pipeline exp_conf = null // the json config file that specifies all the parameters relative to the data manipulation diff --git a/nextflow_schema.json b/nextflow_schema.json index 6091de6..2b54d41 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -12,6 +12,17 @@ "fa_icon": "fas fa-terminal", "description": "Define where the pipeline should find input data.", "properties": { + "input": { + "type": "string", + "format": "file-path", + "exists": true, + "schema": "assets/schema_input.json", + "mimetype": "text/csv", + "pattern": "^\\S+\\.csv$", + "description": "Path to comma-separated file containing information about the samples in the experiment.", + "help_text": "You will need to create a design file with information about the samples in your experiment before running the pipeline. Use this parameter to specify its location. It has to be a comma-separated file with 3 columns, and a header row. See [usage docs](https://nf-co.re/proteinfold/usage#samplesheet-input).", + "fa_icon": "fas fa-file-csv" + }, "csv": { "type": "string", "format": "file-path", From 8b02ba3a3ab1f367d9345cbd318eff1336b7a19f Mon Sep 17 00:00:00 2001 From: JoseEspinosa Date: Mon, 21 Oct 2024 12:51:44 +0200 Subject: [PATCH 14/25] Add outdir parameter to ci command --- .github/workflows/ci_nextflow.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci_nextflow.yml b/.github/workflows/ci_nextflow.yml index 271c1cb..b31e2fb 100644 --- a/.github/workflows/ci_nextflow.yml +++ b/.github/workflows/ci_nextflow.yml @@ -39,7 +39,7 @@ jobs: uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 - name: Run pipeline with test data - # TODO: Get rid of loca profile when the nf-core like configs are in + # TODO: Get rid of local profile when the nf-core like configs are in run: | nextflow run ${GITHUB_WORKSPACE} -profile test,docker @@ -57,4 +57,4 @@ jobs: sudo mv nextflow /usr/local/bin/ - name: Run pipeline with stub-run in alphafold2 split mode run: | - nextflow run ${GITHUB_WORKSPACE} -profile test_stub,docker + nextflow run ${GITHUB_WORKSPACE} -profile test_stub,docker --oudir results From 695523d95fd8e2013be7893c8f3a46b7f5c57de0 Mon Sep 17 00:00:00 2001 From: JoseEspinosa Date: Mon, 21 Oct 2024 14:49:27 +0200 Subject: [PATCH 15/25] Template update for nf-core/tools version 3.0.2 --- .editorconfig | 4 - .github/CONTRIBUTING.md | 2 +- .github/workflows/awsfulltest.yml | 6 +- .github/workflows/ci.yml | 60 ++-- .github/workflows/download_pipeline.yml | 1 - .github/workflows/linting.yml | 4 +- .../workflows/template_version_comment.yml | 21 +- .gitignore | 1 + .nf-core.yml | 30 +- .prettierignore | 1 - CITATIONS.md | 5 - README.md | 3 - docs/output.md | 7 +- docs/usage.md | 2 +- main.nf | 2 +- modules.json | 7 +- nextflow.config | 15 +- nextflow_schema.json | 2 +- .../main.nf | 15 +- .../nf-core/utils_nextflow_pipeline/main.nf | 60 ++-- .../nf-core/utils_nfcore_pipeline/main.nf | 281 ++++++++++-------- 21 files changed, 282 insertions(+), 247 deletions(-) diff --git a/.editorconfig b/.editorconfig index e105881..72dda28 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,7 +11,6 @@ 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 @@ -26,12 +25,9 @@ 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 diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 2cfa080..8a3519a 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -25,6 +25,7 @@ If you'd like to write some code for nf-core/deepmodeloptim, the standard workfl If you're not used to this workflow with git, you can start with some [docs from GitHub](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests) or even their [excellent `git` resources](https://try.github.io/). ## Tests + You have the option to test your changes locally by running the pipeline. For receiving warnings about process selectors and other `debug` information, it is recommended to use the debug profile. Execute all the tests with the following command: ```bash @@ -120,4 +121,3 @@ To get started: Devcontainer specs: - [DevContainer config](.devcontainer/devcontainer.json) - diff --git a/.github/workflows/awsfulltest.yml b/.github/workflows/awsfulltest.yml index 9847dae..c49bbdc 100644 --- a/.github/workflows/awsfulltest.yml +++ b/.github/workflows/awsfulltest.yml @@ -14,16 +14,18 @@ on: jobs: run-platform: name: Run AWS full tests - if: github.repository == 'nf-core/deepmodeloptim' && github.event.review.state == 'approved' + # 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/deepmodeloptim' && github.event.review.state == 'approved' && github.event.pull_request.base.ref == 'master' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest steps: - uses: octokit/request-action@v2.x id: check_approvals with: - route: GET /repos/${{ github.repository }}/pulls/${{ github.event.review.number }}/reviews + route: GET /repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - id: test_variables + if: github.event_name != 'workflow_dispatch' run: | JSON_RESPONSE='${{ steps.check_approvals.outputs.data }}' CURRENT_APPROVALS_COUNT=$(echo $JSON_RESPONSE | jq -c '[.[] | select(.state | contains("APPROVED")) ] | length') diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 879f513..a35c08d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,8 @@ on: 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 }}" @@ -18,7 +20,7 @@ concurrency: jobs: test: - name: Run pipeline with test data + 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/deepmodeloptim') }}" runs-on: ubuntu-latest @@ -27,33 +29,57 @@ jobs: 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@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 - - name: Install Nextflow + - name: Set up Nextflow uses: nf-core/setup-nextflow@v2 with: version: "${{ matrix.NXF_VER }}" - - name: Disk space cleanup - uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 + - name: Set up Apptainer + if: matrix.profile == 'singularity' + uses: eWaterCycle/setup-apptainer@main - - name: Run pipeline with test data (docker) - # TODO nf-core: You can customise CI pipeline run tests as required - # For example: adding multiple test runs with different parameters - # Remember that you can parallelise this by using strategy.matrix + - name: Set up Singularity + if: matrix.profile == 'singularity' run: | - nextflow run ${GITHUB_WORKSPACE} -profile test,docker --outdir ./results + 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: Run pipeline with test data (singularity) - # TODO nf-core: You can customise CI pipeline run tests as required + - name: Set up Conda + if: matrix.profile == 'conda' run: | - nextflow run ${GITHUB_WORKSPACE} -profile test,singularity --outdir ./results - if: "${{ github.base_ref == 'master' }}" + 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 (conda) - # TODO nf-core: You can customise CI pipeline run tests as required + - name: "Run pipeline with test data ${{ matrix.NXF_VER }} | ${{ matrix.test_name }} | ${{ matrix.profile }}" run: | - nextflow run ${GITHUB_WORKSPACE} -profile test,conda --outdir ./results - if: "${{ github.base_ref == 'master' }}" + nextflow run ${GITHUB_WORKSPACE} -profile ${{ matrix.test_name }},${{ matrix.profile }} --outdir ./results diff --git a/.github/workflows/download_pipeline.yml b/.github/workflows/download_pipeline.yml index 63f7dcc..713dc3e 100644 --- a/.github/workflows/download_pipeline.yml +++ b/.github/workflows/download_pipeline.yml @@ -117,4 +117,3 @@ jobs: else echo "The pipeline can be downloaded successfully!" fi - diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index b882838..a502573 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -42,10 +42,10 @@ jobs: architecture: "x64" - name: read .nf-core.yml - uses: pietrobolcato/action-read-yaml@1.0.0 + uses: pietrobolcato/action-read-yaml@1.1.0 id: read_yml with: - config: ${{ github.workspace }}/.nf-core.yaml + config: ${{ github.workspace }}/.nf-core.yml - name: Install dependencies run: | diff --git a/.github/workflows/template_version_comment.yml b/.github/workflows/template_version_comment.yml index 9dea41f..e8aafe4 100644 --- a/.github/workflows/template_version_comment.yml +++ b/.github/workflows/template_version_comment.yml @@ -10,9 +10,11 @@ jobs: steps: - name: Check out pipeline code uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Read template version from .nf-core.yml - uses: pietrobolcato/action-read-yaml@1.0.0 + uses: nichmor/minimal-read-yaml@v0.0.2 id: read_yml with: config: ${{ github.workspace }}/.nf-core.yml @@ -24,20 +26,21 @@ jobs: - name: Check nf-core outdated id: nf_core_outdated - run: pip list --outdated | grep nf-core + run: echo "OUTPUT=$(pip list --outdated | grep nf-core)" >> ${GITHUB_ENV} - name: Post nf-core template version comment uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2 if: | - ${{ steps.nf_core_outdated.outputs.stdout }} =~ 'nf-core' + contains(env.OUTPUT, 'nf-core') with: repo-token: ${{ secrets.NF_CORE_BOT_AUTH_TOKEN }} allow-repeats: false message: | - ## :warning: Newer version of the nf-core template is available. - - Your pipeline is using an old version of the nf-core template: ${{ steps.read_yml.outputs['nf_core_version'] }}. - Please update your pipeline to the latest version. - - For more documentation on how to update your pipeline, please see the [nf-core documentation](https://github.com/nf-core/tools?tab=readme-ov-file#sync-a-pipeline-with-the-template) and [Synchronisation documentation](https://nf-co.re/docs/contributing/sync). + > [!WARNING] + > Newer version of the nf-core template is available. + > + > Your pipeline is using an old version of the nf-core template: ${{ steps.read_yml.outputs['nf_core_version'] }}. + > Please update your pipeline to the latest version. + > + > For more documentation on how to update your pipeline, please see the [nf-core documentation](https://github.com/nf-core/tools?tab=readme-ov-file#sync-a-pipeline-with-the-template) and [Synchronisation documentation](https://nf-co.re/docs/contributing/sync). # diff --git a/.gitignore b/.gitignore index 5124c9a..a42ce01 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ results/ testing/ testing* *.pyc +null/ diff --git a/.nf-core.yml b/.nf-core.yml index 1ef8d01..eddd2c9 100644 --- a/.nf-core.yml +++ b/.nf-core.yml @@ -1,33 +1,33 @@ bump_version: null lint: files_exist: - - conf/igenomes.config - - conf/igenomes_ignored.config - - assets/multiqc_config.yml - - conf/igenomes.config - - conf/igenomes_ignored.config - - assets/multiqc_config.yml + - conf/igenomes.config + - conf/igenomes_ignored.config + - assets/multiqc_config.yml + - conf/igenomes.config + - conf/igenomes_ignored.config + - assets/multiqc_config.yml files_unchanged: - - .github/CONTRIBUTING.md - - assets/sendmail_template.txt - - .github/CONTRIBUTING.md - - assets/sendmail_template.txt + - .github/CONTRIBUTING.md + - assets/sendmail_template.txt + - .github/CONTRIBUTING.md + - assets/sendmail_template.txt multiqc_config: false -nf_core_version: 3.0.0 +nf_core_version: 3.0.2 org_path: null repository_type: pipeline template: author: Mathys Grappote description: nf-core/deepmodeloptim is an end-to-end nextflow based pipeline for statistically testing training procedures of machine learning models - force: true + force: false is_nfcore: true name: deepmodeloptim org: nf-core outdir: . skip_features: - - igenomes - - multiqc - - fastqc + - igenomes + - multiqc + - fastqc version: 1.0.0dev update: null diff --git a/.prettierignore b/.prettierignore index 610e506..437d763 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,3 @@ - email_template.html adaptivecard.json slackreport.json diff --git a/CITATIONS.md b/CITATIONS.md index fd9fb93..f3cf692 100644 --- a/CITATIONS.md +++ b/CITATIONS.md @@ -10,10 +10,6 @@ ## Pipeline tools - - - - ## Software packaging/containerisation tools - [Anaconda](https://anaconda.com) @@ -35,4 +31,3 @@ - [Singularity](https://pubmed.ncbi.nlm.nih.gov/28494014/) > Kurtzer GM, Sochat V, Bauer MW. Singularity: Scientific containers for mobility of compute. PLoS One. 2017 May 11;12(5):e0177459. doi: 10.1371/journal.pone.0177459. eCollection 2017. PubMed PMID: 28494014; PubMed Central PMCID: PMC5426675. - > diff --git a/README.md b/README.md index c10cb51..87f18cf 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,6 @@ workflows use the "tube map" design for that. See https://nf-co.re/docs/contributing/design_guidelines#examples for examples. --> - - - ## Usage > [!NOTE] diff --git a/docs/output.md b/docs/output.md index c3673d0..4a4b659 100644 --- a/docs/output.md +++ b/docs/output.md @@ -2,7 +2,7 @@ ## Introduction -This document describes the output produced by 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. @@ -12,9 +12,6 @@ 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: - - - - [Pipeline information](#pipeline-information) - Report metrics generated during the workflow execution ### Pipeline information @@ -24,7 +21,7 @@ The pipeline is built using [Nextflow](https://www.nextflow.io/) and processes d - `pipeline_info/` - Reports generated by Nextflow: `execution_report.html`, `execution_timeline.html`, `execution_trace.txt` and `pipeline_dag.dot`/`pipeline_dag.svg`. - - Reports generated by the pipeline: `pipeline_report.html`, `pipeline_report.txt` and `software_versions.yml`. The `pipeline_report*` files will only be present if the `--email` / `--email_on_fail` parameter's are used when running the pipeline. + - Reports generated by the pipeline: `pipeline_report.html`, `pipeline_report.txt` and `software_versions.yml`. The `pipeline_report*` files will only be present if the `--email` / `--email_on_fail` parameter's are used when running the pipeline. - Reformatted samplesheet files used as input to the pipeline: `samplesheet.valid.csv`. - Parameters used by the pipeline run: `params.json`. diff --git a/docs/usage.md b/docs/usage.md index b32fbfa..42ec1b6 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -110,7 +110,7 @@ It is a good idea to specify a pipeline version when running the pipeline on you First, go to the [nf-core/deepmodeloptim releases page](https://github.com/nf-core/deepmodeloptim/releases) and find the latest pipeline version - numeric only (eg. `1.3.1`). Then specify this when running the pipeline with `-r` (one hyphen) - eg. `-r 1.3.1`. Of course, you can switch to another version by changing the number after the `-r` flag. -This version number will be logged in reports when you run the pipeline, so that you'll know what you used when you look back in the future. +This version number will be logged in reports when you run the pipeline, so that you'll know what you used when you look back in the future. To further assist in reproducbility, you can use share and re-use [parameter files](#running-the-pipeline) to repeat pipeline runs with the same settings without having to write out a command with every single parameter. diff --git a/main.nf b/main.nf index 6a06252..82896be 100644 --- a/main.nf +++ b/main.nf @@ -61,7 +61,7 @@ workflow { params.outdir, params.input ) - + // // WORKFLOW: Run main workflow // diff --git a/modules.json b/modules.json index fb095bf..be65d1b 100644 --- a/modules.json +++ b/modules.json @@ -4,19 +4,18 @@ "repos": { "https://github.com/nf-core/modules.git": { "modules": { - "nf-core": { - } + "nf-core": {} }, "subworkflows": { "nf-core": { "utils_nextflow_pipeline": { "branch": "master", - "git_sha": "d20fb2a9cc3e2835e9d067d1046a63252eb17352", + "git_sha": "3aa0aec1d52d492fe241919f0c6100ebf0074082", "installed_by": ["subworkflows"] }, "utils_nfcore_pipeline": { "branch": "master", - "git_sha": "2fdce49d30c0254f76bc0f13c55c17455c1251ab", + "git_sha": "1b6b9a3338d011367137808b49b923515080e3ba", "installed_by": ["subworkflows"] }, "utils_nfschema_plugin": { diff --git a/nextflow.config b/nextflow.config index 20d528f..1e914bf 100644 --- a/nextflow.config +++ b/nextflow.config @@ -13,6 +13,10 @@ params { // Input options input = null + + + + // Boilerplate options outdir = null publish_dir_mode = 'copy' @@ -26,6 +30,7 @@ params { show_hidden = false version = false pipelines_testdata_base_path = 'https://raw.githubusercontent.com/nf-core/test-datasets/' + // Config options config_profile_name = null config_profile_description = null @@ -34,9 +39,9 @@ params { custom_config_base = "https://raw.githubusercontent.com/nf-core/configs/${params.custom_config_version}" config_profile_contact = null config_profile_url = null + // Schema validation default options validate_params = true - } // Load base.config by default for all pipelines @@ -151,6 +156,7 @@ includeConfig !System.getenv('NXF_OFFLINE') && params.custom_config_base ? "${pa // Load nf-core/deepmodeloptim 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/deepmodeloptim.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 // Set to your registry if you have a mirror of containers @@ -160,6 +166,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. @@ -237,10 +245,10 @@ validation { """ afterText = """${manifest.doi ? "* 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 + https://doi.org/10.1038/s41587-020-0439-x * Software dependencies - https://github.com/${manifest.name}/blob/master/CITATIONS.md + https://github.com/${manifest.name}/blob/master/CITATIONS.md """ } summary { @@ -251,4 +259,3 @@ validation { // Load modules.config for DSL2 module specific options includeConfig 'conf/modules.config' - diff --git a/nextflow_schema.json b/nextflow_schema.json index 3f7eaba..287dcac 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -156,7 +156,7 @@ { "$ref": "#/$defs/input_output_options" }, - + { "$ref": "#/$defs/institutional_config_options" }, diff --git a/subworkflows/local/utils_nfcore_deepmodeloptim_pipeline/main.nf b/subworkflows/local/utils_nfcore_deepmodeloptim_pipeline/main.nf index 23b8816..f9967c6 100644 --- a/subworkflows/local/utils_nfcore_deepmodeloptim_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_deepmodeloptim_pipeline/main.nf @@ -18,9 +18,9 @@ include { UTILS_NFCORE_PIPELINE } from '../../nf-core/utils_nfcore_pipeline' include { UTILS_NEXTFLOW_PIPELINE } from '../../nf-core/utils_nextflow_pipeline' /* -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SUBWORKFLOW TO INITIALISE PIPELINE -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ workflow PIPELINE_INITIALISATION { @@ -47,7 +47,6 @@ workflow PIPELINE_INITIALISATION { workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1 ) - // // Validate parameters and generate parameter summary to stdout // @@ -56,7 +55,6 @@ workflow PIPELINE_INITIALISATION { validate_params, null ) - // // Check config provided to the pipeline @@ -95,9 +93,9 @@ workflow PIPELINE_INITIALISATION { } /* -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SUBWORKFLOW FOR PIPELINE COMPLETION -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ workflow PIPELINE_COMPLETION { @@ -106,7 +104,6 @@ workflow PIPELINE_COMPLETION { email // string: email address email_on_fail // string: email address sent on pipeline failure plaintext_email // boolean: Send plain-text email instead of HTML - outdir // path: Path to output directory where results will be published monochrome_logs // boolean: Disable ANSI colour codes in log output hook_url // string: hook URL for notifications @@ -143,9 +140,9 @@ workflow PIPELINE_COMPLETION { } /* -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ FUNCTIONS -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ // diff --git a/subworkflows/nf-core/utils_nextflow_pipeline/main.nf b/subworkflows/nf-core/utils_nextflow_pipeline/main.nf index 28e32b2..0fcbf7b 100644 --- a/subworkflows/nf-core/utils_nextflow_pipeline/main.nf +++ b/subworkflows/nf-core/utils_nextflow_pipeline/main.nf @@ -3,13 +3,12 @@ // /* -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SUBWORKFLOW DEFINITION -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ workflow UTILS_NEXTFLOW_PIPELINE { - take: print_version // boolean: print version dump_parameters // boolean: dump parameters @@ -22,7 +21,7 @@ workflow UTILS_NEXTFLOW_PIPELINE { // Print workflow version and exit on --version // if (print_version) { - log.info "${workflow.manifest.name} ${getWorkflowVersion()}" + log.info("${workflow.manifest.name} ${getWorkflowVersion()}") System.exit(0) } @@ -45,9 +44,9 @@ workflow UTILS_NEXTFLOW_PIPELINE { } /* -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ FUNCTIONS -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ // @@ -72,11 +71,11 @@ def getWorkflowVersion() { // Dump pipeline parameters to a JSON file // def dumpParametersToJSON(outdir) { - def timestamp = new java.util.Date().format( 'yyyy-MM-dd_HH-mm-ss') - def filename = "params_${timestamp}.json" - def temp_pf = new File(workflow.launchDir.toString(), ".${filename}") - def jsonStr = groovy.json.JsonOutput.toJson(params) - temp_pf.text = groovy.json.JsonOutput.prettyPrint(jsonStr) + def timestamp = new java.util.Date().format('yyyy-MM-dd_HH-mm-ss') + def filename = "params_${timestamp}.json" + def temp_pf = new File(workflow.launchDir.toString(), ".${filename}") + def jsonStr = groovy.json.JsonOutput.toJson(params) + temp_pf.text = groovy.json.JsonOutput.prettyPrint(jsonStr) nextflow.extension.FilesEx.copyTo(temp_pf.toPath(), "${outdir}/pipeline_info/params_${timestamp}.json") temp_pf.delete() @@ -91,9 +90,14 @@ def checkCondaChannels() { try { def config = parser.load("conda config --show channels".execute().text) channels = config.channels - } catch(NullPointerException | IOException e) { - log.warn "Could not verify conda channel configuration." - return + } + catch (NullPointerException e) { + log.warn("Could not verify conda channel configuration.") + return null + } + catch (IOException e) { + log.warn("Could not verify conda channel configuration.") + return null } // Check that all channels are present @@ -102,23 +106,19 @@ def checkCondaChannels() { def channels_missing = ((required_channels_in_order as Set) - (channels as Set)) as Boolean // Check that they are in the right order - def channel_priority_violation = false - - required_channels_in_order.eachWithIndex { channel, index -> - if (index < required_channels_in_order.size() - 1) { - channel_priority_violation |= !(channels.indexOf(channel) < channels.indexOf(required_channels_in_order[index+1])) - } - } + def channel_priority_violation = required_channels_in_order != channels.findAll { ch -> ch in required_channels_in_order } if (channels_missing | channel_priority_violation) { - log.warn "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - " There is a problem with your Conda configuration!\n\n" + - " You will need to set-up the conda-forge and bioconda channels correctly.\n" + - " Please refer to https://bioconda.github.io/\n" + - " The observed channel order is \n" + - " ${channels}\n" + - " but the following channel order is required:\n" + - " ${required_channels_in_order}\n" + - "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" + log.warn """\ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + There is a problem with your Conda configuration! + You will need to set-up the conda-forge and bioconda channels correctly. + Please refer to https://bioconda.github.io/ + The observed channel order is + ${channels} + but the following channel order is required: + ${required_channels_in_order} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" + """.stripIndent(true) } } diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/main.nf b/subworkflows/nf-core/utils_nfcore_pipeline/main.nf index cbd8495..5cb7baf 100644 --- a/subworkflows/nf-core/utils_nfcore_pipeline/main.nf +++ b/subworkflows/nf-core/utils_nfcore_pipeline/main.nf @@ -3,13 +3,12 @@ // /* -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SUBWORKFLOW DEFINITION -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ workflow UTILS_NFCORE_PIPELINE { - take: nextflow_cli_args @@ -22,9 +21,9 @@ workflow UTILS_NFCORE_PIPELINE { } /* -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ FUNCTIONS -======================================================================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ // @@ -33,12 +32,9 @@ workflow UTILS_NFCORE_PIPELINE { def checkConfigProvided() { def valid_config = true as Boolean if (workflow.profile == 'standard' && workflow.configFiles.size() <= 1) { - log.warn "[$workflow.manifest.name] You are attempting to run the pipeline without any custom configuration!\n\n" + - "This will be dependent on your local compute environment but can be achieved via one or more of the following:\n" + - " (1) Using an existing pipeline profile e.g. `-profile docker` or `-profile singularity`\n" + - " (2) Using an existing nf-core/configs for your Institution e.g. `-profile crick` or `-profile uppmax`\n" + - " (3) Using your own local custom config e.g. `-c /path/to/your/custom.config`\n\n" + - "Please refer to the quick start section and usage docs for the pipeline.\n " + log.warn( + "[${workflow.manifest.name}] You are attempting to run the pipeline without any custom configuration!\n\n" + "This will be dependent on your local compute environment but can be achieved via one or more of the following:\n" + " (1) Using an existing pipeline profile e.g. `-profile docker` or `-profile singularity`\n" + " (2) Using an existing nf-core/configs for your Institution e.g. `-profile crick` or `-profile uppmax`\n" + " (3) Using your own local custom config e.g. `-c /path/to/your/custom.config`\n\n" + "Please refer to the quick start section and usage docs for the pipeline.\n " + ) valid_config = false } return valid_config @@ -49,12 +45,14 @@ def checkConfigProvided() { // def checkProfileProvided(nextflow_cli_args) { if (workflow.profile.endsWith(',')) { - error "The `-profile` option cannot end with a trailing comma, please remove it and re-run the pipeline!\n" + - "HINT: A common mistake is to provide multiple values separated by spaces e.g. `-profile test, docker`.\n" + error( + "The `-profile` option cannot end with a trailing comma, please remove it and re-run the pipeline!\n" + "HINT: A common mistake is to provide multiple values separated by spaces e.g. `-profile test, docker`.\n" + ) } if (nextflow_cli_args[0]) { - log.warn "nf-core pipelines do not accept positional arguments. The positional argument `${nextflow_cli_args[0]}` has been detected.\n" + - "HINT: A common mistake is to provide multiple values separated by spaces e.g. `-profile test, docker`.\n" + log.warn( + "nf-core pipelines do not accept positional arguments. The positional argument `${nextflow_cli_args[0]}` has been detected.\n" + "HINT: A common mistake is to provide multiple values separated by spaces e.g. `-profile test, docker`.\n" + ) } } @@ -64,19 +62,13 @@ def checkProfileProvided(nextflow_cli_args) { def workflowCitation() { def temp_doi_ref = "" def manifest_doi = workflow.manifest.doi.tokenize(",") - // Using a loop to handle multiple DOIs + // Handling multiple DOIs // Removing `https://doi.org/` to handle pipelines using DOIs vs DOI resolvers // Removing ` ` since the manifest.doi is a string and not a proper list manifest_doi.each { doi_ref -> temp_doi_ref += " https://doi.org/${doi_ref.replace('https://doi.org/', '').replace(' ', '')}\n" } - return "If you use ${workflow.manifest.name} for your analysis please cite:\n\n" + - "* The pipeline\n" + - temp_doi_ref + "\n" + - "* The nf-core framework\n" + - " https://doi.org/10.1038/s41587-020-0439-x\n\n" + - "* Software dependencies\n" + - " https://github.com/${workflow.manifest.name}/blob/master/CITATIONS.md" + return "If you use ${workflow.manifest.name} for your analysis please cite:\n\n" + "* The pipeline\n" + temp_doi_ref + "\n" + "* The nf-core framework\n" + " https://doi.org/10.1038/s41587-020-0439-x\n\n" + "* Software dependencies\n" + " https://github.com/${workflow.manifest.name}/blob/master/CITATIONS.md" } // @@ -102,7 +94,7 @@ def getWorkflowVersion() { // def processVersionsFromYAML(yaml_file) { def yaml = new org.yaml.snakeyaml.Yaml() - def versions = yaml.load(yaml_file).collectEntries { k, v -> [ k.tokenize(':')[-1], v ] } + def versions = yaml.load(yaml_file).collectEntries { k, v -> [k.tokenize(':')[-1], v] } return yaml.dumpAsMap(versions).trim() } @@ -112,8 +104,8 @@ def processVersionsFromYAML(yaml_file) { def workflowVersionToYAML() { return """ Workflow: - $workflow.manifest.name: ${getWorkflowVersion()} - Nextflow: $workflow.nextflow.version + ${workflow.manifest.name}: ${getWorkflowVersion()} + Nextflow: ${workflow.nextflow.version} """.stripIndent().trim() } @@ -121,11 +113,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())) } // @@ -133,25 +121,31 @@ def softwareVersionsToYAML(ch_versions) { // def paramsSummaryMultiqc(summary_params) { def summary_section = '' - summary_params.keySet().each { group -> - def group_params = summary_params.get(group) // This gets the parameters of that particular group - if (group_params) { - summary_section += "

    $group

    \n" - summary_section += "
    \n" - group_params.keySet().sort().each { param -> - summary_section += "
    $param
    ${group_params.get(param) ?: 'N/A'}
    \n" + summary_params + .keySet() + .each { group -> + def group_params = summary_params.get(group) + // This gets the parameters of that particular group + if (group_params) { + summary_section += "

    ${group}

    \n" + summary_section += "
    \n" + group_params + .keySet() + .sort() + .each { param -> + summary_section += "
    ${param}
    ${group_params.get(param) ?: 'N/A'}
    \n" + } + summary_section += "
    \n" } - summary_section += "
    \n" } - } - def yaml_file_text = "id: '${workflow.manifest.name.replace('/','-')}-summary'\n" as String - yaml_file_text += "description: ' - this information is collected when the pipeline is started.'\n" - yaml_file_text += "section_name: '${workflow.manifest.name} Workflow Summary'\n" - yaml_file_text += "section_href: 'https://github.com/${workflow.manifest.name}'\n" - yaml_file_text += "plot_type: 'html'\n" - yaml_file_text += "data: |\n" - yaml_file_text += "${summary_section}" + def yaml_file_text = "id: '${workflow.manifest.name.replace('/', '-')}-summary'\n" as String + yaml_file_text += "description: ' - this information is collected when the pipeline is started.'\n" + yaml_file_text += "section_name: '${workflow.manifest.name} Workflow Summary'\n" + yaml_file_text += "section_href: 'https://github.com/${workflow.manifest.name}'\n" + yaml_file_text += "plot_type: 'html'\n" + yaml_file_text += "data: |\n" + yaml_file_text += "${summary_section}" return yaml_file_text } @@ -199,54 +193,54 @@ def logColours(monochrome_logs=true) { colorcodes['hidden'] = monochrome_logs ? '' : "\033[8m" // Regular Colors - colorcodes['black'] = monochrome_logs ? '' : "\033[0;30m" - colorcodes['red'] = monochrome_logs ? '' : "\033[0;31m" - colorcodes['green'] = monochrome_logs ? '' : "\033[0;32m" - colorcodes['yellow'] = monochrome_logs ? '' : "\033[0;33m" - colorcodes['blue'] = monochrome_logs ? '' : "\033[0;34m" - colorcodes['purple'] = monochrome_logs ? '' : "\033[0;35m" - colorcodes['cyan'] = monochrome_logs ? '' : "\033[0;36m" - colorcodes['white'] = monochrome_logs ? '' : "\033[0;37m" + colorcodes['black'] = monochrome_logs ? '' : "\033[0;30m" + colorcodes['red'] = monochrome_logs ? '' : "\033[0;31m" + colorcodes['green'] = monochrome_logs ? '' : "\033[0;32m" + colorcodes['yellow'] = monochrome_logs ? '' : "\033[0;33m" + colorcodes['blue'] = monochrome_logs ? '' : "\033[0;34m" + colorcodes['purple'] = monochrome_logs ? '' : "\033[0;35m" + colorcodes['cyan'] = monochrome_logs ? '' : "\033[0;36m" + colorcodes['white'] = monochrome_logs ? '' : "\033[0;37m" // Bold - colorcodes['bblack'] = monochrome_logs ? '' : "\033[1;30m" - colorcodes['bred'] = monochrome_logs ? '' : "\033[1;31m" - colorcodes['bgreen'] = monochrome_logs ? '' : "\033[1;32m" - colorcodes['byellow'] = monochrome_logs ? '' : "\033[1;33m" - colorcodes['bblue'] = monochrome_logs ? '' : "\033[1;34m" - colorcodes['bpurple'] = monochrome_logs ? '' : "\033[1;35m" - colorcodes['bcyan'] = monochrome_logs ? '' : "\033[1;36m" - colorcodes['bwhite'] = monochrome_logs ? '' : "\033[1;37m" + colorcodes['bblack'] = monochrome_logs ? '' : "\033[1;30m" + colorcodes['bred'] = monochrome_logs ? '' : "\033[1;31m" + colorcodes['bgreen'] = monochrome_logs ? '' : "\033[1;32m" + colorcodes['byellow'] = monochrome_logs ? '' : "\033[1;33m" + colorcodes['bblue'] = monochrome_logs ? '' : "\033[1;34m" + colorcodes['bpurple'] = monochrome_logs ? '' : "\033[1;35m" + colorcodes['bcyan'] = monochrome_logs ? '' : "\033[1;36m" + colorcodes['bwhite'] = monochrome_logs ? '' : "\033[1;37m" // Underline - colorcodes['ublack'] = monochrome_logs ? '' : "\033[4;30m" - colorcodes['ured'] = monochrome_logs ? '' : "\033[4;31m" - colorcodes['ugreen'] = monochrome_logs ? '' : "\033[4;32m" - colorcodes['uyellow'] = monochrome_logs ? '' : "\033[4;33m" - colorcodes['ublue'] = monochrome_logs ? '' : "\033[4;34m" - colorcodes['upurple'] = monochrome_logs ? '' : "\033[4;35m" - colorcodes['ucyan'] = monochrome_logs ? '' : "\033[4;36m" - colorcodes['uwhite'] = monochrome_logs ? '' : "\033[4;37m" + colorcodes['ublack'] = monochrome_logs ? '' : "\033[4;30m" + colorcodes['ured'] = monochrome_logs ? '' : "\033[4;31m" + colorcodes['ugreen'] = monochrome_logs ? '' : "\033[4;32m" + colorcodes['uyellow'] = monochrome_logs ? '' : "\033[4;33m" + colorcodes['ublue'] = monochrome_logs ? '' : "\033[4;34m" + colorcodes['upurple'] = monochrome_logs ? '' : "\033[4;35m" + colorcodes['ucyan'] = monochrome_logs ? '' : "\033[4;36m" + colorcodes['uwhite'] = monochrome_logs ? '' : "\033[4;37m" // High Intensity - colorcodes['iblack'] = monochrome_logs ? '' : "\033[0;90m" - colorcodes['ired'] = monochrome_logs ? '' : "\033[0;91m" - colorcodes['igreen'] = monochrome_logs ? '' : "\033[0;92m" - colorcodes['iyellow'] = monochrome_logs ? '' : "\033[0;93m" - colorcodes['iblue'] = monochrome_logs ? '' : "\033[0;94m" - colorcodes['ipurple'] = monochrome_logs ? '' : "\033[0;95m" - colorcodes['icyan'] = monochrome_logs ? '' : "\033[0;96m" - colorcodes['iwhite'] = monochrome_logs ? '' : "\033[0;97m" + colorcodes['iblack'] = monochrome_logs ? '' : "\033[0;90m" + colorcodes['ired'] = monochrome_logs ? '' : "\033[0;91m" + colorcodes['igreen'] = monochrome_logs ? '' : "\033[0;92m" + colorcodes['iyellow'] = monochrome_logs ? '' : "\033[0;93m" + colorcodes['iblue'] = monochrome_logs ? '' : "\033[0;94m" + colorcodes['ipurple'] = monochrome_logs ? '' : "\033[0;95m" + colorcodes['icyan'] = monochrome_logs ? '' : "\033[0;96m" + colorcodes['iwhite'] = monochrome_logs ? '' : "\033[0;97m" // Bold High Intensity - colorcodes['biblack'] = monochrome_logs ? '' : "\033[1;90m" - colorcodes['bired'] = monochrome_logs ? '' : "\033[1;91m" - colorcodes['bigreen'] = monochrome_logs ? '' : "\033[1;92m" - colorcodes['biyellow'] = monochrome_logs ? '' : "\033[1;93m" - colorcodes['biblue'] = monochrome_logs ? '' : "\033[1;94m" - colorcodes['bipurple'] = monochrome_logs ? '' : "\033[1;95m" - colorcodes['bicyan'] = monochrome_logs ? '' : "\033[1;96m" - colorcodes['biwhite'] = monochrome_logs ? '' : "\033[1;97m" + colorcodes['biblack'] = monochrome_logs ? '' : "\033[1;90m" + colorcodes['bired'] = monochrome_logs ? '' : "\033[1;91m" + colorcodes['bigreen'] = monochrome_logs ? '' : "\033[1;92m" + colorcodes['biyellow'] = monochrome_logs ? '' : "\033[1;93m" + colorcodes['biblue'] = monochrome_logs ? '' : "\033[1;94m" + colorcodes['bipurple'] = monochrome_logs ? '' : "\033[1;95m" + colorcodes['bicyan'] = monochrome_logs ? '' : "\033[1;96m" + colorcodes['biwhite'] = monochrome_logs ? '' : "\033[1;97m" return colorcodes } @@ -261,14 +255,15 @@ def attachMultiqcReport(multiqc_report) { mqc_report = multiqc_report.getVal() if (mqc_report.getClass() == ArrayList && mqc_report.size() >= 1) { if (mqc_report.size() > 1) { - log.warn "[$workflow.manifest.name] Found multiple reports from process 'MULTIQC', will use only one" + log.warn("[${workflow.manifest.name}] Found multiple reports from process 'MULTIQC', will use only one") } mqc_report = mqc_report[0] } } - } catch (all) { + } + catch (Exception all) { if (multiqc_report) { - log.warn "[$workflow.manifest.name] Could not attach MultiQC report to summary email" + log.warn("[${workflow.manifest.name}] Could not attach MultiQC report to summary email") } } return mqc_report @@ -280,26 +275,35 @@ def attachMultiqcReport(multiqc_report) { def completionEmail(summary_params, email, email_on_fail, plaintext_email, outdir, monochrome_logs=true, multiqc_report=null) { // Set up the e-mail variables - def subject = "[$workflow.manifest.name] Successful: $workflow.runName" + def subject = "[${workflow.manifest.name}] Successful: ${workflow.runName}" if (!workflow.success) { - subject = "[$workflow.manifest.name] FAILED: $workflow.runName" + subject = "[${workflow.manifest.name}] FAILED: ${workflow.runName}" } def summary = [:] - summary_params.keySet().sort().each { group -> - summary << summary_params[group] - } + summary_params + .keySet() + .sort() + .each { group -> + summary << summary_params[group] + } def misc_fields = [:] misc_fields['Date Started'] = workflow.start misc_fields['Date Completed'] = workflow.complete misc_fields['Pipeline script file path'] = workflow.scriptFile misc_fields['Pipeline script hash ID'] = workflow.scriptId - if (workflow.repository) misc_fields['Pipeline repository Git URL'] = workflow.repository - if (workflow.commitId) misc_fields['Pipeline repository Git Commit'] = workflow.commitId - if (workflow.revision) misc_fields['Pipeline Git branch/tag'] = workflow.revision - misc_fields['Nextflow Version'] = workflow.nextflow.version - misc_fields['Nextflow Build'] = workflow.nextflow.build + if (workflow.repository) { + misc_fields['Pipeline repository Git URL'] = workflow.repository + } + if (workflow.commitId) { + misc_fields['Pipeline repository Git Commit'] = workflow.commitId + } + if (workflow.revision) { + misc_fields['Pipeline Git branch/tag'] = workflow.revision + } + misc_fields['Nextflow Version'] = workflow.nextflow.version + misc_fields['Nextflow Build'] = workflow.nextflow.build misc_fields['Nextflow Compile Timestamp'] = workflow.nextflow.timestamp def email_fields = [:] @@ -337,7 +341,7 @@ def completionEmail(summary_params, email, email_on_fail, plaintext_email, outdi // Render the sendmail template def max_multiqc_email_size = (params.containsKey('max_multiqc_email_size') ? params.max_multiqc_email_size : 0) as nextflow.util.MemoryUnit - def smail_fields = [ email: email_address, subject: subject, email_txt: email_txt, email_html: email_html, projectDir: "${workflow.projectDir}", mqcFile: mqc_report, mqcMaxSize: max_multiqc_email_size.toBytes() ] + def smail_fields = [email: email_address, subject: subject, email_txt: email_txt, email_html: email_html, projectDir: "${workflow.projectDir}", mqcFile: mqc_report, mqcMaxSize: max_multiqc_email_size.toBytes()] def sf = new File("${workflow.projectDir}/assets/sendmail_template.txt") def sendmail_template = engine.createTemplate(sf).make(smail_fields) def sendmail_html = sendmail_template.toString() @@ -346,30 +350,32 @@ def completionEmail(summary_params, email, email_on_fail, plaintext_email, outdi def colors = logColours(monochrome_logs) as Map if (email_address) { try { - if (plaintext_email) { throw new org.codehaus.groovy.GroovyException('Send plaintext e-mail, not HTML') } + if (plaintext_email) { +new org.codehaus.groovy.GroovyException('Send plaintext e-mail, not HTML') } // Try to send HTML e-mail using sendmail def sendmail_tf = new File(workflow.launchDir.toString(), ".sendmail_tmp.html") sendmail_tf.withWriter { w -> w << sendmail_html } - [ 'sendmail', '-t' ].execute() << sendmail_html - log.info "-${colors.purple}[$workflow.manifest.name]${colors.green} Sent summary e-mail to $email_address (sendmail)-" - } catch (all) { + ['sendmail', '-t'].execute() << sendmail_html + log.info("-${colors.purple}[${workflow.manifest.name}]${colors.green} Sent summary e-mail to ${email_address} (sendmail)-") + } + catch (Exception all) { // Catch failures and try with plaintext - def mail_cmd = [ 'mail', '-s', subject, '--content-type=text/html', email_address ] + def mail_cmd = ['mail', '-s', subject, '--content-type=text/html', email_address] mail_cmd.execute() << email_html - log.info "-${colors.purple}[$workflow.manifest.name]${colors.green} Sent summary e-mail to $email_address (mail)-" + log.info("-${colors.purple}[${workflow.manifest.name}]${colors.green} Sent summary e-mail to ${email_address} (mail)-") } } // Write summary e-mail HTML to a file def output_hf = new File(workflow.launchDir.toString(), ".pipeline_report.html") output_hf.withWriter { w -> w << email_html } - nextflow.extension.FilesEx.copyTo(output_hf.toPath(), "${outdir}/pipeline_info/pipeline_report.html"); + nextflow.extension.FilesEx.copyTo(output_hf.toPath(), "${outdir}/pipeline_info/pipeline_report.html") output_hf.delete() // Write summary e-mail TXT to a file def output_tf = new File(workflow.launchDir.toString(), ".pipeline_report.txt") output_tf.withWriter { w -> w << email_txt } - nextflow.extension.FilesEx.copyTo(output_tf.toPath(), "${outdir}/pipeline_info/pipeline_report.txt"); + nextflow.extension.FilesEx.copyTo(output_tf.toPath(), "${outdir}/pipeline_info/pipeline_report.txt") output_tf.delete() } @@ -380,12 +386,14 @@ def completionSummary(monochrome_logs=true) { def colors = logColours(monochrome_logs) as Map if (workflow.success) { if (workflow.stats.ignoredCount == 0) { - log.info "-${colors.purple}[$workflow.manifest.name]${colors.green} Pipeline completed successfully${colors.reset}-" - } else { - log.info "-${colors.purple}[$workflow.manifest.name]${colors.yellow} Pipeline completed successfully, but with errored process(es) ${colors.reset}-" + log.info("-${colors.purple}[${workflow.manifest.name}]${colors.green} Pipeline completed successfully${colors.reset}-") + } + else { + log.info("-${colors.purple}[${workflow.manifest.name}]${colors.yellow} Pipeline completed successfully, but with errored process(es) ${colors.reset}-") } - } else { - log.info "-${colors.purple}[$workflow.manifest.name]${colors.red} Pipeline completed with errors${colors.reset}-" + } + else { + log.info("-${colors.purple}[${workflow.manifest.name}]${colors.red} Pipeline completed with errors${colors.reset}-") } } @@ -394,21 +402,30 @@ def completionSummary(monochrome_logs=true) { // def imNotification(summary_params, hook_url) { def summary = [:] - summary_params.keySet().sort().each { group -> - summary << summary_params[group] - } + summary_params + .keySet() + .sort() + .each { group -> + summary << summary_params[group] + } def misc_fields = [:] - misc_fields['start'] = workflow.start - misc_fields['complete'] = workflow.complete - misc_fields['scriptfile'] = workflow.scriptFile - misc_fields['scriptid'] = workflow.scriptId - if (workflow.repository) misc_fields['repository'] = workflow.repository - if (workflow.commitId) misc_fields['commitid'] = workflow.commitId - if (workflow.revision) misc_fields['revision'] = workflow.revision - misc_fields['nxf_version'] = workflow.nextflow.version - misc_fields['nxf_build'] = workflow.nextflow.build - misc_fields['nxf_timestamp'] = workflow.nextflow.timestamp + misc_fields['start'] = workflow.start + misc_fields['complete'] = workflow.complete + misc_fields['scriptfile'] = workflow.scriptFile + misc_fields['scriptid'] = workflow.scriptId + if (workflow.repository) { + misc_fields['repository'] = workflow.repository + } + if (workflow.commitId) { + misc_fields['commitid'] = workflow.commitId + } + if (workflow.revision) { + misc_fields['revision'] = workflow.revision + } + misc_fields['nxf_version'] = workflow.nextflow.version + misc_fields['nxf_build'] = workflow.nextflow.build + misc_fields['nxf_timestamp'] = workflow.nextflow.timestamp def msg_fields = [:] msg_fields['version'] = getWorkflowVersion() @@ -433,13 +450,13 @@ def imNotification(summary_params, hook_url) { def json_message = json_template.toString() // POST - def post = new URL(hook_url).openConnection(); + def post = new URL(hook_url).openConnection() post.setRequestMethod("POST") post.setDoOutput(true) post.setRequestProperty("Content-Type", "application/json") - post.getOutputStream().write(json_message.getBytes("UTF-8")); - def postRC = post.getResponseCode(); - if (! postRC.equals(200)) { - log.warn(post.getErrorStream().getText()); + post.getOutputStream().write(json_message.getBytes("UTF-8")) + def postRC = post.getResponseCode() + if (!postRC.equals(200)) { + log.warn(post.getErrorStream().getText()) } } From 31fd7b58763b29f8f14e198e3e229539af6647df Mon Sep 17 00:00:00 2001 From: JoseEspinosa Date: Mon, 21 Oct 2024 15:02:39 +0200 Subject: [PATCH 16/25] Fix tyop and remove c/p leak --- .github/workflows/ci_nextflow.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci_nextflow.yml b/.github/workflows/ci_nextflow.yml index b31e2fb..d5e3aef 100644 --- a/.github/workflows/ci_nextflow.yml +++ b/.github/workflows/ci_nextflow.yml @@ -55,6 +55,6 @@ jobs: run: | wget -qO- get.nextflow.io | bash sudo mv nextflow /usr/local/bin/ - - name: Run pipeline with stub-run in alphafold2 split mode + - name: Run pipeline with stub-run run: | - nextflow run ${GITHUB_WORKSPACE} -profile test_stub,docker --oudir results + nextflow run ${GITHUB_WORKSPACE} -profile test_stub,docker --outdir results From 2033fc0e1c1513e9c9e41e3e0be7f1a3189972d8 Mon Sep 17 00:00:00 2001 From: JoseEspinosa Date: Mon, 21 Oct 2024 15:06:11 +0200 Subject: [PATCH 17/25] Remove previous ci_nextflow, now is in nf-core as ci.yml --- .github/workflows/ci_nextflow.yml | 60 ------------------------------- 1 file changed, 60 deletions(-) delete mode 100644 .github/workflows/ci_nextflow.yml diff --git a/.github/workflows/ci_nextflow.yml b/.github/workflows/ci_nextflow.yml deleted file mode 100644 index d5e3aef..0000000 --- a/.github/workflows/ci_nextflow.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: pipeline CI -# This workflow runs the pipeline with the minimal test dataset to check that it completes without any syntax errors -on: - push: - branches: - - main - pull_request: - release: - types: [published] - -env: - NXF_ANSI_LOG: false - -concurrency: - group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}" - cancel-in-progress: true - -jobs: - test: - name: Run pipeline with test data - # 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 == 'mathysgrapotte/stimulus') }}" - runs-on: ubuntu-latest - strategy: - matrix: - NXF_VER: - - "23.04.0" - - "latest-everything" - steps: - - name: Check out pipeline code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - - name: Install Nextflow - uses: nf-core/setup-nextflow@v1 - with: - version: "${{ matrix.NXF_VER }}" - - - name: Disk space cleanup - uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 - - - name: Run pipeline with test data - # TODO: Get rid of local profile when the nf-core like configs are in - run: | - nextflow run ${GITHUB_WORKSPACE} -profile test,docker - - test_stub: - name: Stub run of the pipeline - if: ${{ github.event_name != 'push' || (github.event_name == 'push' && github.repository == 'mathysgrapotte/stimulus') }} - runs-on: ubuntu-latest - steps: - - name: Check out pipeline code - uses: actions/checkout@v2 - - - name: Install Nextflow - run: | - wget -qO- get.nextflow.io | bash - sudo mv nextflow /usr/local/bin/ - - name: Run pipeline with stub-run - run: | - nextflow run ${GITHUB_WORKSPACE} -profile test_stub,docker --outdir results From 257b7664c17446a5ec777742def4ae43740c9695 Mon Sep 17 00:00:00 2001 From: JoseEspinosa Date: Mon, 21 Oct 2024 15:07:29 +0200 Subject: [PATCH 18/25] Make prettier happy --- nextflow.config | 3 --- 1 file changed, 3 deletions(-) diff --git a/nextflow.config b/nextflow.config index f8f7058..e4c38f3 100644 --- a/nextflow.config +++ b/nextflow.config @@ -44,9 +44,6 @@ params { help = false validate_params = true // tells wether or not to validate input values using nf-schema. - - - // Boilerplate options outdir = null From 741b15d6f9478291d800561a23f895387732716d Mon Sep 17 00:00:00 2001 From: JoseEspinosa Date: Mon, 21 Oct 2024 15:12:56 +0200 Subject: [PATCH 19/25] Run test_stub profile in ci and not test --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a35c08d..d4c270f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: - "docker" - "singularity" test_name: - - "test" + - "test_stub" isMaster: - ${{ github.base_ref == 'master' }} # Exclude conda and singularity on dev @@ -80,6 +80,6 @@ jobs: - 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 }}" + - name: "Stub run of the pipeline ${{ matrix.NXF_VER }} | ${{ matrix.test_name }} | ${{ matrix.profile }}" run: | nextflow run ${GITHUB_WORKSPACE} -profile ${{ matrix.test_name }},${{ matrix.profile }} --outdir ./results From ccee15fd597e3336a6d0437d94e735cf39d70f94 Mon Sep 17 00:00:00 2001 From: JoseEspinosa Date: Tue, 22 Oct 2024 18:38:09 +0200 Subject: [PATCH 20/25] Remove crg_slurm and crg configs --- conf/crg.config | 37 ------------------------------------- conf/crg_slurm.config | 38 -------------------------------------- nextflow.config | 2 -- 3 files changed, 77 deletions(-) delete mode 100644 conf/crg.config delete mode 100644 conf/crg_slurm.config diff --git a/conf/crg.config b/conf/crg.config deleted file mode 100644 index 4910b93..0000000 --- a/conf/crg.config +++ /dev/null @@ -1,37 +0,0 @@ -params { - config_profile_name = 'CRG profile' - config_profile_description = 'Configuration to run on CRG cluster' -} - - -process { - resourceLimits = [ - cpus: 64, - memory: 100.GB, - time: 48.h - ] - - executor = 'crg' - maxRetries = params.max_retries - errorStrategy = params.err_start - - withLabel:process_low { - queue = 'cn-el7,short-centos79' - cpus = { 1 } - memory = { 4.GB * task.attempt } - time = { 1.h * task.attempt } - } - withLabel:process_medium{ - queue = 'cn-el7,short-centos79' - cpus = { 4 } - memory = { 10.GB * task.attempt } - time = { 6.h * task.attempt } - } - withLabel:process_high { - queue = 'cn-el7,long-centos79' - cpus = { 12 } - memory = { 20.GB * task.attempt } - time = { 12.h * task.attempt } - - } -} diff --git a/conf/crg_slurm.config b/conf/crg_slurm.config deleted file mode 100644 index be803d9..0000000 --- a/conf/crg_slurm.config +++ /dev/null @@ -1,38 +0,0 @@ -params { - config_profile_name = 'CRG profile - slurm new cluster' - config_profile_description = 'Configuration to run on CRG new cluster' - -} - -process { - - resourceLimits = [ - cpus: 64, - memory: 100.GB, - time: 48.h - ] - - executor = "slurm" - maxRetries = params.max_retries - errorStrategy = params.err_start - - withLabel:process_low { - clusterOptions = '--qos=shorter' - cpus = { 1 } - memory = { 4.GB * task.attempt } - time = { 1.h * task.attempt } - } - withLabel:process_medium{ - clusterOptions = '--qos=short' - cpus = { 4 } - memory = { 10.GB * task.attempt } - time = { 6.h * task.attempt } - } - withLabel:process_high { - clusterOptions = '--qos=normal' - cpus = { 12 } - memory = { 20.GB * task.attempt } - time = { 12.h * task.attempt } - - } -} diff --git a/nextflow.config b/nextflow.config index e4c38f3..124a9c2 100644 --- a/nextflow.config +++ b/nextflow.config @@ -175,8 +175,6 @@ profiles { executor.memory = 8.GB } test { includeConfig "conf/test.config" } - crg { includeConfig "conf/crg.config" } - crg_slurm { includeConfig "conf/crg_slurm.config" } test_learn { includeConfig "conf/test_learn.config" } test_stub { includeConfig "conf/test_stub.config" } local { includeConfig "conf/local.config" } From 893adea751761bdb0452ca4899a9486f78956db5 Mon Sep 17 00:00:00 2001 From: JoseEspinosa Date: Tue, 22 Oct 2024 18:40:55 +0200 Subject: [PATCH 21/25] Add todo to do it once in nf-core --- nextflow.config | 1 + 1 file changed, 1 insertion(+) diff --git a/nextflow.config b/nextflow.config index 124a9c2..87bd39b 100644 --- a/nextflow.config +++ b/nextflow.config @@ -186,6 +186,7 @@ includeConfig !System.getenv('NXF_OFFLINE') && params.custom_config_base ? "${pa // Load nf-core/deepmodeloptim 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 +// TODO This line can be uncommented once you move the pipeline to nf-core. A file deepmodeloptim.config will be automatically created at the nf-core/configs repo. This is to allow having a pipeline specific config. // includeConfig !System.getenv('NXF_OFFLINE') && params.custom_config_base ? "${params.custom_config_base}/pipeline/deepmodeloptim.config" : "/dev/null" // Set default registry for Apptainer, Docker, Podman, Charliecloud and Singularity independent of -profile From 31f4992cd1ee5a91d82aa891cbf491d366e49881 Mon Sep 17 00:00:00 2001 From: JoseEspinosa Date: Tue, 22 Oct 2024 18:42:44 +0200 Subject: [PATCH 22/25] Add review suggestion --- conf/test.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/test.config b/conf/test.config index 665aa46..24434b7 100644 --- a/conf/test.config +++ b/conf/test.config @@ -28,7 +28,7 @@ process { errorStrategy = params.err_start withLabel:process_low { - cpus = { 2 } + cpus = { 1 } memory = { 4.GB * task.attempt } time = { 10.m * task.attempt } } From ad2354bb730bf938be41bd157769d570582883fe Mon Sep 17 00:00:00 2001 From: JoseEspinosa Date: Tue, 22 Oct 2024 18:44:58 +0200 Subject: [PATCH 23/25] Correct Mathys surname --- nextflow.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nextflow.config b/nextflow.config index 87bd39b..9247834 100644 --- a/nextflow.config +++ b/nextflow.config @@ -244,7 +244,7 @@ dag { manifest { name = 'nf-core/deepmodeloptim' - author = """Mathys Grappote""" + author = """Mathys Grapotte""" homePage = 'https://github.com/nf-core/deepmodeloptim' description = """nf-core/deepmodeloptim is an end-to-end nextflow based pipeline for statistically testing training procedures of machine learning models""" mainScript = 'main.nf' From a210ecec96b7363fc0fd74de77e35e92abb39a49 Mon Sep 17 00:00:00 2001 From: JoseEspinosa Date: Tue, 22 Oct 2024 18:48:13 +0200 Subject: [PATCH 24/25] Use single for output to avoid future linting issues --- modules/local/torch_tune.nf | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/modules/local/torch_tune.nf b/modules/local/torch_tune.nf index acaea66..7b4c893 100644 --- a/modules/local/torch_tune.nf +++ b/modules/local/torch_tune.nf @@ -10,16 +10,7 @@ process TORCH_TUNE { tuple val(combination_key), val(split_transform_key), path(ray_tune_config), path(model), path(data_csv), path(experiment_config), path(initial_weights) output: - tuple val(split_transform_key), - val(combination_key), - path(data_csv), - path(experiment_config), - path("*-config.json"), - path("*-model.safetensors"), - path("*-optimizer.pt"), - path("*-metrics.csv"), - path(initial_weights), - emit: tune_specs + tuple val(split_transform_key), val(combination_key), path(data_csv), path(experiment_config), path("*-config.json"), path("*-model.safetensors"), path("*-optimizer.pt"), path("*-metrics.csv"), path(initial_weights), emit: tune_specs // output the debug files if they are present, making this an optional channel tuple val("debug_${prefix}"), From 95d0d30ccc18b19918ea32a140aaecd116258a20 Mon Sep 17 00:00:00 2001 From: JoseEspinosa Date: Tue, 22 Oct 2024 23:03:13 +0200 Subject: [PATCH 25/25] Fix tyop in Mathys name --- .nf-core.yml | 2 +- LICENSE | 2 +- README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.nf-core.yml b/.nf-core.yml index eddd2c9..42b703b 100644 --- a/.nf-core.yml +++ b/.nf-core.yml @@ -17,7 +17,7 @@ nf_core_version: 3.0.2 org_path: null repository_type: pipeline template: - author: Mathys Grappote + author: Mathys Grapotte description: nf-core/deepmodeloptim is an end-to-end nextflow based pipeline for statistically testing training procedures of machine learning models force: false diff --git a/LICENSE b/LICENSE index 58fbc71..788a749 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) Mathys Grappote +Copyright (c) Mathys Grapotte Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 1cd09e5..ca1fcc6 100644 --- a/README.md +++ b/README.md @@ -265,7 +265,7 @@ The `experiment_config` is a mandatory input for the pipeline and can be passed Update the author list --> -nf-core/deepmodeloptim was originally written by Mathys Grappote. +nf-core/deepmodeloptim was originally written by Mathys Grapotte. We thank the following people for their extensive assistance in the development of this pipeline: