From 01d31043320e2aa96ff4e71ce04b5786206bcb4f Mon Sep 17 00:00:00 2001 From: Vladimir Mikryukov Date: Tue, 17 Feb 2026 14:28:31 +0200 Subject: [PATCH 1/5] rm nextits copy --- .../service_scripts/NextITS/.gitattributes | 4 - .../service_scripts/NextITS/.gitignore | 8 - .../service_scripts/NextITS/CHANGELOG.md | 115 - .../service_scripts/NextITS/CITATION.cff | 19 - .../service_scripts/NextITS/CITATIONS.md | 50 - .../service_scripts/NextITS/LICENSE | 201 -- .../service_scripts/NextITS/README.md | 41 - .../NextITS/bin/ITSx_to_DuckDB.sh | 146 - .../NextITS/bin/aggregate_sequences.R | 173 -- .../NextITS/bin/bucketize_db.R | 248 -- .../NextITS/bin/chimera_rescue.R | 64 - .../NextITS/bin/convert_IUPAC.sh | 42 - .../NextITS/bin/count_homopolymer_stats.sh | 12 - .../NextITS/bin/count_number_of_reads.sh | 12 - .../NextITS/bin/dada2_no_quals.R | 421 --- .../NextITS/bin/disambiguate_primers.R | 37 - .../service_scripts/NextITS/bin/document_s1.R | 337 --- .../service_scripts/NextITS/bin/document_s2.R | 339 --- .../NextITS/bin/hash_sequences.sh | 17 - .../NextITS/bin/homopolymer_collapse_len.sh | 58 - .../NextITS/bin/homopolymer_compression.sh | 7 - .../service_scripts/NextITS/bin/max_ee.R | 57 - .../NextITS/bin/merge_hash_tables.sh | 131 - .../NextITS/bin/merge_sequnce_qualities.sh | 12 - .../NextITS/bin/merge_tj_memberships.sh | 87 - .../NextITS/bin/merge_uc_files.R | 264 -- .../NextITS/bin/pool_seqs_clean_header.sh | 11 - .../service_scripts/NextITS/bin/quick_stats.R | 241 -- .../service_scripts/NextITS/bin/rc.sh | 9 - .../NextITS/bin/read_count_summary.R | 500 ---- .../NextITS/bin/seq_table_assembly.R | 299 -- .../NextITS/bin/substitute_compressed_seqs.R | 67 - .../NextITS/bin/summarize_clustered_data.R | 400 --- .../NextITS/bin/summarize_dereplicated_data.R | 390 --- .../NextITS/bin/tag_jump_removal.R | 138 - .../NextITS/bin/tag_jump_removal_longtab.R | 247 -- .../NextITS/bin/validate_tags.R | 488 --- .../NextITS/conf/docker.config | 12 - .../service_scripts/NextITS/conf/hpc.config | 214 -- .../NextITS/conf/hpc_utslurm.config | 340 --- .../NextITS/conf/params.config | 307 -- .../conf/params_illumina_2color.config | 11 - .../conf/params_illumina_4color.config | 11 - .../NextITS/conf/singularity.config | 16 - .../NextITS/conf/test_step1.config | 40 - .../NextITS/conf/test_step2.config | 43 - .../NextITS/containerfiles/NextITS.def | 306 -- .../NextITS/containerfiles/NextITS.dockerfile | 270 -- .../NextITS/containerfiles/README.md | 25 - .../NextITS/images/NextITS_logo.png | Bin 8790 -> 0 bytes .../NextITS/images/NextITS_logo.svg | 27 - .../service_scripts/NextITS/main.nf | 283 -- .../NextITS/modules/Illumina_pe.nf | 484 --- .../service_scripts/NextITS/modules/colors.nf | 116 - .../NextITS/modules/dump_parameters.nf | 48 - .../NextITS/modules/help_message.nf | 96 - .../NextITS/modules/parameter_summary.nf | 87 - .../NextITS/modules/version_parser.nf | 28 - .../service_scripts/NextITS/nextflow.config | 459 --- .../NextITS/nextflow_schema.json | 771 ----- .../chimera_removal_subworkflow.nf | 296 -- .../subworkflows/clustering_subworkflow.nf | 540 ---- .../NextITS/subworkflows/itsx_subworkflow.nf | 564 ---- .../NextITS/test_data/Test_ChimeraDB.udb | Bin 587232 -> 0 bytes .../Test_S2/Test/07_SeqTable/Seqs.parquet | Bin 21217 -> 0 bytes .../NextITS/test_data/Test_Step1.fastq.gz | Bin 53323 -> 0 bytes .../test_data/Test_Step1_barcodes.fasta | 6 - .../NextITS/tests/test_step2_vsearch.yml | 18 - .../NextITS/workflows/STEP1.nf | 2646 ----------------- .../NextITS/workflows/STEP2.nf | 868 ------ 70 files changed, 14624 deletions(-) delete mode 100644 src/pipecraft-core/service_scripts/NextITS/.gitattributes delete mode 100644 src/pipecraft-core/service_scripts/NextITS/.gitignore delete mode 100644 src/pipecraft-core/service_scripts/NextITS/CHANGELOG.md delete mode 100644 src/pipecraft-core/service_scripts/NextITS/CITATION.cff delete mode 100644 src/pipecraft-core/service_scripts/NextITS/CITATIONS.md delete mode 100644 src/pipecraft-core/service_scripts/NextITS/LICENSE delete mode 100644 src/pipecraft-core/service_scripts/NextITS/README.md delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/ITSx_to_DuckDB.sh delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/aggregate_sequences.R delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/bucketize_db.R delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/chimera_rescue.R delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/convert_IUPAC.sh delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/count_homopolymer_stats.sh delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/count_number_of_reads.sh delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/dada2_no_quals.R delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/disambiguate_primers.R delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/document_s1.R delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/document_s2.R delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/hash_sequences.sh delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/homopolymer_collapse_len.sh delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/homopolymer_compression.sh delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/max_ee.R delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/merge_hash_tables.sh delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/merge_sequnce_qualities.sh delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/merge_tj_memberships.sh delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/merge_uc_files.R delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/pool_seqs_clean_header.sh delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/quick_stats.R delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/rc.sh delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/read_count_summary.R delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/seq_table_assembly.R delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/substitute_compressed_seqs.R delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/summarize_clustered_data.R delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/summarize_dereplicated_data.R delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/tag_jump_removal.R delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/tag_jump_removal_longtab.R delete mode 100755 src/pipecraft-core/service_scripts/NextITS/bin/validate_tags.R delete mode 100644 src/pipecraft-core/service_scripts/NextITS/conf/docker.config delete mode 100644 src/pipecraft-core/service_scripts/NextITS/conf/hpc.config delete mode 100644 src/pipecraft-core/service_scripts/NextITS/conf/hpc_utslurm.config delete mode 100644 src/pipecraft-core/service_scripts/NextITS/conf/params.config delete mode 100644 src/pipecraft-core/service_scripts/NextITS/conf/params_illumina_2color.config delete mode 100644 src/pipecraft-core/service_scripts/NextITS/conf/params_illumina_4color.config delete mode 100644 src/pipecraft-core/service_scripts/NextITS/conf/singularity.config delete mode 100644 src/pipecraft-core/service_scripts/NextITS/conf/test_step1.config delete mode 100644 src/pipecraft-core/service_scripts/NextITS/conf/test_step2.config delete mode 100644 src/pipecraft-core/service_scripts/NextITS/containerfiles/NextITS.def delete mode 100644 src/pipecraft-core/service_scripts/NextITS/containerfiles/NextITS.dockerfile delete mode 100644 src/pipecraft-core/service_scripts/NextITS/containerfiles/README.md delete mode 100644 src/pipecraft-core/service_scripts/NextITS/images/NextITS_logo.png delete mode 100755 src/pipecraft-core/service_scripts/NextITS/images/NextITS_logo.svg delete mode 100644 src/pipecraft-core/service_scripts/NextITS/main.nf delete mode 100644 src/pipecraft-core/service_scripts/NextITS/modules/Illumina_pe.nf delete mode 100644 src/pipecraft-core/service_scripts/NextITS/modules/colors.nf delete mode 100644 src/pipecraft-core/service_scripts/NextITS/modules/dump_parameters.nf delete mode 100644 src/pipecraft-core/service_scripts/NextITS/modules/help_message.nf delete mode 100644 src/pipecraft-core/service_scripts/NextITS/modules/parameter_summary.nf delete mode 100644 src/pipecraft-core/service_scripts/NextITS/modules/version_parser.nf delete mode 100644 src/pipecraft-core/service_scripts/NextITS/nextflow.config delete mode 100644 src/pipecraft-core/service_scripts/NextITS/nextflow_schema.json delete mode 100644 src/pipecraft-core/service_scripts/NextITS/subworkflows/chimera_removal_subworkflow.nf delete mode 100644 src/pipecraft-core/service_scripts/NextITS/subworkflows/clustering_subworkflow.nf delete mode 100644 src/pipecraft-core/service_scripts/NextITS/subworkflows/itsx_subworkflow.nf delete mode 100644 src/pipecraft-core/service_scripts/NextITS/test_data/Test_ChimeraDB.udb delete mode 100644 src/pipecraft-core/service_scripts/NextITS/test_data/Test_S2/Test/07_SeqTable/Seqs.parquet delete mode 100644 src/pipecraft-core/service_scripts/NextITS/test_data/Test_Step1.fastq.gz delete mode 100644 src/pipecraft-core/service_scripts/NextITS/test_data/Test_Step1_barcodes.fasta delete mode 100644 src/pipecraft-core/service_scripts/NextITS/tests/test_step2_vsearch.yml delete mode 100644 src/pipecraft-core/service_scripts/NextITS/workflows/STEP1.nf delete mode 100644 src/pipecraft-core/service_scripts/NextITS/workflows/STEP2.nf diff --git a/src/pipecraft-core/service_scripts/NextITS/.gitattributes b/src/pipecraft-core/service_scripts/NextITS/.gitattributes deleted file mode 100644 index b21921b9..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/.gitattributes +++ /dev/null @@ -1,4 +0,0 @@ -*.config linguist-language=nextflow -*.nf.test linguist-language=nextflow -modules/** linguist-generated -subworkflows/** linguist-generated diff --git a/src/pipecraft-core/service_scripts/NextITS/.gitignore b/src/pipecraft-core/service_scripts/NextITS/.gitignore deleted file mode 100644 index 5124c9ac..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -.nextflow* -work/ -data/ -results/ -.DS_Store -testing/ -testing* -*.pyc diff --git a/src/pipecraft-core/service_scripts/NextITS/CHANGELOG.md b/src/pipecraft-core/service_scripts/NextITS/CHANGELOG.md deleted file mode 100644 index 0b92d7a7..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/CHANGELOG.md +++ /dev/null @@ -1,115 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -This project tries to adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -For version numbering, we use the following convention: `MAJOR.MINOR.PATCH`. -Each element increases numerically (e.g., `1.9.0` -> `1.10.0` -> `1.11.0`). - - -## [1.1.0] - 2026-01-22 - -- Fixed handling of unknown barcode combinations (in `dual asymmetric` mode); thanks to Alice Retter for reporting -- Refactored and optimized the tag-jump removal step -- Fixed a bug with duplicated sequences in the tag-jump removal step; thanks to Valentin Étienne for reporting -- Implemented a chunking option for splitting the dataset into smaller parts prior to clustering in Step-2 (pre-clustering, clustering, and denoising moved to a separate sub-workflow), using MMseqs2 -- Added possibility to disable reference-based and/or de novo chimera removal steps and tag-jump removal -- New parameters added: - - `lima_remove_unknown` (default, `false`; if `true`, unknown barcode combinations are removed from demultiplexed data) - - `chunking_n` (number of chunks to split the dataset into prior to clustering) - - `chunking_id` (minimum sequence identity used for splitting the dataset into chunks) - - `chimera_methods` (specifies which chimera removal methods to use - "ref" for reference-based, "denovo" for de novo, or "ref,denovo" for both; could be also "none" or `null` to disable chimera removal) - - `tj` (specifies whether to run tag-jump removal - "true" or "false") -- Added DADA2 denoising (`--preclustering dada2`; also works with `--clustering none`) -- Implemented automated documentation for analysis procedures (generates `README_Step1_Methods.txt` and `README_Step2_Methods.txt` in the `pipeline_info` directory) -- Refactored parameter validation (using `nf-schema` plugin) -- Refactored the runtime parameter summary and help message -- Added test profiles (`test`, `test1`, `test2`) -- Improved run summary for Step-1 -- Default parameters changed: - - ITSx now checks only a single strand (option `ITSx_complement` set to `F`). This should be safe for most cases, as amplicons were re-oriented using primers during the pipeline run. However, we recommend checking the results carefully (e.g., columns `ITSx_Extracted_Reads` and `ITSx_Yield_Percent` in the run summary) - - Prior to tag-jump removal, sequences are now dereplicated at 100% identity (option `tj_id` set to `1`). It is possible to pre-cluster sequences at a lower similarity threshold (e.g., `--tj_id 0.99`) but this will take much longer. This change should also be safe for most cases, as amplicons undergo homopolymer-correction -- Fixed a minor bug in extraction of sample IDs at the ref-based chimera rescue step; thanks to Valentin Étienne for reporting - -## [1.0.0] - 2025-03-24 - -- Added support of asymmetric barcoding scheme for demultiplexing of PacBio data -- Added support of BAM files (CCS) as input -- Added support for SWARM *d*=1 pre-clustering -- Changed the selection of representative sequences (sequence with the highest quality score is taken as the representative; using [`phredsort`](https://github.com/vmikk/phredsort)) -- Refactored sequence quality estimation -- Improved processing speed (using DuckDB and Parquet format) -- Improved tag valiadion for demultiplexing -- Improved compression speed for output files (runs in parallel using `pigz`) -- Update of the database for reference-based chimera detection (using the [EUKARYOME database](https://eukaryome.org/)) -- New parameters added: - - `step` (specifies which pipeline step to run - "Step1" or "Step2") - - `storagemode` (Adjusts how files are directed to the results folder) - - `gzip_compression` (Controls GZIP compression level in output files) - - categorical `lima_barcodetype` replaces boolean `lima_dualbarcode` - - `lima_minendscore` (For asymmetric and dual barcoding scheme) - - `lima_minrefspan` (Controls barcode coverage) - - `lima_minscoringregions` (Controls the number of reqired barcodes for demultiplexing using dual barcodes) -- Added auxilarry output files: - - [Step-1] All rRNA parts extracted by ITSx (pooled within sequencing run - useful for extracting these regions for representative sequences) - - [Step-1] File with quality scores for full-length sequences (after QC and trimming) - - [Step-2] File with joined sequence memebership (dereplication, pre-clustering, and clustering) -- Primer trimming prior ITSx is now default (sequence quality is also estimated on trimmed sequence) -- Fixed VSEARCH clustering on denoised reads -- Resolved an issue where no *de novo* chimeras were detected -- Reconfigured parameter specification -- Introduced a parameter schema and enhanced parameter validation -- Container updates to included the latest versions of dependencies -- New dependencies - specialized tools written in Go to speed up the processing: - - [`phredsort` (https://github.com/vmikk/phredsort)](https://github.com/vmikk/phredsort) (Sorts sequences by quality score) - - [`seqhasher` (https://github.com/vmikk/seqhasher)](https://github.com/vmikk/seqhasher) (Hashes sequences) - - [`ucs` (https://github.com/vmikk/ucs)](https://github.com/vmikk/ucs) (Parses UC files and converts them to parquet format) - - -## [0.5.0] - 2023-08-08 - -- New `seqstats` sub-workflow (only dereplication, primer validation, and basic run stats) -- Add SWARM clustering ([Mahé et al., 2022 DOI:10.1093/bioinformatics/btab493](https://academic.oup.com/bioinformatics/article/38/1/267/6318385)) -- Add post-clustering curation with LULU ([Frøslev et al., 2017 DOI:doi.org/10.1038](https://www.nature.com/articles/s41467-017-01312-x)) -- Add barcode validation step -- Add SSU and LSU region-based output sequences -- Add support for UNOISE-only output (without clustering) -- Add `merge_replicates` parameter (Step-2) for merging or keeping separate sample replicates -- Update Step-1 run summary (add homopolymer stats) -- Deprecate taxonomy annotation workflow at Step-1 -- Fixed different extensions in demultiplexed input -- Experimental: UNITE-style dereplication (allows query sequences to vary in length at 100% similarity) -- Experimental: support of alternative alignment penalty scores (ITS-specific feature) - - -## [0.4.0] - 2023-05-08 - -- Add Step-2 workflow for pooling, dereplicating, and clustering sequences from Step-1 - - Read clustering with VSEARCH ([Rognes et al., 2016 DOI:10.7717/peerj.2584](https://peerj.com/articles/2584/)) - - Error-correction with UNOISE2 ([Edgar, 2016 DOI:10.1101/081257](https://www.biorxiv.org/content/10.1101/081257v1)) -- Add run summary for Step-1 (read counts at different pipeline stages) -- Separate config for HPC clusters -- Add Docker container - - -## [0.3.0] - 2023-03-02 - -- Add support for pre-demultiplexed data as input -- Add option for semi-full-length ITS (especially useful when forward primer is located at the very end of SSU and the HMM site can not be recognized by ITSx) -- Add removal of long homopolymer artefacts at QC stage -- Correct handling of a case with no valid sequences at primer checking step (thank to Taavi Riit for reporting the bug) -- Bug fixed in `assemble_its` (thanks to Kadri Põldmaa for discovering the error) -- Addition of ITSx detailed results (with information on the HMM profile used for ITS extraction) -- Fixed sample names for the rescued chimeric sequences -- Minor fixes related with the Singularity container, output directory, help message, and single-end QC - -## [0.2.0] - 2022-09-30 - -- Add Ilumina-based workflow (see `--seqplatform` flag) -- Publish Singularity image to Singularity library -- Minor bugfixes in `primer_check` (multiprimer artefacts), `pool_seqs` (sequence headers), and `prep_asvtab` (aggregation of non-unique joined Illumina sequences) processes -- New logo design (thanks to Olesya Dulya) - -## [0.0.1] - 2022-07-07 - -- Initial release diff --git a/src/pipecraft-core/service_scripts/NextITS/CITATION.cff b/src/pipecraft-core/service_scripts/NextITS/CITATION.cff deleted file mode 100644 index 67541b19..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/CITATION.cff +++ /dev/null @@ -1,19 +0,0 @@ -cff-version: 1.2.0 -title: "NextITS: a pipeline for metabarcoding eukaryotes with full-length ITS sequenced with PacBio" -type: software -authors: -- family-names: "Mikryukov" - given-names: "Vladimir" - orcid: "https://orcid.org/0000-0003-2786-2690" -- family-names: "Anslan" - given-names: "Sten" - orcid: "https://orcid.org/0000-0002-2299-454X" -- family-names: "Tedersoo" - given-names: "Leho" - orcid: "https://orcid.org/0000-0002-1635-1249" -version: 1.1.0 -doi: 10.5281/zenodo.15074881 -date-released: 2025-03-24 -url: "https://github.com/vmikk/NextITS" -license: Apache-2.0 -repository-code: "https://github.com/vmikk/NextITS" diff --git a/src/pipecraft-core/service_scripts/NextITS/CITATIONS.md b/src/pipecraft-core/service_scripts/NextITS/CITATIONS.md deleted file mode 100644 index 56acadd6..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/CITATIONS.md +++ /dev/null @@ -1,50 +0,0 @@ -# Citations - -## **NextITS** - -> Mikryukov V., Anslan S., Tedersoo L. NextITS: a pipeline for metabarcoding fungi and other eukaryotes with full-length ITS sequenced with PacBio. [https://github.com/vmikk/NextITS](https://github.com/vmikk/NextITS), doi: 10.5281/zenodo.15074881 - -## [**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 - -### Core tools - -- [ITSx](https://besjournals.onlinelibrary.wiley.com/doi/10.1111/2041-210X.12073) - - > Bengtsson-Palme, J., Ryberg, M., Hartmann, M., Branco, S., Wang, Z., Godhe, A., De Wit, P., Sánchez-García, M., Ebersberger, I., de Sousa, F., Amend, A., Jumpponen, A., Unterseher, M., Kristiansson, E., Abarenkov, K., Bertrand, Y.J.K., Sanli, K., Eriksson, K.M., Vik, U., Veldre, V. and Nilsson, R.H.. Improved software detection and extraction of ITS1 and ITS2 from ribosomal ITS sequences of fungi and other eukaryotes for analysis of environmental sequencing data. Methods Ecol Evol 2013, 4: 914-919. doi: 10.1111/2041-210X.12073. - -- [VSEARCH](https://peerj.com/articles/2584/) - - > Rognes T, Flouri T, Nichols B, Quince C, Mahé F. VSEARCH: a versatile open source tool for metagenomics. PeerJ. 2016 4:e2584. doi: 10.7717/peerj.2584 - -- VSEARCH option usearch_global implements the [USEARCH](https://doi.org/10.1093/bioinformatics/btq461) algorithm - - > Edgar RC. Search and clustering orders of magnitude faster than BLAST. Bioinformatics. 2010 26(19) 2460-2461 - -- [Cutadapt](https://journal.embnet.org/index.php/embnetjournal/article/view/200/479) - - > Marcel, M. Cutadapt removes adapter sequences from high-throughput sequencing reads. EMBnet. journal 17.1 (2011): pp-10. doi: 10.14806/ej.17.1.200. - -### Database for reference-based chimera removal - -- [EUKARYOME database](https://eukaryome.org/) - - > Tedersoo L, Hosseyni Moghaddam MS, Mikryukov V, Hakimzadeh A, Bahram M, Nilsson RH, Yatsiuk I, Geisen S, Schwelm A, Piwosz K, Prous M, Sildever S, Chmolowska D, Rueckert S, Skaloud P, Laas P, Tines M, Jung J-H, Choi JH, Alkahtani S, Anslan S (2024). EUKARYOME: The rRNA gene reference database for identification of all eukaryotes. Database, 2024, baae043. doi: 10.1093/database/baae043 - - -### Taxonomic classification and databases - -- [UNITE - eukaryotic nuclear ribosomal ITS region](https://unite.ut.ee/) - -Abarenkov K, Nilsson RH, Larsson K-H, Taylor AFS, May TW, Frøslev TG, Pawlowska J, Lindahl B, Põldmaa K, Truong C, Vu D, Hosoya T, Niskanen T, Piirmann T, Ivanov F, Zirk A, Peterson M, Cheeke TE, Ishigami Y, Jansson AT, Stjernegaard Jeppesen T, Kristiansson E, Mikryukov V, Miller JT, Oono R, Ossandon FJ, Paupério J, Saar I, Schigel D, Suija A, Tedersoo L, Kõljalg U (2023). The UNITE database for molecular identification and taxonomic communication of fungi and other eukaryotes: Sequences, taxa and classifications reconsidered. Nucleic Acids Research, gkad1039. doi: 10.1093/nar/gkad1039 - - -## Software packaging/containerisation tools - -- [Docker](https://dl.acm.org/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/src/pipecraft-core/service_scripts/NextITS/LICENSE b/src/pipecraft-core/service_scripts/NextITS/LICENSE deleted file mode 100644 index 521e13f5..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [2021-2025] [Vladimir Mikruykov] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/src/pipecraft-core/service_scripts/NextITS/README.md b/src/pipecraft-core/service_scripts/NextITS/README.md deleted file mode 100644 index 3f504e26..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# NextITS - -![GitHub (latest release)](https://img.shields.io/github/v/release/vmikk/NextITS?label=GitHub%20release&color=23aa62) -[![Nextflow](https://img.shields.io/badge/version-%E2%89%A525.04.2-green?style=flat&logo=nextflow&logoColor=white&color=%230DC09D&link=https%3A%2F%2Fnextflow.io)](https://www.nextflow.io/) -[![GitHub license](https://img.shields.io/github/license/vmikk/NextITS)](https://github.com/vmikk/NextITS/blob/main/LICENSE) - -[![Runs with Singularity](https://img.shields.io/badge/Runs%20with-Singularity-blue?style=flat&logo=singularity)](https://cloud.sylabs.io/library/vmiks/nextits/nextits) -[![Runs with Docker](https://img.shields.io/badge/Runs%20with-Docker-blue?style=flat&logo=docker)](https://hub.docker.com/r/vmikk/nextits/tags) -[![Runs with Podman](https://img.shields.io/badge/Runs%20with-Podman-blue?style=flat&logo=podman)](https://podman.io/) - -[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.15074881.svg)](https://doi.org/10.5281/zenodo.15074881) - -NextITS is an automated pipeline for metabarcoding fungi and other eukaryotes with full-length ITS sequenced with PacBio. -Amplicons obtained with Illumina are also supported. - -## Introduction - -The most widely used genetic markers for metabarcoding fungal communities are highly variable rRNA ITS1 and ITS2 sub-regions of the internal transcribed spacer. High-throughput metabarcoding has greatly improved our understanding of fungal community ecology. Here, we present NextITS, an automated pipeline for analyzing full-length ITS sequences (ITS1-5.8S-ITS2) from the Pacific Biosciences (PacBio) third-generation sequencing platform. Although the PacBio HiFi reads are highly accurate, the primary type of sequencing error is insertions or deletions in homopolymeric sites, which are also naturally common in fungal ITS. In the pipeline, we implemented correction of homopolymer errors, detection of tag-switching artefacts, and recovery of sequences false-positively annotated as chimeric. The pipeline is built using Nextflow workflow manager, with all the software dependencies packaged into Docker and Singularity containers. - -## User Documentation - -User documentation: https://Next-ITS.github.io/ - -## Quick Start - -``` -nextflow run vmikk/NextITS -r main \ - -profile singularity \ - -resume \ - --input "pacbio_ccs.fastq.gz" \ - --barcodes "sample_barcodes.fasta" \ - --primer_forward "GTACACACCGCCCGTCG" \ - --primer_reverse "CCTSCSCTTANTDATATGC" \ - --its_region "full" \ - --outdir "Results" -``` - -## Citation - -Mikryukov V., Anslan S., Tedersoo L. NextITS: a pipeline for metabarcoding fungi and other eukaryotes with full-length ITS sequenced with PacBio. [https://github.com/vmikk/NextITS](https://github.com/vmikk/NextITS). DOI:10.5281/zenodo.15074881 - diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/ITSx_to_DuckDB.sh b/src/pipecraft-core/service_scripts/NextITS/bin/ITSx_to_DuckDB.sh deleted file mode 100755 index 84b69ae5..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/ITSx_to_DuckDB.sh +++ /dev/null @@ -1,146 +0,0 @@ -#!/bin/bash - -## Import sequences into DuckDB-compatible tables - -## Input = FASTA formatted sequences (header = "hash;size=...") -## Output = Table in DuckDB-native format or Parquet - -# Define usage function -usage() { - echo "Usage: $0 [-i input_file] [-o output_file] [-f format]" - echo " -i : Input FASTA file (required)" - echo " -o : Output file (optional, defaults to input filename with .db/.parquet extension)" - echo " -f : Output format (optional): 'duckdb' or 'parquet' (default, 'parquet')" - exit 1 -} - -# Parse command line arguments -input_file="" -output_file="" -format="parquet" # format="duckdb" - -while getopts "i:o:f:h" opt; do - case $opt in - i) input_file="$OPTARG" ;; - o) output_file="$OPTARG" ;; - f) format="$OPTARG" ;; - h) usage ;; - ?) usage ;; - esac -done - -# Validate required parameters -if [ -z "$input_file" ]; then - echo "Error: Input file is required" - usage -fi - -# Validate output format -if [ "$format" != "duckdb" ] && [ "$format" != "parquet" ]; then - echo "Error: Format must be either 'duckdb' or 'parquet'" - usage -fi - -## Extract rRNA region name from filename -if [[ $input_file =~ ([^.]+)\.fasta\.gz$ ]]; then - rRNA_part="${BASH_REMATCH[1]}" -else - echo "Error in extracting rRNA region name from filename" - rRNA_part="X" -fi - -## Check if rRNA region name is valid -VALID_PARTS=("full" "SSU" "ITS1" "5_8S" "ITS2" "LSU") -if [[ ! " ${VALID_PARTS[@]} " =~ " ${rRNA_part} " ]]; then - echo "..Error: Invalid rRNA region name. Supported names are: ${VALID_PARTS[*]}" - rRNA_part="X" -fi - -## 'full' is a reserved keyword in DuckDB, rename to ITS -if [ "$rRNA_part" == "full" ]; then - rRNA_part="ITS" -fi - -## DuckDB table name cannot start with a number -if [[ "$rRNA_part" == "5_8S" ]]; then - rRNA_part="S58" -fi - -## Extract sample name from filename -sample_name="${input_file/.fasta.gz/}" - -# Set output file if not specified -if [ -z "${output_file}" ]; then - if [ "${format}" == "duckdb" ]; then - output_file="${sample_name}.db" - else - output_file="${sample_name}.parquet" - fi -fi - -## Check if input file exists -if [ ! -f "${input_file}" ]; then - echo "..Error: File ${input_file} not found" - exit 1 -fi - -echo "..Importing ${input_file} into ${output_file} (format: ${format})" - -if [ "$format" == "duckdb" ]; then - seqkit fx2tab "${input_file}" \ - | sed 's/;size=/\t/' \ - | duckdb "${output_file}" \ - " - DROP TABLE IF EXISTS ${rRNA_part}; - CREATE TABLE ${rRNA_part} ( - SeqID VARCHAR PRIMARY KEY, - Abundance INTEGER, - Sequence VARCHAR - ); - - INSERT INTO ${rRNA_part} - SELECT * FROM read_csv( - '/dev/stdin', - header = false, delim = '\t', - columns = { - 'SeqID': 'VARCHAR', - 'Abundance': 'INTEGER', - 'Sequence': 'VARCHAR' - } - );" -else - seqkit fx2tab "${input_file}" \ - | sed 's/;size=/\t/' \ - | duckdb -c " - COPY ( - SELECT * FROM read_csv( - '/dev/stdin', - header = false, delim = '\t', - columns = { - 'SeqID': 'VARCHAR', - 'Abundance': 'INTEGER', - 'Sequence': 'VARCHAR' - } - ) - ) TO '${output_file}' (FORMAT PARQUET, COMPRESSION 'ZSTD', COMPRESSION_LEVEL 12);" -fi - -echo "..Data imported to ${output_file}" - - -#### Check the data -# duckdb "$db_file" -# -# -- Show all tables -# SHOW TABLES; -# SELECT * FROM information_schema.tables; -# -# -- Show all column names and their types -# DESCRIBE ITS1; -# -# -- Show first 10 rows -# SELECT * FROM ITS1 LIMIT 10; -# -# -- Get count of rows -# SELECT COUNT(*) FROM ITS1; -# \ No newline at end of file diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/aggregate_sequences.R b/src/pipecraft-core/service_scripts/NextITS/bin/aggregate_sequences.R deleted file mode 100755 index 8f71d9f2..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/aggregate_sequences.R +++ /dev/null @@ -1,173 +0,0 @@ -#!/usr/bin/env Rscript - -## Script to aggregate sequences from multiple runs into a single file (for dereplication and subsequent clustering) -## Also, performs removal of de novo chimeras with high scores (with option to recover sequences that occurred in multiple runs) - -## Do-novo chimera recovery: -# if a sequence identified as putative chimera was observed in the other samples, -# where there is no evidence that it is chimeric, it will be recovered - - -## Function to load packages -load_pckg <- function(pkg = "data.table"){ - suppressPackageStartupMessages( library(package = pkg, character.only = TRUE) ) - cat(".. ", paste(pkg, packageVersion(pkg), "\n")) -} - -cat("Loading packages:\n") - -load_pckg("optparse") -load_pckg("data.table") -load_pckg("Biostrings") -load_pckg("plyr") -load_pckg("arrow") -# load_pckg("dplyr") - - -cat("Parsing input options and arguments...\n") - -option_list <- list( - make_option("--seqtabs", action="store", default=NA, type='character', help = "Direcotry containing long tables with quality-filtered sequences (Parquet format)"), - make_option("--maxchim", action="store", default=0.6, type='numeric', help = "Maximum de novo chimera score to remove"), - make_option("--recoverdenovo", action="store", default=TRUE, type='logical', help="Recover de-novo chimeras (logical)"), - make_option("--output", action="store", default="Seqs", type='character', help = "Output prefix"), - make_option("--threads", action="store", default=4, type='integer', help = "Number of CPU threads to use") -) - -opt <- parse_args(OptionParser(option_list=option_list)) - -## Function to convert text "NA"s to NA -to_na <- function(x){ - if(x %in% c("NA", "null", "Null")){ x <- NA } - return(x) -} - -## Replaces "null"s from Nextflow with NA -opt <- lapply(X = opt, FUN = to_na) - - -## Validation of the required arguments -if (is.na(opt$seqtabs)) { - stop("Input directory with quality-filtered sequences is not specified\n") -} - -## Assign variables -SEQTABS <- opt$seqtabs -MAXCHIM <- opt$maxchim -RECOV_DENOVO <- opt$recoverdenovo -OUTPUT <- opt$output -CPUTHREADS <- as.numeric( opt$threads ) - -## Log assigned variables -cat(paste("Path to sequence tables: ", SEQTABS, "\n", sep="")) -cat(paste("Max de novo chimera score: ", MAXCHIM, "\n", sep="")) -cat(paste("De novo chimera recovery: ", RECOV_DENOVO, "\n", sep="")) -cat(paste("Output prefix: ", OUTPUT, "\n", sep="")) -cat(paste("CPU threads: ", CPUTHREADS, "\n", sep="")) - -cat("\n") - -## Set CPU thread number -cat("Setting number of CPU threads to: ", CPUTHREADS, "\n") -setDTthreads(threads = CPUTHREADS) # for data.table -set_cpu_count(CPUTHREADS) # for arrow - - -###################################### -###################################### Process the data -###################################### - -## Load sequence tables -cat("\n..Looking for sequence tables\n") -TABS <- list.files(path = SEQTABS, pattern = ".parquet", full.names = TRUE, recursive = TRUE) -cat("... Tables found: ", length(TABS), "\n") - -cat("\n..Loading sequence tables\n") -TAB <- alply(.data = TABS, .margins = 1, .fun = function(x){ - res <- arrow::read_parquet(x) - setDT(res) - return(res) -}) -TAB <- rbindlist(TAB, use.names = TRUE, fill = TRUE) -cat("... Total number of records: ", nrow(TAB), "\n") -cat("... Total number unique sequences: ", length(unique(TAB$Sequence)), "\n") -cat("... Total number unique samples (fastq files): ", length(unique(TAB$SampleID)), "\n") - - -## Filter sequences by chimeric score (MAXCHIM) -if(!is.na(MAXCHIM)){ - - cat("\n..Filtering data by max de novo chimera score\n") - nrecs <- nrow(TAB) - nabun <- sum(TAB$Abundance, na.rm = TRUE) - - cat("... Max de novo chimera score observed: ", max(TAB$DeNovo_Chimera_Score, na.rm = TRUE), "\n") - - ## If no chimera recovery is required - if(RECOV_DENOVO == FALSE){ - - TAB <- TAB[ DeNovo_Chimera_Score < MAXCHIM | is.na(DeNovo_Chimera_Score) ] - - ## If we need to recover chimeras - } else { - - ## Find putative chimeras - CHIMERAS <- TAB[ DeNovo_Chimera_Score >= MAXCHIM, .(SeqID___SampleID, DeNovo_Chimera_Score, Sequence, Abundance) ] - NONCHIMERAS <- TAB[ ! SeqID___SampleID %in% CHIMERAS$SeqID___SampleID ] - - ## Recover false-positives - chim_seqs <- unique(CHIMERAS$Sequence) - nonchim_seqs <- unique(NONCHIMERAS$Sequence) - fp_chims <- chim_seqs %in% nonchim_seqs - if(any(fp_chims)){ - cat(".... Probably there are a few false-positive chimeras\n") - cat(".... Recovering ", sum(fp_chims), "sequences\n") - fp_seqs <- chim_seqs[ fp_chims ] - CHIMERAS <- CHIMERAS[ ! Sequence %in% fp_seqs ] - rm(fp_seqs) - } - - TAB <- TAB[ ! Sequence %in% CHIMERAS$Sequence ] - rm(CHIMERAS, NONCHIMERAS) - - } # end of chimera recovery - - ## Data summary after filtering - nrecs_delta <- nrecs - nrow(TAB) - nabun_delta <- nabun - sum(TAB$Abundance, na.rm = TRUE) - - cat("... Records removed: ", nrecs_delta, " (", round(nrecs_delta/nrecs * 100, 1), "%)\n") - cat("... Reads removed: ", nabun_delta, " (", round(nabun_delta/nabun * 100, 1), "%)\n") - - rm(nrecs_delta, nabun_delta) - -} # end of MAXCHIM filtering - - -cat("\n..Sorting table by abundance, quality score\n") -setorder(x = TAB, -Abundance, -PhredScore, SeqID) - -cat("..Preparing FASTA file\n") - -SQF <- DNAStringSet(x = TAB$Sequence) -names(SQF) <- paste0(TAB$SeqID, ";size=", TAB$Abundance) # , ";sample=", TAB$SampleID, ";" - -## Export FASTA -cat("..Exporting FASTA file with filtered sequences\n") - -writeXStringSet( - x = SQF, - filepath = paste0(OUTPUT, ".fa.gz"), - compress = TRUE, format = "fasta", width = 9999) - -## Export FASTA -cat("..Exporting filtered table\n") - -write_parquet( - x = TAB, - sink = paste0(OUTPUT, ".parquet"), - compression = "zstd", - compression_level = 10, - use_dictionary = TRUE) - -cat("All done.\n") diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/bucketize_db.R b/src/pipecraft-core/service_scripts/NextITS/bin/bucketize_db.R deleted file mode 100755 index e5b0874c..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/bucketize_db.R +++ /dev/null @@ -1,248 +0,0 @@ -#!/usr/bin/env Rscript - -## Aim - evenly distribute sequence clusters across a specified number of buckets. -## The goal is to have the total length of sequences in each bucket as equal as possible. - -## Number of buckets can be automatically selected -## (e.g., to avoid the DADA2s' error message `long vectors not supported yet`, related with > 2^31 elements) - -## Usage examples: -# bucketize_db.R \ -# --db stat_clusters.txt \ -# --fasta Input.fa.gz \ -# --summary bucket_summary.txt \ -# --numbuckets 10 \ -# --threads 10 - - -## Check time -start_time <- Sys.time() - -cat("\nParsing input options and arguments...\n") - -suppressPackageStartupMessages(require(optparse)) - -## Parse arguments -option_list <- list( - make_option(c("-d", "--db"), action="store", default="DB_clu.tsv", type='character', help="Clustering database"), - make_option(c("-f", "--fasta"), action="store", default="Input.fa.gz", type='character', help="Input sequences in FASTA format"), - make_option(c("-s", "--summary"), action="store", default="bucket_summary.txt", type='character', help="Output file summary information"), - make_option(c("-n", "--numbuckets"), action="store", default=NA, type='integer', help="Number of output buckets (NA, for automatic selection)"), - make_option(c("-t", "--threads"), action="store", default=4, type='integer', help="Number of CPU threads to use") -) -opt <- parse_args(OptionParser(option_list=option_list)) - -# Validation of the required arguments -if(is.na(opt$fasta)){ - stop("Input file with sequences is not specified\n") -} -if(is.na(opt$db)){ - stop("Clustering results are not specified\n") -} -if(!is.na(opt$numbuckets) & opt$numbuckets <= 1){ - stop("Number of buckets should be > 1\n") -} - - -## Assign variables -DATABASE <- opt$db -FASTA <- opt$fasta -SUMMARY <- opt$summary -NBUCKETS <- opt$numbuckets -THREADS <- opt$threads - -## Log assigned variables -cat("\nParameters specified:\n") -cat(paste("Clustering database: " , DATABASE, "\n", sep = "")) -cat(paste("Input sequences (FASTA): " , FASTA, "\n", sep = "")) -cat(paste("Output with bucket summary: ", SUMMARY, "\n", sep = "")) -if(is.na(NBUCKETS)){ - cat(paste("Number of buckets: ", "auto", "\n", sep = "")) -} else { - cat(paste("Number of buckets: ", NBUCKETS, "\n", sep = "")) -} - -cat(paste("CPU threads: ", THREADS, "\n", sep = "")) -cat("\n") - - -############################################## Load packages - -cat("Loading R packages...\n") - -load_pckg <- function(pkg = "data.table"){ - suppressPackageStartupMessages( library(package = pkg, character.only = TRUE) ) - cat(paste(pkg, packageVersion(pkg), "\n")) -} - -load_pckg("data.table") -load_pckg("Biostrings") -load_pckg("plyr") - -if(THREADS < 1){ THREADS <- 1 } -if(THREADS > 1){ - cat("Preparing multi-threaded setup\n") - - load_pckg("doFuture") - registerDoFuture() - plan(multicore, workers = THREADS) - options(future.globals.maxSize = 6e10) # 60GB - - setDTthreads(threads = THREADS) # for data.table - - parall <- TRUE - -} else { - parall <- FALSE - setDTthreads(threads = 1) -} - -cat("\n") - - -############################################## Workflow - -## Load seq stats -cat("..Loading input sequences\n") -seqs <- readDNAStringSet(filepath = FASTA) - -## Load clustering file -cat("..Loading clustering file\n") -DB <- fread(file = DATABASE, - sep = "\t", header = FALSE, - col.names = c("Cluster", "Member")) - -## Estimate sequence length -cat("..Estimating total length of the sequences\n") -seqt <- data.table(Member = names(seqs), Len = width(seqs)) -DB <- merge(x = DB, y = seqt, by = "Member", all.x = TRUE) -rm(seqt) - -## Estimate number of sequences per cluster and the total length of sequences -cat("..Estimating cluster sizes\n") -datt <- DB[ , .(num_seqs = .N, sum_len = sum(Len, na.rm = TRUE)), by = "Cluster" ] - -## Sort clusters by the number of sequenes in descending order -cat("..Sorting clusters\n") -setorder(datt, -sum_len, -num_seqs) - - -cat("..Bucketizing\n") - -if(is.na(NBUCKETS)){ - cat("...Number of buckets is not specified, using automatic selection\n") - - ## For DADA2, a matrix with quality values is required `as(Biostrings::quality(fq), "matrix")` - ## It should not exceed 2^31 (2147483648) elemens, - ## Meaning that `num_seq * len_seq` must be < 2^31 - - ## Calculate approximate estimate for the maximum number of sequences per bucket - maxseqs <- 2^31 / max(DB$Len) # quantile(x = DB$Len, probs = 0.99) - - ## Number of buckets - NBUCKETS <- ceiling(nrow(DB) / maxseqs) - - cat("...The sugested number of buckets is ", NBUCKETS, "\n") -} - - -## Initializing buckets and bucket sizes -buckets <- vector("list", length = NBUCKETS) -bucket_size_numseqs <- numeric(NBUCKETS) -bucket_size_lenseqs <- numeric(NBUCKETS) - -## Distributing files into buckets -## By starting with the largest files and placing each one in the currently smallest bucket, -## we try to prevent any single bucket from becoming significantly larger than the others -for (i in 1:nrow(datt)) { - - ## Find the bucket with the minimum total sequence length - min_bucket_index <- which.min(bucket_size_lenseqs) - - ## Add the cluster ID to the chosen bucket - buckets[[ min_bucket_index ]] <- c( - buckets[[ min_bucket_index ]], - datt[i, ]$Cluster - ) - - # Updating the total sequence length of the chosen bucket - bucket_size_lenseqs[ min_bucket_index ] <- bucket_size_lenseqs[min_bucket_index] + datt[i, ]$sum_len - bucket_size_numseqs[ min_bucket_index ] <- bucket_size_numseqs[min_bucket_index] + datt[i, ]$num_seqs - -} - -cat("..Bucket summary:\n\n") - -## Prepare bucket summary -smr <- data.table( - BucketID = 1:length(buckets), - Num_clusters = laply(.data = buckets, .fun = function(x){ length(x) }), - sum_len = bucket_size_lenseqs, - num_seqs = bucket_size_numseqs) - -print(smr) - -## Add percentages -smr[ , NumClust_Percent := round(Num_clusters / sum(Num_clusters) * 100, 2) ] -smr[ , TotLen_Percent := round(sum_len / sum(sum_len) * 100, 2) ] -smr[ , TotSeqs_Percent := round(num_seqs / sum(num_seqs) * 100, 2) ] - - -cat("\n\n..Exporting FASTA file for each bucket\n") - -## Exporting function -export_bucket <- function(clustnum = 1){ - - cat("...Bucket ", clustnum, "\n") - - ## IDs of cluster representatives - clustids <- buckets[[ clustnum ]] - - ## Find sequence IDs to export - ids <- data.table(SeqID = DB[ Cluster %in% clustids ]$Member) - - ## Sort sequences by size - ids[ , Size := tstrsplit(SeqID, split = ";", keep = 2) ] - ids[ , Size := as.numeric( sub(pattern = "size=", replacement = "", x = Size) ) ] - setorder(ids, -Size, SeqID) - - ## Cluster ID with leading zero - cl <- sprintf(paste0("%0", nchar(NBUCKETS), "d"), clustnum) - - ## Extract and export - writeXStringSet( - x = seqs[ ids$SeqID ], - filepath = paste0("bucket_", cl, ".fa.gz"), - compress = TRUE, - format = "fasta", - width = 9999) - -} - -a_ply( - .data = seq_along(buckets), - .margins = 1, - .fun = export_bucket, - .parallel = parall) - - -## Bucket summary -cat("..Exporting bucket summary\n") -fwrite(x = smr, file = SUMMARY, sep = "\t", col.names = TRUE) - - -cat("\nAll done.\n") - - -##################### Session info - -## Check time -end_time <- Sys.time() - -tmm <- as.numeric(difftime(end_time, start_time, units = "min")) -cat("\nElapsed time: ", tmm, " minutes\n") - -cat("\n") -cat("Session info:\n") -sessionInfo() -cat("\n") diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/chimera_rescue.R b/src/pipecraft-core/service_scripts/NextITS/bin/chimera_rescue.R deleted file mode 100755 index b53f3a3f..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/chimera_rescue.R +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env Rscript - -## Script to rescue sequences that were annotated as chimeric, -## but have high occurrence within sequenceing run (occurrence > 2) - -# Input is given as positional arguments: -# 1. List of all chimeric sequences (`All_chimeras.txt.gz`) -# 2. Min sequence occurrence to be preserved (e.g., 2) -# 3. Output file name (`Rescued_Chimeric_sequences.fa.gz`) - -suppressMessages(library(data.table)); setDTthreads(threads = 1) -suppressMessages(library(Biostrings)) - -args <- commandArgs(trailingOnly = TRUE) - -MINOCC <- as.numeric( args[2] ) - -## Load sequences -cat("..Loading chimeric sequences\n") -CH <- try( - fread(file = args[1], - sep = "\t", header = F, - col.names = c("SeqID", "Seq")) - ) - -if("try-error" %in% class(CH)){ - cat("\nCould not read the file with chimeric sequences\n") - cat("Most likely, the file file is empty (no chimeras)\n") - q(save = "no", status = 0, runLast = FALSE) -} - -cat("..Total number of chimeric records: ", nrow(CH), "\n") - -if(nrow(CH) > 0){ - - ## Extract sample name and sequencing run ID - CH[, SampleID := tstrsplit(x = SeqID, split = ";", keep = 2) ] - CH[, SampleID := gsub(pattern = "sample=", replacement = "", x = SampleID) ] - - ## Estimate sequence frequency - cat("..Estimating chimera occurrence\n") - CF <- CH[, .(Occurrence = .N), by = "Seq"] - - ## Exclude sequences with low occurrence (most probably chimeric) - ## Sequences with higher occurrence should be "real" sequences - CF <- CF[ Occurrence > MINOCC ] - - cat("..Total number of unique chimeric sequences: ", length(unique(CH$Seq)), ".\n") - - ## Export sequences - if(nrow(CF) > 0){ - cat("..There are", nrow(CF), "unique sequence to rescue.\n") - NCH <- CH[ Seq %in% CF$Seq ] - SQS <- DNAStringSet(x = NCH$Seq) - names(SQS) <- NCH$SeqID - - cat("..Exporting rescued sequences\n") - writeXStringSet(x = SQS, filepath = args[3], - compress=TRUE, format="fasta", width=9999) - } else { - cat("..No sequences were rescued.\n") - } - -} \ No newline at end of file diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/convert_IUPAC.sh b/src/pipecraft-core/service_scripts/NextITS/bin/convert_IUPAC.sh deleted file mode 100755 index 4a250b74..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/convert_IUPAC.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash - -## Function to convert IUPAC codes in primers -# Based on PipeCraft2 scripts -# https://github.com/SuvalineVana/pipecraft/blob/main/src/pipecraft-core/service_scripts/submodules/framework.functions.sh -# Git commit 5650545 (Jun 9, 2022) -# Author - Sten Anslan - -echo "$1" | \ -if grep -q -E "R|Y|S|W|K|M|B|D|H|V|N|I" ; then - - ## Define IUPAC codes - R=$"[AG]" - Y=$"[CT]" - S=$"[GC]" - W=$"[AT]" - K=$"[GT]" - M=$"[AC]" - B=$"[CGT]" - D=$"[AGT]" - H=$"[ACT]" - V=$"[ACG]" - N=$"[ATGC]" - I=$"[ATGC]" - - ## Replace IUPAC codes - primer=$(echo "$1" | \ - sed -e "s/R/$R/g; s/Y/$Y/g; \ - s/S/$S/g; s/W/$W/g; s/K/$K/g; \ - s/M/$M/g; s/B/$B/g; s/D/$D/g; \ - s/H/$H/g; s/V/$V/g; s/N/$N/g; \ - s/I/$I/g") - - ## Return convered primer - echo "$primer" -else - ## Return original primer when no IUPAC codes were detected - echo "$1" -fi - -## Example: -# ./convert_IUPAC.sh "CGACCWGCGGARGGATCATTA" # CGACC[AT]GCGGA[AG]GGATCATTA diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/count_homopolymer_stats.sh b/src/pipecraft-core/service_scripts/NextITS/bin/count_homopolymer_stats.sh deleted file mode 100755 index c2c4af75..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/count_homopolymer_stats.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -# $1 = input file -# $2 = text to add to the resulting file - -zcat "$1" \ - | awk \ - -F '\t' -v OFS='\t' \ - -v fnm="$2" \ - '$1 ~ /H/ { print fnm , $9 , $10 }' \ - | sed 's/_uch.uc//' - diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/count_number_of_reads.sh b/src/pipecraft-core/service_scripts/NextITS/bin/count_number_of_reads.sh deleted file mode 100755 index ae811632..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/count_number_of_reads.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -## Count number of reads in the dereplicated file -## Size annotations should be in USEARCH-style (e.g., size=100) - -# $1 = input file -# $2 = text to add to the resulting file - -seqkit seq --name "$1" \ - | grep -Po ';size=[0-9]+' \ - | sed 's/;size=//g' \ - | awk -F '\t' -v OFS='\t' -v fnm="$2" '{sum+=$1} END {print fnm , sum}' diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/dada2_no_quals.R b/src/pipecraft-core/service_scripts/NextITS/bin/dada2_no_quals.R deleted file mode 100755 index 930c4363..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/dada2_no_quals.R +++ /dev/null @@ -1,421 +0,0 @@ -#!/usr/bin/env Rscript - -## Perform sequence denoising with DADA2 - -### Notes: -## - `USE_QUALS = FALSE` will be deprecated -## https://github.com/benjjneb/dada2/issues/816#issuecomment-521836313 -## therefore, use `noqualErrfun` -## - - -## TO DO: -## - Benchmark params (especially BAND_SIZE, DETECT_SINGLETONS, and OMEGA_A) - - -## Usage example: -# dada2_no_quals.R \ -# --input input.fq.gz \ -# --nbases 1e6 \ -# --bandsize 16 \ -# --detectsingletons TRUE \ -# --omegaA 1e-20 \ -# --omegaC 1e-40 \ -# --omegaP 1e-4 \ -# --maxconsist 10 \ -# --match 4 \ -# --mismatch -5 \ -# --gappenalty -8 \ -# --threads 8 - -## Outputs: -# - DADA2_ErrorRates_noqualErrfun.RData -# - DADA2_denoised.fa.gz -# - DADA2_denoised.uc.gz -# - DADA2_UC.qs -# - DADA2_denoising_summary.txt - - -############################################## Parse input parameters - -## Check time -start_time <- Sys.time() - - -cat("\nParsing input options and arguments...\n") - -suppressPackageStartupMessages(require(optparse)) - -## Parse arguments -option_list <- list( - make_option(c("-i", "--input"), action="store", default=NA, type='character', help=""), - make_option(c("-n", "--nbases"), action="store", default=1e6, type='double', help=""), - make_option(c("-b", "--bandsize"), action="store", default=16, type='double', help=""), - make_option(c("-s", "--detectsingletons"), action="store", default=TRUE, type='logical', help=""), - make_option(c("-A", "--omegaA"), action="store", default=1e-20, type='double', help=""), - make_option(c("-C", "--omegaC"), action="store", default=1e-40, type='double', help=""), - make_option(c("-P", "--omegaP"), action="store", default=1e-4, type='double', help=""), - make_option(c("-x", "--maxconsist"), action="store", default=10, type='integer', help=""), - make_option("--match", action="store", default=4, type='double', help=""), - make_option("--mismatch", action="store", default=-5, type='double', help=""), - make_option("--gappenalty", action="store", default=-8, type='double', help=""), - make_option("--hpgap", action="store", default=NULL, type='double', help=""), - make_option(c("-t", "--threads"), action="store", default=4L, type='integer', help="Number of CPU threads for arrow, default 4") -) -opt <- parse_args(OptionParser(option_list=option_list)) - -# Validation of the required argiments -if(is.na(opt$input)){ - cat("Input file is not specified: ....\n", file=stderr()) - stop() -} - - -## Function to convert text "NA"s to NA -# to_na <- function(x){ -# if(x %in% c("NA", "null", "Null")){ x <- NA } -# return(x) -# } - -## Assign variables -INPUT <- opt$input -NBASES <- opt$nbases -BAND_SIZE <- opt$bandsize -DETECT_SINGLETONS <- opt$detectsingletons -OMEGA_A <- opt$omegaA -OMEGA_C <- opt$omegaC -OMEGA_P <- opt$omegaP -MAX_CONSIST <- opt$maxconsist -MATCH <- opt$match -MISMATCH <- opt$mismatch -GAP_PENALTY <- opt$gappenalty -HOMOPOLYMER_GAP_PENALTY <- opt$hpgap # PacBio CCS does not make homopolymer errors at a higher rate than normal indels -> NULL -CPUTHREADS <- opt$threads - - -## Log assigned variables -cat("\nParameters specified:\n") -cat(paste("Input file: " , INPUT, "\n", sep="")) -cat(paste("Number of bases to use for error rate learning: ", NBASES, "\n", sep = "")) -cat(paste("Band size for the Needleman-Wunsch alignment: ", BAND_SIZE, "\n", sep = "")) -cat(paste("Singleton detection: ", DETECT_SINGLETONS, "\n", sep = "")) -cat(paste("OMEGA_A: ", OMEGA_A, "\n", sep = "")) -cat(paste("OMEGA_C: ", OMEGA_C, "\n", sep = "")) -cat(paste("OMEGA_P: ", OMEGA_P, "\n", sep = "")) -cat(paste("Number of iterations of the self-consistency loop: ", MAX_CONSIST, "\n", sep = "")) -cat(paste("Alignment for matches: ", MATCH, "\n", sep = "")) -cat(paste("Alignment for mismatches: ", MISMATCH, "\n", sep = "")) -cat(paste("Gap penalty: ", GAP_PENALTY, "\n", sep = "")) -cat(paste("Homopolymer gap penalty: ", HOMOPOLYMER_GAP_PENALTY, "\n", sep = "")) -cat(paste("Number of CPU threads to use: ", CPUTHREADS, "\n", sep="")) - -cat("\n") - - - -############################################## Load packages - -cat("Loading R packages...\n") - -load_pckg <- function(pkg = "data.table"){ - suppressPackageStartupMessages( library(package = pkg, character.only = TRUE) ) - cat(paste(pkg, packageVersion(pkg), "\n")) -} - -load_pckg("Biostrings") -load_pckg("ShortRead") -load_pckg("data.table") -load_pckg("dada2") - -cat("\n") - -## Set CPU thread number -cat("Setting number of CPU threads to: ", CPUTHREADS, "\n") -setDTthreads(threads = CPUTHREADS) # for data.table - -## Set seed -set.seed(111) - -## Set DADA options -cat("Setting DADA2 options\n") -setDadaOpt( - BAND_SIZE = BAND_SIZE, # dada2 default, 16 - DETECT_SINGLETONS = DETECT_SINGLETONS, # dada2 default, FALSE - OMEGA_A = OMEGA_A, # dada2 default, 1e-40 - OMEGA_C = OMEGA_C, # dada2 default, 1e-40 - OMEGA_P = OMEGA_P, # dada2 default, 1e-4 - MAX_CONSIST = MAX_CONSIST, # dada2 default, 10 - GAP_PENALTY = GAP_PENALTY, # dada2 default, -8 - MATCH = MATCH, # dada2 default, 4 - MISMATCH = MISMATCH, # dada2 default, -5 - HOMOPOLYMER_GAP_PENALTY = HOMOPOLYMER_GAP_PENALTY # PacBio CCS does not make homopolymer errors at a higher rate than normal indels - ) - -## Get DADA options -# getDadaOpt() - -############################################## Workflow - - -## Load FASTQ file -cat("\nLoading input data\n") -fq <- readFastq(dirPath = INPUT, qualityType = "FastqQuality") - -## Extract sequence headers -cat("Processing sequences\n") -sq <- as.data.table(fq@id) -setnames(x = sq, new = "SeqName") -sq[ , c("SeqID", "Abundance") := tstrsplit(x = SeqName, split = ";size=", keep = 1:2) ] -sq[ , Abundance := as.numeric(Abundance) ] -sq[ , Sequence := as.character(sread(fq))] - -## Extract sequence qualities -cat("Processing sequence quality scores\n") -seq_quals <- as(quality(fq), "matrix") -# dada2:::qtables2(fq) - -## Summary stats -num_seqs <- nrow(sq) -num_singl <- nrow(sq[ Abundance < 2 ]) -num_reads <- sum(sq$Abundance, na.rm = TRUE) -perc_nonsingleton <- round((num_seqs - num_singl) / num_seqs * 100, 2) - -cat("\n") -cat("Number of unique sequences detected: ", num_seqs, "\n") -cat("Number of singleton sequences: ", num_singl, "\n") -cat("Total abundance of sequences: ", num_reads, "\n") -cat("Percentage of non-singleton sequences: ", round(perc_nonsingleton, 2), "\n") - -## Test the rule of thumb, https://github.com/benjjneb/dada2/issues/1663#issuecomment-1359905397 -if(perc_nonsingleton < 10){ - cat("WARNING: <10% of reads are duplicates of other reads,\n") - cat(" meaning that DADA2 might not be the right algorithmic choice\n") -} - - -## Manually create a derep-class object -## See also https://github.com/benjjneb/dada2/blob/004ce26909268e1318a2f68e0ea26807412c7a2d/R/sequenceIO.R#L240-L242 -# https://github.com/benjjneb/dada2/blob/004ce26909268e1318a2f68e0ea26807412c7a2d/R/sequenceIO.R#L45 - -## Prepare derep-class object -cat("\nPreparing derep-class object\n") -uniques <- sq$Abundance -names(uniques) <- as.character(sread(fq)) # names = full amplicon sequence -rownames(seq_quals) <- names(uniques) - -derep <- list( - uniques = uniques, - quals = seq_quals, - map = NULL, - SeqID = sq$SeqID # add allso sequence IDs - ) - -derep <- as(derep, "derep") - -## Clean up -rm(uniques, seq_quals) - - - -## Estimate error rates for each type of transition while ignoring quality scores -cat("\nEstimating error rates\n") -errors <- try( - learnErrors( - fls = derep, - nbases = NBASES, - errorEstimationFunction = noqualErrfun, - qualityType = "FastqQuality", - verbose = 1, - multithread = CPUTHREADS - ) - ) - -## Retry if multithreading failed -if("try-error" %in% class(errors) & CPUTHREADS > 1){ - cat("..Multi-threaded error rate estimation failed\n") - cat("..Trying to resume with a single CPU thread\n") - - errors <- learnErrors( - fls = derep, - nbases = NBASES, - errorEstimationFunction = noqualErrfun, - qualityType = "FastqQuality", - verbose = 1, - multithread = 1 - ) -} -if("try-error" %in% class(errors) & CPUTHREADS == 1){ - stop("..Error rate estimation failed\n") -} - - -## Export results -cat("\nExporting error rates\n") -saveRDS(object = errors, - file = "DADA2_ErrorRates_noqualErrfun.RData", - compress = "xz") - - -## Plot observed and estimated error rates -# plotErrors(errors) - - -## Run sample inference with DADA2 -cat("\nRunning sample inference\n") -dadares <- dada( - derep = derep, - err = errors, - errorEstimationFunction = noqualErrfun, - selfConsist = FALSE, - verbose = 1, - multithread = CPUTHREADS) - -cat("\nExporting DADA2 object\n") -saveRDS(object = dadares, - file = "DADA2_InferedSeqs_noqualErrfun.RData", - compress = "xz") - - -## Prepare resulting data -cat("Preparing resulting table\n") -res <- data.table( - Sequence = dadares$sequence, - Abundance = dadares$denoised) - -## Add sequence IDs -res[ , SeqNumID := .I ] -res <- merge( - x = res, - y = sq[, .(SeqID, Sequence)], - by = "Sequence", all.x = TRUE) - -## Sort by abundance -setorder(res, -Abundance, SeqID, na.last = TRUE) - -## Export denoised sequences -cat("Exporting denoised sequences\n") -ASVS <- DNAStringSet(x = res$Sequence) -names(ASVS) <- paste0(res$SeqID, ";size=", res$Abundance) - -writeXStringSet( - x = ASVS, - filepath = "DADA2_denoised.fa.gz", - compress = TRUE, - format = "fasta", - width = 20000) - - - -## Create UC file -cat("Preparing pseudo-UC file\n") -UC <- data.table( - DerepSeqID = derep$SeqID, - SeqNumID = dadares$map, - Abundance = derep$uniques) - -UC <- merge( - x = UC, - y = res[ , .(SeqNumID, SeqID) ], - by = "SeqNumID", all.x = TRUE) - -setorder(UC, SeqNumID, na.last = TRUE) -setnames(x = UC, old = "SeqID", new = "ASV") - -## Export pre-UC file -cat("Exporting pre-UC file\n") -# saveRDS( -# object = UC, -# file = "DADA2_UC.RData", -# compress = "xz") -qs::qsave(x = UC, file = "DADA2_UC.qs", - preset = "custom", algorithm = "zstd", compress_level = 15L, nthreads = CPUTHREADS) - -## Summary stats -num_asvs <- nrow(res) -num_asvreads <- sum(res$Abundance, na.rm = T) -num_merged <- nrow(UC[ !is.na(SeqNumID) & DerepSeqID != ASV ]) # excluding representative seqs -num_dsc <- nrow(UC[ is.na(SeqNumID) ]) -num_dscreads <- sum(UC[ is.na(SeqNumID) ]$Abundance) -perc_dsc <- round(num_dsc / num_seqs * 100, 2) -perc_dscreads <- round(num_dscreads / num_reads * 100, 2) - -cat("\nRun summary:\n") -cat("Number of ASVs infered: ", num_asvs, "\n") -cat("Number of reads in ASV table: ", num_asvreads , "\n") -cat("Number of sequences merged into ASVs: ", num_merged, "\n") -cat("Number of discarded sequences (%): ", num_dsc, "(", perc_dsc, "% )\n") -cat("Number of reads of discarded sequences (%): ", num_dscreads, "(", perc_dscreads, "% )\n") - - -## Format pseudo-UC file -# 1 Record type S, H, C or N (see table below) -# 2 Cluster number (0-based) -# 3 Sequence length (S, N and H) or cluster size (C) -# 4 For H records, percent identity with target -# 5 For H records, the strand: + or - for nucleotides, . for proteins -# 6 Not used, parsers should ignore this field. Included for backwards compatibility -# 7 Not used, parsers should ignore this field. Included for backwards compatibility -# 8 Compressed alignment or the symbol '=' (equals sign). The = indicates that the query is 100% identical to the target sequence (field 10) -# 9 Label of query sequence (always present) -# 10 Label of target sequence (H records only) - -## Remove noisy sequences -UC <- UC[ ! is.na(SeqNumID), .(DerepSeqID, ASV) ] -UC[ , RecordType := fifelse(DerepSeqID == ASV, "C", "H", na = NA) ] -UC[ , `:=` (ClustNum = NA, SeqLen = NA, Ident = NA, Strand = "+", V6 = NA, V7 = NA, ALN = ".") ] - -setcolorder(x = UC, - neworder = c("RecordType", "ClustNum", "SeqLen", "Ident", "Strand", "V6", "V7", "ALN", "DerepSeqID", "ASV")) - -## Export UC file -cat("\nExporting pseudo-UC file\n") -fwrite(x = UC, - file = "DADA2_denoised.uc.gz", - quote = FALSE, sep = "\t", - col.names = FALSE, row.names = FALSE, - compress = "gzip") - - - -## Write summary -cat("Exporting run statistics\n") -smr <- rbind( - data.table(Param = "Number of unique sequences (prior denoising)", Value = num_seqs), - data.table(Param = "Number of singleton sequences (prior denoising)", Value = num_singl), - data.table(Param = "Total abundance of sequences (prior denoising)", Value = num_reads), - data.table(Param = "Percentage of non-singleton sequences (prior denoising)", Value = perc_nonsingleton), - - data.table(Param = "Number of ASVs infered", Value = num_asvs), - data.table(Param = "Number of reads in ASV table", Value = num_asvreads), - data.table(Param = "Number of sequences merged into ASVs (excluding representative seqs)", Value = num_merged), - data.table(Param = "Number of discarded sequences", Value = num_dsc), - data.table(Param = "Percentage of discarded sequences", Value = perc_dsc), - data.table(Param = "Number of reads of discarded sequences", Value = num_dscreads), - data.table(Param = "Percentage of reads of discarded sequences", Value = perc_dscreads) - ) - -fwrite(x = smr, - file = "DADA2_denoising_summary.txt", - quote = FALSE, sep = "\t") - - - -## Construct sequence table (rows = samples, cols = ASVs) -# makeSequenceTable(dadares, orderBy = "abundance") - - - -cat("\nAll done.\n") - - -##################### Session info - -## Check time -end_time <- Sys.time() - -tmm <- as.numeric(difftime(end_time, start_time, units = "min")) -cat("\nElapsed time: ", tmm, " minutes\n") - -cat("\n") -cat("Session info:\n") -sessionInfo() -cat("\n") diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/disambiguate_primers.R b/src/pipecraft-core/service_scripts/NextITS/bin/disambiguate_primers.R deleted file mode 100755 index 0ba61102..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/disambiguate_primers.R +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env Rscript - -## The script to disambiguate sequences -## (expand ambiguous nucleotides into all combinations) -## Based on IUPAC codes - -# Input is given as positional arguments: -# 1. A text string (e.g., "ACTGNK") -# 2. output file name (e.g., "Primer_F.fasta") - -# Output: -# - FASTA with disambiguated sequences - -args <- commandArgs(trailingOnly = TRUE) - -cat("..Loading packages\n") -suppressMessages(library(DECIPHER)) -suppressMessages(library(Biostrings)) - -## Convert input string into DNAStringSet object -cat("..Preparing DNAStringSet\n") -dna <- DNAStringSet(args[1]) - -## Disambiguate -cat("..Disambiguating\n") -res <- Disambiguate(dna)[[1]] - -## Assign names -names(res) <- paste0("seq", 1:length(res), sep = "") - -## Export FASTA -cat("..Exporting FASTA\n") -writeXStringSet(x = res, - filepath = args[2], - compress=FALSE, format="fasta", width=9999) - -cat("..done\n") diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/document_s1.R b/src/pipecraft-core/service_scripts/NextITS/bin/document_s1.R deleted file mode 100755 index 755bb888..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/document_s1.R +++ /dev/null @@ -1,337 +0,0 @@ -#!/usr/bin/env Rscript - -## Script to document the Step-1 workflow of the NextITS pipeline. - -## Usage: -## Rscript document_s1.R [output_path] - -## Input: -## - software_versions.yml -## - pipeline_params.tsv - -## Output: -## - README_Step1_Methods.txt -## with two sections: methods and references - - -## Function to load packages -load_pckg <- function(pkg = "data.table"){ - suppressPackageStartupMessages( library(package = pkg, character.only = TRUE) ) - cat(paste(pkg, packageVersion(pkg), "\n")) -} - -load_pckg("glue") -load_pckg("data.table") -load_pckg("yaml") - -## Parse arguments -args <- commandArgs(trailingOnly = TRUE) -if (length(args) < 2) { - cat("Usage: document_s1.R [output_path]\n") - stop() -} - -versions_path <- args[[1]] -params_path <- args[[2]] -output_path <- ifelse(length(args) >= 3, args[[3]], "README_Step1_Methods.txt") - - -## Validation -if(is.null(versions_path) || versions_path == ""){ - stop("Versions YAML not specified") -} -if(is.null(params_path) || params_path == ""){ - stop("Params table not specified") -} - -if(!file.exists(versions_path)){ - stop(glue("Versions YAML not found: {versions_path}")) -} -if(!file.exists(params_path)){ - stop(glue("Params table not found: {params_path}")) -} - - -################################## -################################## Data for debugging -################################## - -# versions_path <- "software_versions.yml" -# params_path <- "pipeline_params.tsv" -# output_path <- "README_Step1_Methods.txt" - - -################################## -################################## References -################################## - -## Citation registry -citation_db <- list( - nextits = "Mikryukov V, Anslan S, Tedersoo L (2025) NextITS - A pipeline for metabarcoding fungi and other eukaryotes with full-length ITS sequenced with PacBio. DOI:10.5281/zenodo.15074882", - nextflow = "Di Tommaso P, et al. (2017) Nextflow enables reproducible computational workflows. Nat Biotechnol 35, 316-319, DOI:10.1038/nbt.3820", - lima = "Pacific Biosciences (2025) LIMA - The PacBio barcode demultiplexer and primer remover. URL: https://lima.how/", - seqkit = "Shen W, Sipos B, Zhao L (2024) SeqKit2: A Swiss Army Knife for Sequence and Alignment Processing. iMeta e191. DOI:10.1002/imt2.191", - csvtk = "Shen W (2025) csvtk - a cross-platform, efficient and practical CSV/TSV toolkit. URL: https://github.com/shenwei356/csvtk", -# brename = "Shen W (2025) brename - batch renaming safely, URL: https://github.com/shenwei356/brename", - cutadapt = "Martin M (2011) Cutadapt removes adapter sequences. EMBnet.journal 17(1):10-12, DOI:10.14806/ej.17.1.200", - itsx = "Bengtsson-Palme J, et al (2013) Improved software detection and extraction of ITS1 and ITS2 from ribosomal ITS sequences of fungi and other eukaryotes for analysis of environmental sequencing data. Methods Ecol Evol 4:914-919, DOI:10.1111/2041-210X.12073", - vsearch = "Rognes T, Flouri T, Nichols B, Quince C, Mahé F (2016) VSEARCH: a versatile open source tool for metagenomics. PeerJ 4:e2584. DOI:10.7717/peerj.2584", - uchime2 = "Edgar RC (2016) UCHIME2: improved chimera prediction for amplicon sequencing. bioRxiv 074252. DOI:10.1101/074252", - uncross2 = "Edgar RC (2018) UNCROSS2: identification of cross-talk in 16S rRNA OTU tables. bioRxiv 400762. DOI:10.1101/400762", - chimscore = "Nilsson RH, et al. (2015) A Comprehensive, Automatically Updated Fungal ITS Sequence Dataset for Reference-Based Chimera Control in Environmental Sequencing Efforts. Microbes Environ. 30(2), 145-50. DOI:10.1264/jsme2.ME14121", - bedtools = "Quinlan AR, Hall IM (2010) BEDTools: a flexible suite of utilities for comparing genomic features. Bioinformatics 26:841-842. DOI:10.1093/bioinformatics/btq033", - duckdb = "Raasveldt M, Mühleisen H (2019) DuckDB: an Embeddable Analytical Database. SIGMOD '19: Proceedings of the 2019 International Conference on Management of Data, 1981-1984. DOI:10.1145/3299869.332021", - parallel = "Tange O (2011) GNU Parallel: The command-line power tool. Usenix Mag 36 (1), 42", - eukaryome = "Tedersoo L, et al. (2024). EUKARYOME: the rRNA gene reference database for identification of all eukaryotes. Database (Oxford) 12:baae043. DOI:10.1093/database/baae043", - R = "R Core Team (2025) R: A Language and Environment for Statistical Computing. R Foundation for Statistical Computing, Vienna, Austria. URL: https://www.R-project.org/", - arrow = "Richardson N, Cook I, Crane N, Dunnington D, François R, Keane J, Moldovan-Grünfeld D, Ooms J, Wujciak-Jens J, and Apache Arrow (2025) arrow: Integration to Apache Arrow. URL: https://github.com/apache/arrow/", - ggplot2 = "Wickham H (2016) ggplot2: Elegant Graphics for Data Analysis. Springer. DOI:10.1007/978-3-319-24277-4", - biostrings= "Pagès H, Aboyoun P, Gentleman R, DebRoy S (2025) Biostrings: Efficient manipulation of biological strings. DOI:10.18129/B9.bioc.Biostrings", - datatable = "Barrett T, Dowle M, Srinivasan A, Gorecki J, Chirico M, Hocking T, Schwendinger B, Krylov I (2025) data.table: Extension of data.frame. URL: " -) - - -################################## -################################## Helpers -################################## - -## Get version number -getv <- function(v, process, tool){ - # v = list (from YAML file) - # process = process name - # tool = tool name - - if(is.null(v[[process]]) || is.null(v[[process]][[tool]])){ return("") } - as.character( v[[process]][[tool]] ) -} -# E.g., getv(versions, "demux", "lima") - - -## Get parameter -getp <- function(p, pname, default = NA){ - # p = table with parameters (two columns: name and value) - # pname = parameter name - # default = default value if parameter is not found - - pp <- p[ name == pname ]$value - if(is.null(pp) || is.na(pp)){ return(default) } - return(pp) -} -# E.g., getp(params, "lima_minscore", 93) - - -## Remove NAs and empty strings (to curate the citations) -trim_na <- function(x){ - x[ !is.na(x) & nzchar(x) ] -} - -################################## -################################## Body builders -################################## - -emit_nextits <- function(v) { - nextits_v <- if(!is.null(v$NextITS$version)){ as.character(v$NextITS$version) } else { "" } - glue("Bioinformatic processing was performed using the \\ - NextITS pipeline v.{nextits_v} (Mikryukov et al., 2025).") -} - -emit_nextflow <- function(v) { - nextflow_v <- if(!is.null(v$Nextflow$version)){ as.character(v$Nextflow$version) } else { "" } - glue("Workflow management was performed using \\ - Nextflow v.{nextflow_v} (Di Tommaso et al., 2017).") -} - -emit_demux_pacbio <- function(p, v) { - ms <- getp(p, "lima_minscore", 93) - mb <- getp(p, "lima_barcodetype", "dual_symmetric") - vs <- getv(v, "demux", "lima") - switch(mb, - "single" = {barcode_type <- "single-end barcodes"}, - "dual_symmetric" = {barcode_type <- "symmetric dual-end barcodes"}, - "dual_asymmetric" = {barcode_type <- "asymmetric dual-end barcodes"}, - "dual" = {barcode_type <- "combination of symmetric and asymmetric dual-end barcodes"}) - - glue("Demultiplexed PacBio reads using LIMA v.{vs} (Pacific Biosciences) with min score {ms} and {barcode_type}.") -} - -emit_qc_pacbio <- function(p, v) { - glue("Quality control was performed using \\ - VSEARCH v.{getv(v,'qc_se','vsearch')} (Rognes et al., 2016) and \\ - seqkit v.{getv(v,'qc_se','seqkit')} (Shen et al., 2024). \\ - Reads with the number of ambiguous bases >= {getp(p,'qc_maxn',4)}, \\ - expected error rate >= {getp(p,'qc_maxeerate',0.01)}, \\ - or homopolymer stretches longer than {getp(p,'qc_maxhomopolymerlen',25)} nt were removed.") -} - -# emit_demux_illumina <- function(p, v) { -# c( -# glue("- Illumina PE QC and merging; demultiplexed merged reads with cutadapt v.{getv(v,'primer_check','cutadapt')} using barcode window {getp(p,'barcode_window',30)}, max errors {getp(p,'barcode_errors',1)}, min overlap {getp(p,'barcode_overlap',11)}."), -# glue("- Non-merged reads optionally retained (join padding '{getp(p,'illumina_joinpadgap','NNNNNNNNNN')}').") -# ) -# } - -emit_primer_check <- function(p, v) { - glue("Primers were trimmed using \\ - cutadapt v.{getv(v, 'primer_check', 'cutadapt')} (Martin, 2011) \\ - with <= {getp(p, 'primer_mismatches', 2)} mismatches. \\ - Reads without both primers were discarded.") -} - -emit_itsx <- function(p, v) { - switch(getp(p,'its_region','full'), - "full" = {its_region <- "full-length ITS"}, - "SSU" = {its_region <- "SSU"}, - "ITS1" = {its_region <- "ITS1"}, - # "5_8S" = {its_region <- "5.8S"}, # not-yet-implemented - "ITS2" = {its_region <- "ITS2"}, - "LSU" = {its_region <- "LSU"}, - "ITS1_5.8S_ITS2" = {its_region <- "near-full-length ITS"}) - - - glue("Extraction of rRNA regions ({its_region}) was performed using \\ - ITSx v.{getv(v,'itsx','ITSx')} (Bengtsson-Palme et al., 2013).") -} - -emit_assemble_its <- function(p, v) { - glue("To assemble near-full-length ITS sequences, we ... (TODO)") -} - -emit_hp_and_chimeras <- function(p, v, did_hp) { - res <- character() - if(isTRUE(did_hp)){ - res <- c(res, glue( - "Homopolymer correction of sequences was performed using an algorithm implemented in NextITS \\ - with support of VSEARCH v.{getv(v,'homopolymer','vsearch')} and seqkit v.{getv(v,'homopolymer','seqkit')}.") ) - } else { - res <- c(res, "Homopolymer correction of sequences was not performed.") - } - res <- c(res, - glue( - "Two-step chimera detection was done using VSEARCH v.{getv(v,'chimera_denovo','vsearch')}: - - de novo using UCHIME2 algorithm (Edgar, 2016) with max score {getp(p,'max_ChimeraScore',0.6)} (Nilsson et al., 2015), - - then reference-based against the EUKARYOME database (Tedersoo et al., 2024).") - ) - res <- paste0(res, collapse = "\n") - return(res) -} - -emit_tj <- function(p, v) { - glue("Tag-jump detection and removal was performed using \\ - UNCROSS2 algorithm (Edgar, 2018) with the parameter f = {getp(p,'tj_f',0.01)}.") -} - -emit_seqtab <- function(p, v) { - glue("Sequence counts table was generated using \\ - R v.{getv(v,'prep_seqtab','R')} (R Core Team, 2025), \\ - data.table v.{getv(v,'prep_seqtab','data.table')} (Barrett et al., 2025), \\ - and Apache Arrow v.{getv(v,'prep_seqtab','arrow')} (Richardson et al., 2025) \\ - packages.") -} - - - -################################## -################################## Workflow-dependent method descriptions -################################## - -## Function to assembly the workflow description and references -build_docs <- function(versions, params){ - body <- character() - tools_used <- character() - - body <- c(body, emit_nextits(versions)) - tools_used <- c(tools_used, "nextits") - - body <- c(body, emit_nextflow(versions)) - tools_used <- c(tools_used, "nextflow") - - demuxed <- tolower(as.character(getp(params, "demultiplexed", FALSE))) %in% c("true", "t", "1") - platform <- getp(params, "seqplatform", "PacBio") - - if(!demuxed){ - if(platform %in% "PacBio"){ - - body <- c(body, emit_demux_pacbio(params, versions)) - tools_used <- c(tools_used, c("lima")) - - body <- c(body, emit_qc_pacbio(params, versions)) - tools_used <- c(tools_used, c("vsearch", "seqkit")) - - } else { - body <- c(body, emit_demux_illumina(params, versions)) - tools_used <- c(tools_used, c("cutadapt")) - } - } else { - if(platform %in% "PacBio"){ - body <- c(body, emit_qc_pacbio(params, versions)) - tools_used <- c(tools_used, c("vsearch", "seqkit")) - } else { - ## TODO - } - } - - ## Primer trimming - body <- c(body, emit_primer_check(params, versions)) - tools_used <- c(tools_used, c("cutadapt")) - - ## ITS extraction - its_region <- getp(params, "its_region", "full") - if(its_region %in% c("full", "ITS1", "ITS2", "SSU", "LSU")){ - body <- c(body, emit_itsx(params, versions)) - tools_used <- c(tools_used, c("itsx", "vsearch", "duckdb", "seqkit", "cutadapt")) - } else if (its_region %in% "ITS1_5.8S_ITS2") { - body <- c(body, emit_itsx(params, versions)) # , emit_assemble_its(params, versions)) - tools_used <- c(tools_used, c("itsx", "vsearch", "duckdb", "seqkit", "cutadapt")) - } - - did_hp <- tolower(as.character(getp(params, "hp", TRUE))) %in% c("true", "t", "1") - body <- c(body, emit_hp_and_chimeras(params, versions, did_hp)) - tools_used <- c(tools_used, c("vsearch", "uchime2", "eukaryome")) - - body <- c(body, emit_tj(params, versions)) - tools_used <- c(tools_used, c("uncross2")) - - body <- c(body, emit_seqtab(params, versions)) - tools_used <- c(tools_used, c("arrow", "datatable", "R")) - - tools_used <- unique(tools_used) - citations <- trim_na( unlist(citation_db[tools_used]) ) - citations <- sort(unique(citations)) - - res <- list( - body = body, - citations = citations) - - return(res) -} - - -################################## -################################## Assemble body and citations -################################## - -## Load inputs -cat("Loading versions YAML...\n") -versions <- yaml::read_yaml(versions_path) - -cat("Loading params table...\n") -params <- data.table::fread(params_path, sep = "\t", header = TRUE, na.strings = c("", "NA")) -setnames(params, new = c("name", "value")) - -## Build body and citations -res <- build_docs(versions, params) - -## Write output -con <- file(output_path, open = "wt") - -writeLines("Methods:", con) -writeLines(res$body, con) - -writeLines("", con) - -writeLines("References:", con) -writeLines(paste0("- ", res$citations), con) - -close(con) - -cat("All done.\n") diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/document_s2.R b/src/pipecraft-core/service_scripts/NextITS/bin/document_s2.R deleted file mode 100755 index 1469ba39..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/document_s2.R +++ /dev/null @@ -1,339 +0,0 @@ -#!/usr/bin/env Rscript - -## Script to document the Step-2 workflow of the NextITS pipeline. - -## Usage: -## Rscript document_s2.R [output_path] - - - -## Function to load packages -load_pckg <- function(pkg = "data.table"){ - suppressPackageStartupMessages( library(package = pkg, character.only = TRUE) ) - cat(paste(pkg, packageVersion(pkg), "\n")) -} - -load_pckg("glue") -load_pckg("data.table") -load_pckg("yaml") - -## Parse arguments -args <- commandArgs(trailingOnly = TRUE) -if (length(args) < 2) { - cat("Usage: document_s2.R [output_path]\n") - stop() -} - -versions_path <- args[[1]] -params_path <- args[[2]] -output_path <- ifelse(length(args) >= 3, args[[3]], "README_Step2_Methods.txt") - - -## Validation -if(is.null(versions_path) || versions_path == ""){ - stop("Versions YAML not specified") -} -if(is.null(params_path) || params_path == ""){ - stop("Params table not specified") -} - -if(!file.exists(versions_path)){ - stop(glue("Versions YAML not found: {versions_path}")) -} -if(!file.exists(params_path)){ - stop(glue("Params table not found: {params_path}")) -} - - - -################################## -################################## References -################################## - -## Citation registry -citation_db <- list( - nextits = "Mikryukov V, Anslan S, Tedersoo L (2025) NextITS - A pipeline for metabarcoding fungi and other eukaryotes with full-length ITS sequenced with PacBio. DOI:10.5281/zenodo.15074882", - nextflow = "Di Tommaso P, et al. (2017) Nextflow enables reproducible computational workflows. Nat Biotechnol 35, 316-319, DOI:10.1038/nbt.3820", - vsearch = "Rognes T, Flouri T, Nichols B, Quince C, Mahé F (2016) VSEARCH: a versatile open source tool for metagenomics. PeerJ 4:e2584. DOI:10.7717/peerj.2584", - dada2 = "Callahan BJ, et al. (2016) DADA2: High-resolution sample inference from Illumina amplicon data. Nat Methods 13:581-583. DOI:10.1038/nmeth.3869", - unoise = "Edgar RC (2016) UNOISE2: improved error-correction for Illumina 16S and ITS amplicon sequencing. bioRxiv 081257. DOI:10.1101/081257", - swarm = "Mahé F, Czech L, Stamatakis A, Quince C, de Vargas C, Dunthorn M, Rognes T. (2021) Swarm v3: towards tera-scale amplicon clustering. Bioinformatics 38(1), 267-269. DOI:10.1093/bioinformatics/btab493", - lulu = "Frøslev TG, et al. (2017) Algorithm for post-clustering curation of DNA amplicon data yields reliable biodiversity estimates. Nat Commun 8:1188. DOI:10.1038/s41467-017-01312-x", - mumu = "Mahé F (2025) MUMU: C++ implementation of LULU, a R package for post-clustering curation of metabarcoding data. URL: https://github.com/frederic-mahe/mumu", - ucs = "Mikryukov V (2025) ucs - USEARCH cluster file parser. URL: https://github.com/vmikk/ucs", - duckdb = "Raasveldt M, Mühleisen H (2019) DuckDB: an Embeddable Analytical Database. SIGMOD '19: Proceedings of the 2019 International Conference on Management of Data, 1981-1984. DOI:10.1145/3299869.332021", - R = "R Core Team (2025) R: A Language and Environment for Statistical Computing. R Foundation for Statistical Computing, Vienna, Austria. URL: https://www.R-project.org/", - arrow = "Richardson N, Cook I, Crane N, Dunnington D, François R, Keane J, Moldovan-Grünfeld D, Ooms J, Wujciak-Jens J, and Apache Arrow (2025) arrow: Integration to Apache Arrow. URL: https://github.com/apache/arrow/", - biostrings= "Pagès H, Aboyoun P, Gentleman R, DebRoy S (2025) Biostrings: Efficient manipulation of biological strings. DOI:10.18129/B9.bioc.Biostrings", - datatable = "Barrett T, Dowle M, Srinivasan A, Gorecki J, Chirico M, Hocking T, Schwendinger B, Krylov I (2025) data.table: Extension of data.frame. URL: " -) - - -################################## -################################## Helpers -################################## - -## Get version number -getv <- function(v, process, tool){ - # v = list (from YAML file) - # process = process name - # tool = tool name - - if(is.null(v[[process]]) || is.null(v[[process]][[tool]])){ return("") } - as.character( v[[process]][[tool]] ) -} -# E.g., getv(versions, "dereplication", "vsearch") - - -## Get parameter -getp <- function(p, pname, default = NA){ - # p = table with parameters (two columns: name and value) - # pname = parameter name - # default = default value if parameter is not found - - pp <- p[ name == pname ]$value - if(is.null(pp) || is.na(pp)){ return(default) } - return(pp) -} -# E.g., getp(params, "otu_id", 0.98) - - -## Remove NAs and empty strings (to curate the citations) -trim_na <- function(x){ - x[ !is.na(x) & nzchar(x) ] -} - -################################## -################################## Body builders -################################## - -emit_nextits <- function(v) { - nextits_v <- if(!is.null(v$NextITS$version)){ as.character(v$NextITS$version) } else { "" } - glue("Bioinformatic processing was performed using the \\ - NextITS pipeline v.{nextits_v} (Mikryukov et al., 2025).") -} - -emit_nextflow <- function(v) { - nextflow_v <- if(!is.null(v$Nextflow$version)){ as.character(v$Nextflow$version) } else { "" } - glue("Workflow management was performed using \\ - Nextflow v.{nextflow_v} (Di Tommaso et al., 2017).") -} - -emit_aggregation <- function(p, v) { - glue("Sequences from all sequencing runs were aggregated and \\ - de novo chimeric sequences with chimera score >= {getp(p,'max_ChimeraScore',0.6)} were removed.") -} - -emit_dereplication <- function(p, v) { - minlen <- getp(p, "ampliconlen_min", NA) - maxlen <- getp(p, "ampliconlen_max", NA) - - length_filter <- "" - if(!is.na(minlen) && !is.na(maxlen)){ - length_filter <- glue(" Sequences shorter than {minlen} nt or longer than {maxlen} nt were excluded.") - } else if(!is.na(minlen)){ - length_filter <- glue(" Sequences shorter than {minlen} nt were excluded.") - } else if(!is.na(maxlen)){ - length_filter <- glue(" Sequences longer than {maxlen} nt were excluded.") - } - - glue("Global sequence dereplication was performed using \\ - VSEARCH v.{getv(v,'dereplication','vsearch')} (Rognes et al., 2016).\\ - {length_filter}") -} - -emit_preclustering <- function(p, v) { - preclustering_method <- getp(p, "preclustering", "none") - - res <- switch(preclustering_method, - - "none" = "", # No pre-clustering or denoising was performed - - "homopolymer" = glue( - "Global homopolymer correction was performed using an algorithm implemented in NextITS \\ - with support of VSEARCH v.{getv(v,'homopolymer','vsearch')} (Rognes et al., 2016)."), - - "unoise" = glue( - "Sequence denoising was performed using the UNOISE3 algorithm (Edgar, 2016) \\ - implemented in VSEARCH v.{getv(v,'unoise','vsearch')} (Rognes et al., 2016) \\ - with alpha parameter {getp(p,'unoise_alpha',6.0)} and minimum size {getp(p,'unoise_minsize',1)}."), - - "dada2" = glue( - "Sequence denoising was performed using \\ - DADA2 v.{getv(v,'dada2','dada2')} (Callahan et al., 2016)"), - # using {getp(p,'dada2_pooling','global')} pooling strategy." - - "swarm_d1" = glue( - "Pre-clustering was performed using \\ - SWARM v.{getv(v,'precluster_swarm','swarm')} (Mahé et al., 2021) \\ - with d=1 and fastidious option enabled.") - ) - - return(res) -} - -emit_clustering <- function(p, v) { - clustering_method <- getp(p, "clustering", "vsearch") - preclustering_method <- getp(p, "preclustering", "none") - - ## Handle special case where SWARM pre-clustering = SWARM clustering with same d - # if(preclustering_method == "swarm_d1" && clustering_method == "swarm" && getp(p, "swarm_d", 1) == 1){ - # return("No additional clustering was performed (SWARM pre-clustering with d=1 was used as final clustering).") - # } - - cls <- switch(clustering_method, - - "vsearch" = glue( - "VSEARCH v.{getv(v,'cluster_vsearch','vsearch')} (Rognes et al., 2016) \\ - with { as.numeric(getp(p,'otu_id',0.98))*100}% similarity threshold."), - - "swarm" = { - fastidious_text <- if(getp(p, "swarm_fastidious", TRUE) && getp(p, "swarm_d", 1) == 1) { - " with fastidious option enabled" - } else { - "" - } - glue("SWARM v.{getv(v,'cluster_swarm','swarm')} (Mahé et al., 2021) \\ - with parameter d={getp(p,'swarm_d',1)}{fastidious_text}.") - }) - - res <- glue("OTU clustering was performed using {cls}") - return(res) -} - -emit_uc_merging <- function(p, v) { - glue("UC files from dereplication, pre-clustering, and clustering steps were merged using \\ - ucs v.{getv(v,'merge_uc','ucs')} (Mikryukov, 2025) and \\ - DuckDB v.{getv(v,'merge_uc','duckdb')} (Raasveldt & Mühleisen, 2019) \\ - to track sequence membership through all processing steps.") -} - -emit_lulu <- function(p, v) { - glue("Post-clustering curation was performed using \\ - LULU algorithm (Frøslev et al., 2017) \\ - as implemented in MUMU v.{getv(v,'lulu','mumu')} (Mahé, 2025) \\ - with {getp(p,'lulu_match',95.0)}% minimum similarity, \\ - {getp(p,'lulu_ratio',1.0)} minimum abundance ratio, \\ - and {getp(p,'lulu_relcooc',0.95)} minimum relative co-occurrence. \\ - Pairwise sequence similarities were calculated using \\ - VSEARCH v.{getv(v,'lulu','vsearch')} (Rognes et al., 2016).") -} - - -################################## -################################## Workflow-dependent method descriptions -################################## - -## Function to assembly the workflow description and references -build_docs <- function(versions, params){ - body <- character() - tools_used <- character() - - ## Pipeline version - body <- c(body, emit_nextits(versions)) - tools_used <- c(tools_used, "nextits") - - ## Nextflow version - body <- c(body, emit_nextflow(versions)) - tools_used <- c(tools_used, "nextflow") - - ## Sequence aggregation - body <- c(body, emit_aggregation(params, versions)) - - ## Sequence dereplication and amplicon length filtering - body <- c(body, emit_dereplication(params, versions)) - tools_used <- c(tools_used, "vsearch") - - ## Conditional: pre-clustering/denoising - preclustering_method <- getp(params, "preclustering", "none") - if(preclustering_method != "none" && !is.na(preclustering_method)){ - body <- c(body, emit_preclustering(params, versions)) - - switch(preclustering_method, - "homopolymer" = { - tools_used <- c(tools_used, "vsearch") - }, - "unoise" = { - tools_used <- c(tools_used, c("vsearch", "unoise")) - }, - "dada2" = { - tools_used <- c(tools_used, "dada2") - }, - "swarm_d1" = { - tools_used <- c(tools_used, "swarm") - } - ) - } - - ## Conditional: clustering - clustering_method <- getp(params, "clustering", "vsearch") - if(clustering_method != "none" && !is.na(clustering_method)){ - ## Skip clustering description if it's redundant with pre-clustering - if(!(preclustering_method == "swarm_d1" && clustering_method == "swarm" && getp(params, "swarm_d", 1) == 1)){ - body <- c(body, emit_clustering(params, versions)) - - switch(clustering_method, - "vsearch" = { - tools_used <- c(tools_used, "vsearch") - }, - "swarm" = { - tools_used <- c(tools_used, "swarm") - } - ) - } - } - - ## UC file merging - body <- c(body, emit_uc_merging(params, versions)) - tools_used <- c(tools_used, c("ucs", "duckdb")) - - ## Conditional: LULU post-clustering curation - lulu_enabled <- tolower(as.character(getp(params, "lulu", "true"))) %in% c("true", "t", "1") - if(lulu_enabled){ - body <- c(body, emit_lulu(params, versions)) - tools_used <- c(tools_used, c("mumu", "lulu", "vsearch")) - } - - ## Generate citations - tools_used <- unique(tools_used) - citations <- trim_na( unlist(citation_db[tools_used]) ) - citations <- sort(unique(citations)) - - res <- list( - body = body, - citations = citations) - - return(res) -} - - -################################## -################################## Assemble body and citations -################################## - -## Load inputs -cat("Loading versions YAML...\n") -versions <- yaml::read_yaml(versions_path) - -cat("Loading params table...\n") -params <- data.table::fread(params_path, sep = "\t", header = TRUE, na.strings = c("", "NA")) -setnames(params, new = c("name", "value")) - -## Build body and citations -res <- build_docs(versions, params) - -## Write output -con <- file(output_path, open = "wt") - -writeLines("Methods:", con) -writeLines(res$body, con) - -writeLines("", con) - -writeLines("References:", con) -writeLines(paste0("- ", res$citations), con) - -close(con) - -cat("All done.\n") - diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/hash_sequences.sh b/src/pipecraft-core/service_scripts/NextITS/bin/hash_sequences.sh deleted file mode 100755 index d36f6ba0..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/hash_sequences.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -awk \ - '{ print $0 "\t" encodeData( $2 ) } - function encodeData( fld, cmd, output ) { - cmd = "printf \047" fld "\047 | sha1sum" - if ( (cmd | getline output) > 0 ) { - sub(/ .*/,"",output) - } - else { - print "failed to hash " fld | "cat>&2" - output = fld - } - close( cmd ) - return output - }' \ - "$1" diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/homopolymer_collapse_len.sh b/src/pipecraft-core/service_scripts/NextITS/bin/homopolymer_collapse_len.sh deleted file mode 100755 index c7575431..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/homopolymer_collapse_len.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash - -## Script to compress homopolymer stretches (e.g., prior to k-mer counting) -## All homopolymer stretches will be collapsed to max H length (default, 1) - -## Input: -# $1 = Parameter H (max homopolymer length) -# $2 = input FASTA file - -## Usage example: -# -# cat > input.fasta <<'EOT' -# >s1 -# AACCCTTTTGGGGG -# >s2 -# ACCTTTGGGGGGGG -# >s3 -# ACTGACTGACTGAC -# EOT -# -# homopolymer_compression.sh 2 input.fasta - - - -awk -v H="$1" '\ - -BEGIN { - if (H < 1) H = 1; -} - -# If the line is a header, print it as is -/^>/ { - print; - next; -} - -# Process sequence lines -{ - sequence = $0; - collapsedSeq = ""; - count = 1; - - for (i = 2; i <= length(sequence); i++) { - if (substr(sequence, i, 1) == substr(sequence, i - 1, 1)) { - count++; - } else { - collapsedSeq = collapsedSeq substr(sequence, i - count, (count > H) ? H : count); - count = 1; - } - } - - # Handle the last homopolymer stretch - collapsedSeq = collapsedSeq substr(sequence, length(sequence) - count + 1, (count > H) ? H : count); - - print collapsedSeq; -} -' "$2" - diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/homopolymer_compression.sh b/src/pipecraft-core/service_scripts/NextITS/bin/homopolymer_compression.sh deleted file mode 100755 index 03e66d5e..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/homopolymer_compression.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -## Script to collapse homopolymer stretches to a single letter - -bioawk \ - '{ gsub(/[A]+/,"A");gsub(/[C]+/,"C");gsub(/[T]+/,"T");gsub(/[G]+/,"G");gsub(/[N]+/,"N") }1' \ - "$1" diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/max_ee.R b/src/pipecraft-core/service_scripts/NextITS/bin/max_ee.R deleted file mode 100755 index 3031d40d..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/max_ee.R +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env Rscript - -## Script to combine Phred scores and MaxEE estimates - -# Input is given as positional arguments: -# 1. Phred score table (`tmp_hash_table.txt`) -# 2. MaxEE table (`tmp_ee.txt`) -# 3. Output file name (`${sampID}_hash_table.txt`) - - -args <- commandArgs(trailingOnly = TRUE) - -## Debug: -# args <- c( -# "tmp_hash_table.txt", -# "tmp_ee.txt", -# "res_hash_table.txt" -# ) - -suppressMessages(library(data.table)) - - -## Load table with Phred scores -cat("..Loading Phred scores\n") -T1 <- fread( - file = args[1], - sep = "\t", header = FALSE, - col.names = c("SeqID", "SeqHash", "Len", "PhredScore"), - colClasses = c("character", "character", "numeric", "numeric")) - -if(any(is.na(T1$Len))){ - cat("WARNING: non-numeric data detected. Maybe there are some empty sequences\n") -} - -## Load table with Phred scores -cat("..Loading MaxEE estimates\n") -T2 <- fread( - file = args[2], - sep = "\t", header = FALSE, - col.names = c("SeqID", "MaxEE")) - -## Merge tables -cat("..Merging tables\n") -TAB <- merge(x = T1, y = T2, by = "SeqID", all.x = TRUE) - -## Estimate the MEEP score (Koparde et al., DOI:10.1504/IJCBDD.2017.10006006) -## Maximum number of probable incorrect base calls per every 100 bases in the read -cat("..Estimating MEEP score\n") -TAB[ , MEEP := 100 * MaxEE / Len ] - -## Export results -cat("..Exporting results\n") -fwrite(x = TAB, - file = args[3], - sep = "\t", - compress = "none") - diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/merge_hash_tables.sh b/src/pipecraft-core/service_scripts/NextITS/bin/merge_hash_tables.sh deleted file mode 100755 index ec5313f4..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/merge_hash_tables.sh +++ /dev/null @@ -1,131 +0,0 @@ -#!/bin/bash - -## Usage: -# merge_hash_tables.sh \ -# -i '/path/to/input/directory' \ -# -o '/path/to/output.parquet' \ -# -t 4 - -## Input data: -# - Tab-delimited tables with columns: -# SampleID - Hash - PacBioID - AvgPhredScore - MaxEE - MEEP - Sequence - Quality - Length - -## Notes -# - memory constraints might reduce the number of threads used -# - when saving to parquet, the ROW_GROUP_SIZE param might be adjusted to reduce memory usage (but the effect is not very significant): -# default ROW_GROUP_SIZE = 122,880 (with DuckDB's vector size = 2,048 -> 60 row groups) -# here, a half of the default value is used (ROW_GROUP_SIZE = 61,440 -> 30 row groups) - -## Function to display usage information -usage() { - echo "Usage: $0 -i INPUTDIR -o OUTPUT [-t THREADS] [-m MEMORY] [-x TEMP_DIR] [-z COMPRESSION]" - echo " -i INPUTDIR : Input directory with text files" - echo " -o OUTPUT : Output Parquet file path" - echo " -t THREADS : Number of CPU threads to use (optional)" - echo " -m MEMORY : Memory limit (e.g., '100GB') (optional)" - echo " -z COMPRESSION : ZSTD compression level (0-22) (optional, default: 12)" - exit 1 -} - -## Initialize variables -INPUT="" -OUTPUT="" -THREADS="" -MEMORY="" -COMPRESSION="12" - -## Parse command-line options -while getopts "i:o:t:m:z:" opt; do - case $opt in - i) INPUT="$OPTARG" ;; - o) OUTPUT="$OPTARG" ;; - t) THREADS="$OPTARG" ;; - m) MEMORY="$OPTARG" ;; - z) COMPRESSION="$OPTARG" ;; - *) usage ;; - esac -done - - -## Validate input parameters -if [[ -z "$INPUT" || -z "$OUTPUT" ]]; then - echo -e "Error: Missing required parameters!\n" - usage -fi - -## Threads should be a positive integer -if [[ -n "$THREADS" && "$THREADS" -le 0 ]]; then - echo -e "Error: Threads must be a positive integer!\n" - usage -fi - -## Validate compression level -if ! [[ "$COMPRESSION" =~ ^[0-9]+$ ]] || [ "$COMPRESSION" -lt 0 ] || [ "$COMPRESSION" -gt 22 ]; then - echo -e "Error: Compression level must be an integer between 0 and 22!\n" - usage -fi - -## View user-supplied parameters -echo -e "\nInput parameters:" -echo "Input directory: $INPUT" -echo "Output file: $OUTPUT" -if [[ -n "$THREADS" ]]; then - echo "Threads: $THREADS" -fi -if [[ -n "$MEMORY" ]]; then - echo "Memory: $MEMORY" -fi -echo "Parquet compression level (ZSTD): $COMPRESSION" - - -SQL_COMMAND="" - -## Add configuration settings (if provided) -if [[ -n "$THREADS" ]]; then - SQL_COMMAND+=" -SET threads TO ${THREADS}; -" -fi - -if [[ -n "$MEMORY" ]]; then - SQL_COMMAND+=" -SET memory_limit = '${MEMORY}'; -" -fi - -SQL_COMMAND+=" -COPY ( - SELECT - column0 as SampleID, - column1 as Hash, - column2 as PacBioID, - column3 as AvgPhredScore, - column4 as MaxEE, - column5 as MEEP, - column6 as Sequence, - column7 as Quality, - column8 as Length - FROM read_csv('${INPUT}/*.txt.gz', - header = false, - delim = '\t', - quote = '', - columns = { - 'column0': 'VARCHAR', - 'column1': 'VARCHAR', - 'column2': 'VARCHAR', - 'column3': 'DOUBLE', - 'column4': 'DOUBLE', - 'column5': 'DOUBLE', - 'column6': 'VARCHAR', - 'column7': 'VARCHAR', - 'column8': 'INTEGER' - } - ) -) TO '${OUTPUT}' (FORMAT PARQUET, ROW_GROUP_SIZE 61_440, COMPRESSION 'ZSTD', COMPRESSION_LEVEL ${COMPRESSION}); -" - -## Execute the SQL command -echo -e "\nExecuting DuckDB command" - -duckdb -c "${SQL_COMMAND}" - diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/merge_sequnce_qualities.sh b/src/pipecraft-core/service_scripts/NextITS/bin/merge_sequnce_qualities.sh deleted file mode 100755 index c30735ce..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/merge_sequnce_qualities.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -# $1 = input file -# $2 = text to add to the resulting file - -zcat "$1" \ - | awk \ - -F '\t' -v OFS='\t' \ - -v fnm="$2" \ - 'NR>1 { print fnm , $2 , $3 , $4 , $5 , $6 }' \ - | sed 's/_hash_table.txt//' \ - | sed '1i SampleID\tSeqID\tSeqLen\tPhredScore\tMaxEE\tMEEP' diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/merge_tj_memberships.sh b/src/pipecraft-core/service_scripts/NextITS/bin/merge_tj_memberships.sh deleted file mode 100755 index 683b834c..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/merge_tj_memberships.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/bin/bash - -## Usage: -# merge_tj_memberships.sh \ -# -d 'Dereplicated.parquet' \ -# -c 'Clustered.parquet' \ -# -o 'TJPreclust.uc.parquet' \ -# -t 4 - -## Input data: -# - Parsed UC file from dereplication (`Dereplicated.parquet`) -# - Parsed UC file from clustering (`Clustered.parquet`) - -## Function to display usage information -usage() { - echo "Usage: $0 -d DEREP -c CLUST -o OUTPUT [-t THREADS]" - echo " -d DEREP : Parquet file from dereplication" - echo " -c CLUST : Parquet file from clustering" - echo " -o OUTPUT : Output Parquet file path" - echo " -t THREADS : Number of CPU threads to use (optional)" - exit 1 -} - -## Initialize variables -DEREP="" -CLUST="" -OUTPUT="TJPreclust.uc.parquet" # default output file name -THREADS="" - -## Parse command-line options -while getopts "d:c:o:t:" opt; do - case $opt in - d) DEREP="$OPTARG" ;; - c) CLUST="$OPTARG" ;; - o) OUTPUT="$OPTARG" ;; - t) THREADS="$OPTARG" ;; - *) usage ;; - esac -done - - -## Validate input parameters -if [[ -z "$DEREP" || -z "$CLUST" ]]; then - echo -e "Error: Missing required parameters!\n" - usage -fi - -## Threads should be a positive integer -if [[ -n "$THREADS" && "$THREADS" -le 0 ]]; then - echo -e "Error: Threads must be a positive integer!\n" - usage -fi - -## View user-supplied parameters -echo -e "\nInput parameters:" -echo "Parquet file from dereplication: $DEREP" -echo "Parquet file from clustering: $CLUST" -echo "Output file: $OUTPUT" -if [[ -n "$THREADS" ]]; then - echo "Threads: $THREADS" -fi - -SQL_COMMAND="" - -## Add configuration settings (if provided) -if [[ -n "$THREADS" ]]; then - SQL_COMMAND+=" -SET threads TO ${THREADS}; -" -fi - - -SQL_COMMAND+=" -COPY ( - SELECT - d.query AS SeqID, - c.target AS OTU - FROM read_parquet('${DEREP}') AS d - LEFT JOIN read_parquet('${CLUST}') AS c - ON d.target = c.query -) TO '${OUTPUT}' (FORMAT PARQUET, COMPRESSION 'ZSTD', COMPRESSION_LEVEL 8); -" - -## Execute the SQL command -echo -e "\nExecuting DuckDB command" - -duckdb -c "${SQL_COMMAND}" diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/merge_uc_files.R b/src/pipecraft-core/service_scripts/NextITS/bin/merge_uc_files.R deleted file mode 100755 index 94be7771..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/merge_uc_files.R +++ /dev/null @@ -1,264 +0,0 @@ -#!/usr/bin/env Rscript - -## Merge UC files from different steps (dereplication, pre-clustering, clustering) into a single file - -cat("Joining parquet files\n\n") - -## Check time -start_time <- Sys.time() - -## Function to load packages -load_pckg <- function(pkg = "data.table"){ - suppressPackageStartupMessages( library(package = pkg, character.only = TRUE) ) - cat(".. ", paste(pkg, packageVersion(pkg), "\n")) -} - -cat("Loading packages:\n") -load_pckg("DBI") -load_pckg("duckdb") -# load_pckg("qs") -# load_pckg("data.table") -# load_pckg("arrow") -# load_pckg("dplyr") - -cat("\nParsing input options and arguments...\n") - -suppressPackageStartupMessages(require(optparse)) - -## Parse arguments -option_list <- list( - make_option("--ucderep", action="store", default=NA, type='character', help="UC file from global dereplication"), - make_option("--ucpreclust", action="store", default=NA, type='character', help="UC file from pre-clustering (optional)"), - make_option("--ucclust", action="store", default=NA, type='character', help="UC file from clustering"), - make_option("--output", action="store", default="UC_Pooled.parquet", type='character', help="Output file name"), - make_option(c("-t", "--threads"), action="store", default=4L, type='integer', help="Number of CPU threads for arrow, default 4") -) -opt <- parse_args(OptionParser(option_list=option_list)) - -## Function to convert text "NA"s to NA -to_na <- function(x){ - if(x %in% c("NA", "null", "Null")){ x <- NA } - return(x) -} - -## Replaces "null"s from Nextflow with NA -opt <- lapply(X = opt, FUN = to_na) - - -## Validation of the required argiments -if(is.na(opt$ucderep)){ - cat("Input file is not specified: UC file from global dereplication.\n", file=stderr()) - stop() -} -if(is.na(opt$ucclust)){ - cat("Input file is not specified: UC file from clustering.\n", file=stderr()) - stop() -} - - -## Assign variables -UCDEREP <- opt$ucderep -UCPRECLUST <- opt$ucpreclust -UCCLUST <- opt$ucclust -OUTPUT <- opt$output -CPUTHREADS <- as.numeric( opt$threads ) - -## Log assigned variables -cat("\nParameters specified:\n") -cat(paste(" UC file from global dereplication: ", UCDEREP, "\n", sep="")) -cat(paste(" UC file from pre-clustering or denoising: ", UCPRECLUST, "\n", sep="")) -cat(paste(" UC file from clustering: ", UCCLUST, "\n", sep="")) -cat(paste(" Output file name: ", OUTPUT, "\n", sep="")) -cat(paste(" Number of CPU threads to use: ", CPUTHREADS, "\n", sep="")) - -cat("\n") - -## Data for debugging -# UCDEREP <- "UC_derep.parquet" -# UCPRECLUST <- "UC_preclust.parquet" # "NoPrecluster" -# UCCLUST <- "UC_clust.parquet" -# OUTPUT <- "UC_Pooled.parquet" -# CPUTHREADS <- 4 - -if(UCPRECLUST == "NoPrecluster") { - UCPRECLUST <- NA -} - - -###################################### -###################################### Load and process the data [duckdb] -###################################### - -## Initialize DuckDB connection -cat("..Initializing DuckDB connection\n") -con <- DBI::dbConnect(duckdb::duckdb()) -# duckdb::duckdb(dbdir = tempfile(pattern = "nextits.duckdb.") - -## Register parquet files as tables -cat("..Registering parquet files as tables\n") -cat("...Dereplication UC\n") -dbExecute(con, sprintf("CREATE VIEW derep_seqs AS SELECT * FROM parquet_scan('%s')", UCDEREP)) - -if(!is.na(UCPRECLUST)) { - cat("...Pre-clustering UC\n") - dbExecute(con, sprintf("CREATE VIEW preclust_seqs AS SELECT * FROM parquet_scan('%s')", UCPRECLUST)) -} - -cat("...Clustering UC\n") -dbExecute(con, sprintf("CREATE VIEW clust_seqs AS SELECT * FROM parquet_scan('%s')", UCCLUST)) - -## Set number of threads -cat("..Setting number of threads for DuckDB\n") -dbExecute(con, sprintf("SET threads TO %d;", CPUTHREADS)) - - -## Process and merge the data -if(is.na(UCPRECLUST)) { - - ## Two-file merge (no pre-clustering) - cat("..Merging UC files [no pre-clustering or denoising]\n") - dbExecute(con, sprintf(" - COPY ( - WITH derep AS ( - SELECT DISTINCT - query as SeqID, - target as DerepID - FROM derep_seqs - QUALIFY ROW_NUMBER() OVER (PARTITION BY query ORDER BY target) = 1 - ), - clust AS ( - SELECT DISTINCT - query as DerepID, - target as OTU - FROM clust_seqs - QUALIFY ROW_NUMBER() OVER (PARTITION BY query ORDER BY target) = 1 - ) - SELECT - d.SeqID, - d.DerepID, - c.OTU - FROM derep d - LEFT JOIN clust c ON d.DerepID = c.DerepID - ) TO '%s' - (FORMAT 'parquet', COMPRESSION 'ZSTD', COMPRESSION_LEVEL 8)", - OUTPUT)) - -} else { - - ## Three-file merge (with pre-clustering) - cat("..Merging UC files [with pre-clustering or denoising]\n") - - dbExecute(con, sprintf(" - COPY ( - WITH derep AS ( - SELECT DISTINCT - query as SeqID, - target as DerepID - FROM derep_seqs - QUALIFY ROW_NUMBER() OVER (PARTITION BY query ORDER BY target) = 1 - ), - preclust AS ( - SELECT DISTINCT - query as DerepID, - target as PreclusterID - FROM preclust_seqs - QUALIFY ROW_NUMBER() OVER (PARTITION BY query ORDER BY target) = 1 - ), - clust AS ( - SELECT DISTINCT - query as PreclusterID, - target as OTU - FROM clust_seqs - QUALIFY ROW_NUMBER() OVER (PARTITION BY query ORDER BY target) = 1 - ) - SELECT - d.SeqID, - d.DerepID, - p.PreclusterID, - c.OTU - FROM derep d - LEFT JOIN preclust p ON d.DerepID = p.DerepID - LEFT JOIN clust c ON p.PreclusterID = c.PreclusterID - ) TO '%s' - (FORMAT 'parquet', COMPRESSION 'ZSTD', COMPRESSION_LEVEL 8)", - OUTPUT)) - -} - -## Clean up -cat("..Disconnecting from DuckDB\n") -dbDisconnect(con, shutdown = TRUE) - - -cat("..Done!\n") - - - -###################################### -###################################### Load and process the data [arrow + dplyr + data.table] -###################################### - -# ## Set number of threads for data.table -# cat("..Setting number of threads\n") -# setDTthreads(threads = CPUTHREADS) # for data.table -# set_cpu_count(CPUTHREADS) # for arrow -# -# ## Globally dereplicated sequences (remove multi-target matches) -# cat("..Loading globally dereplicated sequences\n") -# UCA <- open_dataset(UCDEREP) %>% -# rename(SeqID = query, DerepID = target) %>% -# to_duckdb() %>% -# distinct(SeqID, .keep_all = TRUE) %>% -# collect() %>% -# setDT() -# -# ## Pre-clustered sequences (remove multi-target matches) -# if(!is.na(UCPRECLUST)){ -# cat("..Loading pre-clustered sequences\n") -# UCP <- open_dataset(UCPRECLUST) %>% -# rename(DerepID = query, PreclusterID = target) %>% -# to_duckdb() %>% -# distinct(DerepID, .keep_all = TRUE) %>% -# collect() %>% -# setDT() -# } -# -# ## Clustered sequences (remove multi-target matches) -# cat("..Loading clustering UC file\n") -# if(!is.na(UCPRECLUST)){ -# UCO <- open_dataset(UCCLUST) %>% -# rename(PreclusterID = query, OTU = target) %>% -# to_duckdb() %>% -# distinct(PreclusterID, .keep_all = TRUE) %>% -# collect() %>% -# setDT() -# } else { -# UCO <- open_dataset(UCCLUST) %>% -# rename(DerepID = query, OTU = target) %>% -# to_duckdb() %>% -# distinct(DerepID, .keep_all = TRUE) %>% -# collect() %>% -# setDT() -# } -# -# ## Merge UC files -# if(is.na(UCPRECLUST)){ -# -# ## No pre-clustering or denoising -# cat("..Merging UC files [no pre-clustering or denoising ]\n") -# RES <- merge(x = UCA, y = UCO, by = "DerepID", all.x = TRUE) -# -# } else { -# -# ## Merge UC files with pre-clustering or denoising -# cat("..Merging UC files [with pre-clustering or denoising ]\n") -# -# cat("... Adding pre-cluster or denoised IDs\n") -# RES <- merge(x = UCA, y = UCP, by = "DerepID", all.x = TRUE) -# -# cat("... Adding clustering IDs\n") -# RES <- merge(x = RES, y = UCO, by = "PreclusterID", all.x = TRUE) -# -# } -# - diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/pool_seqs_clean_header.sh b/src/pipecraft-core/service_scripts/NextITS/bin/pool_seqs_clean_header.sh deleted file mode 100755 index 773efa4b..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/pool_seqs_clean_header.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -# $1 = input file (full path) -# $2 = sample ID (e.g., basename of the input file) - -zcat "${1}" \ - | sed -r '/^>/ s/;sample=[^;]*/;/g ; s/;;/;/g' \ - | sed "s/>.*/&;sample=${2}; / ; s/_NoChimera.fa//g ; s/_RescuedChimera.fa//g ; s/_JoinedPE//g ; s/Rescued_Chimeric_sequences.part_//g" \ - | sed -r '/^>/ s/;;/;/g' - - diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/quick_stats.R b/src/pipecraft-core/service_scripts/NextITS/bin/quick_stats.R deleted file mode 100755 index a132c364..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/quick_stats.R +++ /dev/null @@ -1,241 +0,0 @@ -#!/usr/bin/env Rscript - -## Summarise number of reads (demultiplexed and primer-checked) - -# quick_stats.R \ -# --raw Counts_1.RawData.txt \ -# --qc Counts_2.QC.txt \ -# --demuxed Counts_3.Demux.txt \ -# --primer Counts_4.PrimerCheck.txt \ -# --primerartef Counts_4.PrimerArtefacts.txt \ -# --threads 4 - - - -############################################## Parse input parameters - -## Check time -start_time <- Sys.time() - - -cat("Parsing input options and arguments...\n") - -suppressPackageStartupMessages(require(optparse)) - -## Parse arguments -option_list <- list( - - make_option("--raw", action="store", default=NA, type='character', help="Raw read counts"), - make_option("--qc", action="store", default=NA, type='character', help="Counts of reads passed QC"), - make_option("--demuxed", action="store", default=NA, type='character', help="Counts of demultiplexed reads"), - make_option("--primer", action="store", default=NA, type='character', help="Counts of reads with both primers detected"), - make_option("--primerartef",action="store", default=NA, type='character', help="Counts of primer artefacts"), - make_option(c("-t", "--threads"), action="store", default=4L, type='integer', help="Number of CPU threads for arrow, default 4") -) -opt <- parse_args(OptionParser(option_list=option_list)) - - -## Validation of the required argiments -# if(is.na(opt$raw)){ -# cat("Input file is not specified: ....\n", file=stderr()) -# stop() -# } - - - -## Function to convert text "NA"s to NA -to_na <- function(x){ - if(x %in% c("NA", "null", "Null")){ x <- NA } - return(x) -} - -## Assign variables -RAW <- opt$raw -QC <- opt$qc -DEMUXED <- opt$demuxed -PRIMER <- opt$primer -PRIMERARTEF <- opt$primerartef -CPUTHREADS <- as.numeric( opt$threads ) - -## Log assigned variables -cat(paste("Counts - RawData: " , RAW, "\n", sep="")) -cat(paste("Counts - QC: " , QC, "\n", sep="")) -cat(paste("Counts - Demux: " , DEMUXED, "\n", sep="")) -cat(paste("Counts - PrimerCheck: " , PRIMER, "\n", sep="")) -cat(paste("Counts - Primer Artefacts: " , PRIMERARTEF, "\n", sep="")) -cat(paste("Number of CPU threads to use: ", CPUTHREADS, "\n", sep="")) - -cat("\n") - - -############################################## data for debuging - -# RAW <- "Counts_1.RawData.txt" -# QC <- "Counts_2.QC.txt" -# DEMUXED <- "Counts_3.Demux.txt" -# PRIMER <- "Counts_4.PrimerCheck.txt" -# PRIMERARTEF <- "Counts_4.PrimerArtefacts.txt" -# CPUTHREADS <- 6 - - -############################################## Load packages and data - -cat("Loading R packages...\n") - -load_pckg <- function(pkg = "data.table"){ - suppressPackageStartupMessages( library(package = pkg, character.only = TRUE) ) - cat(paste(pkg, packageVersion(pkg), "\n")) -} - -load_pckg("data.table") -load_pckg("plyr") -load_pckg("metagMisc") -load_pckg("openxlsx") - -cat("\n") - -## Set CPU thread number -cat("Setting number of CPU threads to: ", CPUTHREADS, "\n") -setDTthreads(threads = CPUTHREADS) # for data.table - - -###################################### -###################################### Load the data -###################################### - -cat("\nLoading input data\n") - - -#### Per-dataset stats - -## Load ASV table -cat("..Loading raw counts\n") -RAW <- fread(RAW) - -cat("..Loading QC counts\n") -QC <- fread(QC) - -#### Per-sample stats - -SEQKITCOUNTS <- list() - -cat("..Loading demux counts\n") -SEQKITCOUNTS$DEMUXED <- fread(DEMUXED) - -cat("..Loading primer-checked data counts\n") -SEQKITCOUNTS$PRIMER <- fread(PRIMER) -SEQKITCOUNTS$PRIMERARTEF <- fread(PRIMERARTEF) - - -## Remove NULL-files -null_seqk <- laply(.data = SEQKITCOUNTS, .fun = nrow) - -if(any(null_seqk == 0)){ - cat("Some files with counts are missing:\n") - to_rm <- which(null_seqk == 0) - cat(".. ", paste(names(SEQKITCOUNTS)[ to_rm ], collapse = ", "), "\n") - SEQKITCOUNTS[ to_rm ] <- NULL - rm(to_rm) -} - - -## Process seqkit counts -seqkit_process <- function(x){ - if(nrow(x) > 0){ - - ## Remove reudndant columns - x <- x[ , .(file, num_seqs) ] - - ## Remove file extensions - x[ , file := sub(pattern = ".fastq.gz", replacement = "", x = file) ] - x[ , file := sub(pattern = ".fq.gz", replacement = "", x = file) ] - x[ , file := sub(pattern = ".fa.gz", replacement = "", x = file) ] - x[ , file := sub(pattern = "_PrimerChecked$", replacement = "", x = file) ] - x[ , file := sub(pattern = "_PrimerArtefacts$",replacement = "", x = file) ] - - } - return(x) -} - - -cat("Processing data\n") -SEQKITCOUNTS <- llply(.data = SEQKITCOUNTS, .fun = seqkit_process) - -## Rename columns -if(!is.null(SEQKITCOUNTS$DEMUXED)){ -setnames(x = SEQKITCOUNTS$DEMUXED, old = "num_seqs", new = "Demultiplexed_Reads", skip_absent = TRUE) -} -if(!is.null(SEQKITCOUNTS$PRIMER)){ -setnames(x = SEQKITCOUNTS$PRIMER, old = "num_seqs", new = "PrimerChecked_Reads", skip_absent = TRUE) -} -if(!is.null(SEQKITCOUNTS$PRIMERARTEF)){ -setnames(x = SEQKITCOUNTS$PRIMERARTEF, old = "num_seqs", new = "PrimerArtefacts_Reads", skip_absent = TRUE) -} - -## Merge seqkit and custom counts into a single list -cat("Pooling per-sample counts\n") -COUNTS <- SEQKITCOUNTS - -## Pool per-file estimates -merge_dt <- function(x,y){ merge(x, y, by = "file", all = TRUE) } -PER_SAMPLE_COUNTS_merged <- Reduce(f = merge_dt, x = COUNTS) - -## Estimate percentage of multiprimer artefacts -PER_SAMPLE_COUNTS_merged[ , - PrimerArtefacts_Percent := round( - PrimerArtefacts_Reads / (PrimerChecked_Reads + PrimerArtefacts_Reads) * 100, - 2) - ] - - -### ... update -# .. replace NAs with zero -# .. reorder columns -# .. estimate percentages -# .. add tag-jump summary -# .. add final counts from the Seq table -# .. add positive / negative counts (based on default sample names) - -## Prepare per-run stats -PER_RUN_COUNTS_merged <- data.table( - Total_Number_Of_Reads = RAW$num_seqs, - Reads_Passed_QC = QC$num_seqs, - Reads_Demultiplexed = sum(PER_SAMPLE_COUNTS_merged$Demultiplexed_Reads, na.rm = TRUE), - Reads_PrimerChecked = sum(PER_SAMPLE_COUNTS_merged$PrimerChecked_Reads, na.rm = TRUE) - ) - -## Estimate percentages -cat("..Estimating per-run percentages\n") -PER_RUN_COUNTS_merged[ , Percentage_QC := - round(Reads_Passed_QC / Total_Number_Of_Reads * 100, 1) ] - -PER_RUN_COUNTS_merged[ , Percentage_Demultiplexed := - round(Reads_Demultiplexed / Total_Number_Of_Reads * 100, 1) ] - -PER_RUN_COUNTS_merged[ , Percentage_Passed := - round(Reads_PrimerChecked / Total_Number_Of_Reads * 100, 1) ] - - -## Export summary stats -write.xlsx(list( - "per_sample" = PER_SAMPLE_COUNTS_merged, - "per_run" = PER_RUN_COUNTS_merged - ), - file = "Run_summary.xlsx", colNames = TRUE) - - -cat("\nAll done.\n") - - -##################### Session info - -## Check time -end_time <- Sys.time() - -tmm <- as.numeric(difftime(end_time, start_time, units = "min")) -cat("\nElapsed time: ", tmm, " minutes\n") - -cat("\n") -cat("Session info:\n") -sessionInfo() -cat("\n") diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/rc.sh b/src/pipecraft-core/service_scripts/NextITS/bin/rc.sh deleted file mode 100755 index 77f6ee69..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/rc.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -## Function to reverse-complement DNA sequences (with the support of IUPAC codes) - -echo "$1" \ - | tr \ - "[ATGCUatgcuNnYyRrSsWwKkMmBbDdHhVv]" \ - "[TACGAtacgaNnRrYySsWwMmKkVvHhDdBb]" \ - | rev diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/read_count_summary.R b/src/pipecraft-core/service_scripts/NextITS/bin/read_count_summary.R deleted file mode 100755 index ea399aa1..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/read_count_summary.R +++ /dev/null @@ -1,500 +0,0 @@ -#!/usr/bin/env Rscript - -## Summarise number of reads per process - -# read_count_summary.R \ -# --raw Counts_1.RawData.txt \ -# --qc Counts_2.QC.txt \ -# --demuxed Counts_3.Demux.txt \ -# --primer Counts_4.PrimerCheck.txt \ -# --primerartef Counts_4.PrimerArtefacts.txt \ -# --itsx Counts_5.ITSx_or_PrimTrim.txt \ -# --homopolymer Counts_5.Homopolymers.txt \ -# --chimrefn Counts_6.ChimRef_reads.txt \ -# --chimrefu Counts_6.ChimRef_uniqs.txt \ -# --chimdenovo Counts_7.ChimDenov.txt \ -# --chimrecovn Counts_8.ChimRecov_reads.txt \ -# --chimrecovu Counts_8.ChimRecov_uniqs.txt \ -# --tj TagJump_scores.qs \ -# --seqtab Seqs.parquet \ -# --maxchim 0.6 \ -# --threads 4 - - -############################################## Parse input parameters - -## Check time -start_time <- Sys.time() - - -cat("\nParsing input options and arguments...\n") - -suppressPackageStartupMessages(require(optparse)) - -## Parse arguments -option_list <- list( - - make_option("--raw", action="store", default=NA, type='character', help="Raw read counts"), - make_option("--qc", action="store", default=NA, type='character', help="Counts of reads passed QC"), - make_option("--demuxed", action="store", default=NA, type='character', help="Counts of demultiplexed reads"), - make_option("--primer", action="store", default=NA, type='character', help="Counts of reads with both primers detected"), - make_option("--primerartef",action="store", default=NA, type='character', help="Counts of primer artefacts"), - make_option("--itsx", action="store", default=NA, type='character', help="Read counts after ITSx or primer removal"), - make_option("--homopolymer",action="store", default=NA, type='character', help="Homopolymer correction results"), - make_option("--chimrefn", action="store", default=NA, type='character', help="Number of reads for reference-based chimeras"), - make_option("--chimrefu", action="store", default=NA, type='character', help="Number of unique sequences detected as reference-based chimeras"), - make_option("--chimdenovo", action="store", default=NA, type='character', help="Number of de novo chimeras"), - make_option("--chimrecovn", action="store", default=NA, type='character', help="Number of resued reads for de novo chimeras (false positives)"), - make_option("--chimrecovu", action="store", default=NA, type='character', help="Number of resued unique sequences detected as de novo chimeras (false positives)"), - make_option("--tj", action="store", default=NA, type='character', help="Tag jump removal data (serialized in qs format)"), - make_option("--seqtab", action="store", default=NA, type='character', help="Final seq table (Parquet format)"), - make_option("--maxchim", action="store", default=0.6, type='numeric', help = "Maximum de novo chimera score to remove"), - make_option(c("-t", "--threads"), action="store", default=4L, type='integer', help="Number of CPU threads for arrow, default 4") -) -opt <- parse_args(OptionParser(option_list=option_list)) - - -## Validation of the required argiments -# if(is.na(opt$raw)){ -# cat("Input file is not specified: ....\n", file=stderr()) -# stop() -# } - - - -## Function to convert text "NA"s to NA -to_na <- function(x){ - if(x %in% c("NA", "null", "Null")){ x <- NA } - return(x) -} - -## Assign variables -RAW <- opt$raw -QC <- opt$qc -DEMUXED <- opt$demuxed -PRIMER <- opt$primer -PRIMERARTEF <- opt$primerartef -ITSX <- opt$itsx -HOMOPOLY <- opt$homopolymer -CHIMREFN <- opt$chimrefn -CHIMREFU <- opt$chimrefu -CHIMDENOVO <- opt$chimdenovo -CHIMRECOVN <- opt$chimrecovn -CHIMRECOVU <- opt$chimrecovu -TJ <- opt$tj -SEQTAB <- opt$seqtab -MAXCHIM <- opt$maxchim -CPUTHREADS <- as.numeric( opt$threads ) - -## Log assigned variables -cat(paste("Counts - RawData: " , RAW, "\n", sep="")) -cat(paste("Counts - QC: " , QC, "\n", sep="")) -cat(paste("Counts - Demux: " , DEMUXED, "\n", sep="")) -cat(paste("Counts - PrimerCheck: " , PRIMER, "\n", sep="")) -cat(paste("Counts - Primer Artefacts: " , PRIMERARTEF, "\n", sep="")) -cat(paste("Counts - ITSx or Primer Trim: " , ITSX, "\n", sep="")) -cat(paste("Counts - Homopolymer correction results: " , HOMOPOLY, "\n", sep="")) -cat(paste("Counts - Chimera Ref-based, reads: " , CHIMREFN, "\n", sep="")) -cat(paste("Counts - Chimera Ref-based, unique sequences: " , CHIMREFU, "\n", sep="")) -cat(paste("Counts - Chimera de novo: " , CHIMDENOVO, "\n", sep="")) -cat(paste("Counts - Chimera Ref-based recoverd, reads: " , CHIMRECOVN, "\n", sep="")) -cat(paste("Counts - Chimera Ref-based recoverd, unique sequences: " , CHIMRECOVU, "\n", sep="")) -cat(paste("Tag-jump data: " , TJ, "\n", sep="")) -cat(paste("Final sequence table: " , SEQTAB, "\n", sep="")) -cat(paste("Maximum de novo chimera score: ", MAXCHIM, "\n", sep="")) -cat(paste("Number of CPU threads to use: ", CPUTHREADS, "\n", sep="")) - -cat("\n") - - -############################################## data for debuging - -# RAW <- "Counts_1.RawData.txt" -# QC <- "Counts_2.QC.txt" -# DEMUXED <- "Counts_3.Demux.txt" -# PRIMER <- "Counts_4.PrimerCheck.txt" -# PRIMERARTEF <- "Counts_4.PrimerArtefacts.txt" -# ITSX <- "Counts_5.ITSx_or_PrimTrim.txt" -# HOMOPOLY <- "Counts_5.Homopolymers.txt" -# CHIMREFN <- "Counts_6.ChimRef_reads.txt" -# CHIMREFU <- "Counts_6.ChimRef_uniqs.txt" -# CHIMDENOVO <- "Counts_7.ChimDenov.txt" -# CHIMRECOVN <- "Counts_8.ChimRecov_reads.txt" -# CHIMRECOVU <- "Counts_8.ChimRecov_uniqs.txt" -# TJ <- "TagJump_scores.qs" -# SEQTAB <- "Seqs.parquet" -# MAXCHIM <- 0.6 -# CPUTHREADS <- 6 - - -############################################## Load packages and data - -cat("Loading R packages...\n") - -load_pckg <- function(pkg = "data.table"){ - suppressPackageStartupMessages( library(package = pkg, character.only = TRUE) ) - cat(paste(pkg, packageVersion(pkg), "\n")) -} - -load_pckg("data.table") -load_pckg("plyr") -load_pckg("arrow") -# load_pckg("dplyr") -load_pckg("metagMisc") -load_pckg("openxlsx") - -cat("\n") - -## Set CPU thread number -cat("Setting number of CPU threads to: ", CPUTHREADS, "\n") -setDTthreads(threads = CPUTHREADS) # for data.table - - -###################################### -###################################### Load the data -###################################### - -cat("\nLoading input data\n") - - -#### Per-dataset stats - -## Load ASV table -cat("..Loading raw counts\n") -RAW <- fread(RAW) - -cat("..Loading QC counts\n") -QC <- fread(QC) - -#### Per-sample stats - -SEQKITCOUNTS <- list() -CUSTOMCOUNTS <- list() - -cat("..Loading demux counts\n") -SEQKITCOUNTS$DEMUXED <- fread(DEMUXED) - -cat("..Loading primer-checked data counts\n") -SEQKITCOUNTS$PRIMER <- fread(PRIMER) -SEQKITCOUNTS$PRIMERARTEF <- fread(PRIMERARTEF) - -cat("..Loading ITSx or primer trim counts\n") -CUSTOMCOUNTS$ITSX <- fread(ITSX) - -cat("..Loading homopolymer correction results\n") -HOMOPOLY_data <- fread(HOMOPOLY) - -cat("..Loading ref-based chimera counts\n") -CUSTOMCOUNTS$CHIMREFN <- fread(CHIMREFN) -SEQKITCOUNTS$CHIMREFU <- fread(CHIMREFU) - -cat("..Loading de novo chimera counts\n") -CHIMDENOVO <- fread(CHIMDENOVO) # incorporate to the main table - -cat("..Loading rescued ref-based chimera counts\n") -CUSTOMCOUNTS$CHIMRECOVN <- fread(CHIMRECOVN) -SEQKITCOUNTS$CHIMRECOVU <- fread(CHIMRECOVU) - -if(!is.na(TJ) && TJ != "no_tj" && file.exists(TJ)){ - cat("..Loading tag-jump filtration data\n") - TJ <- qs::qread(TJ) - tjdata <- TRUE -} else { - cat("..No tag-jump filtration data found\n") - tjdata <- FALSE -} - -cat("..Loading sequence table\n") -SEQTAB <- arrow::open_dataset(SEQTAB) - - -## Remove NULL-files -null_cust <- laply(.data = CUSTOMCOUNTS, .fun = nrow) -null_seqk <- laply(.data = SEQKITCOUNTS, .fun = nrow) - -if(any(null_cust == 0)){ - cat("Some files with counts are missing:\n") - to_rm <- which(null_cust == 0) - cat(".. ", paste(names(CUSTOMCOUNTS)[ to_rm ], collapse = ", "), "\n") - CUSTOMCOUNTS[ to_rm ] <- NULL - rm(to_rm) -} - -if(any(null_seqk == 0)){ - cat("Some files with counts are missing:\n") - to_rm <- which(null_seqk == 0) - cat(".. ", paste(names(SEQKITCOUNTS)[ to_rm ], collapse = ", "), "\n") - SEQKITCOUNTS[ to_rm ] <- NULL - rm(to_rm) -} - - -## Process seqkit counts -seqkit_process <- function(x){ - if(nrow(x) > 0){ - - ## Remove reudndant columns - x <- x[ , .(file, num_seqs) ] - - ## Remove file extensions - x[ , file := sub(pattern = ".fastq.gz$", replacement = "", x = file) ] - x[ , file := sub(pattern = ".fq.gz$", replacement = "", x = file) ] - x[ , file := sub(pattern = ".fa.gz$", replacement = "", x = file) ] - x[ , file := sub(pattern = ".full.fasta$", replacement = "", x = file) ] - x[ , file := sub(pattern = ".ITS1.fasta.gz$", replacement = "", x = file) ] - x[ , file := sub(pattern = ".ITS2.fasta.gz$", replacement = "", x = file) ] - x[ , file := sub(pattern = "_PrimerChecked$", replacement = "", x = file) ] - x[ , file := sub(pattern = "_PrimerArtefacts$", replacement = "", x = file) ] - x[ , file := sub(pattern = "_Chimera$", replacement = "", x = file) ] - x[ , file := sub(pattern = "_RescuedChimera$", replacement = "", x = file) ] - x[ , file := sub(pattern = "^Rescued_Chimeric_sequences.part_", replacement = "", x = file) ] - - } - return(x) -} - -## Process custom counts -custom_process <- function(x){ - if(nrow(x) > 0){ - - ## There should be just two columns - `SampleID` & `NumReads` - - ## Rename "SampleID" into "file" - setnames(x = x, old = "SampleID", new = "file") - - ## Remove file extensions - x[ , file := sub(pattern = ".full.fasta$", replacement = "", x = file) ] - x[ , file := sub(pattern = "_ITS1_58S_ITS2.fasta$", replacement = "", x = file) ] - x[ , file := sub(pattern = "_Chimera.fa$", replacement = "", x = file) ] - x[ , file := sub(pattern = "_RescuedChimera.fa$", replacement = "", x = file) ] - x[ , file := sub(pattern = "^Rescued_Chimeric_sequences.part_", replacement = "", x = file) ] - - } - return(x) -} - - -cat("Processing data\n") -SEQKITCOUNTS <- llply(.data = SEQKITCOUNTS, .fun = seqkit_process) -CUSTOMCOUNTS <- llply(.data = CUSTOMCOUNTS, .fun = custom_process) - -cat("Estimating homopolymer stats\n") -if(nrow(HOMOPOLY_data) > 0){ - HOMOPOLY_counts <- HOMOPOLY_data[ , .( - N_UniqSequences_AfterITSx_or_PrimerTrimming = .N, - N_UniqSequences_AfterHomopolymerCorrection = length(unique(Target)) - ), - by = "SampleID" ] -} else { - cat("..No homopolymer correction data found\n") -} - -# HOMOPOLY_counts[, Num_HomopolymerCorrectedSequences := -# N_UniqSequences_AfterITSx_or_PrimerTrimming - N_UniqSequences_AfterHomopolymerCorrection ] - - -## Rename columns -if(!is.null(SEQKITCOUNTS$DEMUXED)){ -setnames(x = SEQKITCOUNTS$DEMUXED, old = "num_seqs", new = "Demultiplexed_Reads", skip_absent = TRUE) -} -if(!is.null(SEQKITCOUNTS$PRIMER)){ -setnames(x = SEQKITCOUNTS$PRIMER, old = "num_seqs", new = "PrimerChecked_Reads", skip_absent = TRUE) -} -if(!is.null(SEQKITCOUNTS$PRIMERARTEF)){ -setnames(x = SEQKITCOUNTS$PRIMERARTEF, old = "num_seqs", new = "PrimerArtefacts_Reads", skip_absent = TRUE) -} -if(!is.null(SEQKITCOUNTS$CHIMREFU)){ -setnames(x = SEQKITCOUNTS$CHIMREFU, old = "num_seqs", new = "ReferenceBasedChimera_NumUniqSequences", skip_absent = TRUE) -} -if(!is.null(SEQKITCOUNTS$CHIMRECOVU)){ -setnames(x = SEQKITCOUNTS$CHIMRECOVU, old = "num_seqs", new = "Recovered_ReferenceBasedChimea_NumUniqSequences", skip_absent = TRUE) -} - -if(!is.null(CUSTOMCOUNTS$ITSX)){ -setnames(x = CUSTOMCOUNTS$ITSX, old = "NumReads", new = "ITSx_Extracted_Reads", skip_absent = TRUE) -} -if(!is.null(CUSTOMCOUNTS$CHIMREFN)){ -setnames(x = CUSTOMCOUNTS$CHIMREFN, old = "NumReads", new = "ReferenceBasedChimera_Reads", skip_absent = TRUE) -} -if(!is.null(CUSTOMCOUNTS$CHIMRECOVN)){ -setnames(x = CUSTOMCOUNTS$CHIMRECOVN, old = "NumReads", new = "Recovered_ReferenceBasedChimea_Reads", skip_absent = TRUE) -} - -## Merge seqkit and custom counts into a single list -cat("Pooling per-sample counts\n") -COUNTS <- c(SEQKITCOUNTS, CUSTOMCOUNTS) - -## Pool per-file estimates -merge_dt <- function(x,y){ merge(x, y, by = "file", all = TRUE) } -PER_SAMPLE_COUNTS_merged <- Reduce(f = merge_dt, x = COUNTS) - -## If there are no primer artefacts -if(is.null(SEQKITCOUNTS$PRIMERARTEF)){ - PER_SAMPLE_COUNTS_merged[ , PrimerArtefacts_Reads := 0 ] -} - -## Estimate percentage of primer artefacts -cat("Estimating percentage of primer artefacts\n") -PER_SAMPLE_COUNTS_merged[ , - PrimerArtefacts_Percent := round( - PrimerArtefacts_Reads / (PrimerChecked_Reads + PrimerArtefacts_Reads) * 100, - 2) - ] - -## Estimate tag-jump stats -if(tjdata == TRUE){ - cat("Estimating tag-jump removal yields\n") - TJ_stats <- TJ[ TagJump == TRUE, .( - TagJump_Events = .N, - TagJump_Reads = sum(Abundance, na.rm = TRUE)), - by = "SampleID" ] - - if(nrow(TJ_stats) > 0){ - PER_SAMPLE_COUNTS_merged <- merge( - x = PER_SAMPLE_COUNTS_merged, - y = TJ_stats, - by.x = "file", by.y = "SampleID", all.x = TRUE) - } else { - PER_SAMPLE_COUNTS_merged[ , TagJump_Events := 0 ] - PER_SAMPLE_COUNTS_merged[ , TagJump_Reads := 0 ] - } -} - -## Add homopolymer stats -if(nrow(HOMOPOLY_data) > 0){ - cat("Adding homopolymer stats\n") - PER_SAMPLE_COUNTS_merged <- merge( - x = PER_SAMPLE_COUNTS_merged, - y = HOMOPOLY_counts, - by.x = "file", by.y = "SampleID", all.x = TRUE) -} - -## Add de novo chimera stats -cat("Adding de novo chimera stats\n") -denovo_stats <- SEQTAB |> - dplyr::filter(DeNovo_Chimera_Score >= MAXCHIM) |> - dplyr::group_by(SampleID) |> - dplyr::summarize( - DeNovoChimeras_NumReads = sum(Abundance, na.rm = TRUE), - DeNovoChimeras_NumUniqSeqs = n()) |> - dplyr::collect() |> - setDT() - -PER_SAMPLE_COUNTS_merged <- merge( - x = PER_SAMPLE_COUNTS_merged, - y = denovo_stats, - by.x = "file", by.y = "SampleID", all.x = TRUE) - - - -## Number of reads and unique sequences in the sequence table (per sample) -## Excluding putative de novo chimeras (with score >= MAXCHIM) -cat("Adding sequence table stats\n") -seqtab_stats <- SEQTAB |> - dplyr::filter(DeNovo_Chimera_Score < MAXCHIM | is.na(DeNovo_Chimera_Score) ) |> - dplyr::group_by(SampleID) |> - dplyr::summarize( - SeqTable_NumReads = sum(Abundance, na.rm = TRUE), - SeqTable_NumUniqSeqs = n()) |> - dplyr::collect() |> - setDT() - -PER_SAMPLE_COUNTS_merged <- merge( - x = PER_SAMPLE_COUNTS_merged, - y = seqtab_stats, - by.x = "file", by.y = "SampleID", all.x = TRUE) - - -## Replace NAs with zeros -cat("Replacing NAs with zero\n") -for (j in seq_len(ncol(PER_SAMPLE_COUNTS_merged))){ - set(PER_SAMPLE_COUNTS_merged, which(is.na(PER_SAMPLE_COUNTS_merged[[j]])), j, 0) -} -rm(j) - -## Estimate percentage of reads retained (starting from demultiplexed reads) -cat("Estimating percentage of reads retained\n") -PER_SAMPLE_COUNTS_merged[ , Percentage_Reads_Retained := round( SeqTable_NumReads / Demultiplexed_Reads * 100, 2) ] - -## Estimate percentage of reads retained after ITSx -if("ITSx_Extracted_Reads" %in% colnames(PER_SAMPLE_COUNTS_merged)){ - PER_SAMPLE_COUNTS_merged[ , ITSx_Yield_Percent := round( ITSx_Extracted_Reads / PrimerChecked_Reads * 100, 2) ] -} - -### TODO: -# .. estimate percentages -# .. add per-run positive / negative counts (based on default sample names) - - -## Reorder columns -setcolorder(PER_SAMPLE_COUNTS_merged, - skip_absent = TRUE, - neworder = c( - "file", "Demultiplexed_Reads", - "PrimerChecked_Reads", "PrimerArtefacts_Reads", "PrimerArtefacts_Percent", - "ReferenceBasedChimera_Reads", "ReferenceBasedChimera_NumUniqSequences", - "Recovered_ReferenceBasedChimea_Reads", "Recovered_ReferenceBasedChimea_NumUniqSequences", - "DeNovoChimeras_NumReads", "DeNovoChimeras_NumUniqSeqs", - "ITSx_Extracted_Reads", "ITSx_Yield_Percent", - "N_UniqSequences_AfterITSx_or_PrimerTrimming", - "N_UniqSequences_AfterHomopolymerCorrection", - # "Num_HomopolymerCorrectedSequences", - "TagJump_Reads", "TagJump_Events", - "SeqTable_NumReads", "SeqTable_NumUniqSeqs", - "Percentage_Reads_Retained")) - - - - -## Prepare per-run stats -cat("Preparing per-run stats\n") -PER_RUN_COUNTS_merged <- data.table( - Total_Number_Of_Reads = sum(RAW$num_seqs, na.rm = TRUE), - Reads_Demultiplexed = sum(PER_SAMPLE_COUNTS_merged$Demultiplexed_Reads, na.rm = TRUE), - Reads_Passed_QC = sum(QC$num_seqs, na.rm = TRUE), - Reads_PrimerChecked = sum(PER_SAMPLE_COUNTS_merged$PrimerChecked_Reads, na.rm = TRUE) - ) - -if("ITSx_Extracted_Reads" %in% colnames(PER_SAMPLE_COUNTS_merged)){ - PER_RUN_COUNTS_merged[ , Reads_ITSx_Extracted := sum(PER_SAMPLE_COUNTS_merged$ITSx_Extracted_Reads, na.rm = TRUE) ] -} - -## Estimate percentage of reads passed primer checking -cat("..Estimating per-run percentages\n") -PER_RUN_COUNTS_merged[ , Percentage_Demultiplexed := - round(Reads_Demultiplexed / Total_Number_Of_Reads * 100, 1) ] - -PER_RUN_COUNTS_merged[ , Percentage_PrimerChecked := - round(Reads_PrimerChecked / Total_Number_Of_Reads * 100, 1) ] - -## Final per-run num reads -PER_RUN_COUNTS_merged[ , SeqTable_NumReads := sum(PER_SAMPLE_COUNTS_merged$SeqTable_NumReads, na.rm = TRUE) ] -PER_RUN_COUNTS_merged[ , Percentage_Reads_Retained := round( SeqTable_NumReads / Total_Number_Of_Reads * 100, 2) ] - -NumUniqSeqs <- SEQTAB |> dplyr::select(SeqID) |> dplyr::summarize(N = n()) |> dplyr::collect() -PER_RUN_COUNTS_merged$SeqTable_NumUniqueSequences <- NumUniqSeqs$N - -## Export summary stats -cat("Exporting results\n") -write.xlsx(list( - "per_sample" = PER_SAMPLE_COUNTS_merged, - "per_run" = PER_RUN_COUNTS_merged - ), - file = "Run_summary.xlsx", colNames = TRUE) - - -cat("\nAll done.\n") - - -##################### Session info - -## Check time -end_time <- Sys.time() - -tmm <- as.numeric(difftime(end_time, start_time, units = "min")) -cat("\nElapsed time: ", tmm, " minutes\n") - -cat("\n") -cat("Session info:\n") -sessionInfo() -cat("\n") diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/seq_table_assembly.R b/src/pipecraft-core/service_scripts/NextITS/bin/seq_table_assembly.R deleted file mode 100755 index 161fc594..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/seq_table_assembly.R +++ /dev/null @@ -1,299 +0,0 @@ -#!/usr/bin/env Rscript - -## Script to perform tag-jump removal - -## To do: -# - add HMM profile ID if ITSx was used - -# Inputs: -# 1. tag-jump-filtered Seq table (`Seq_tab_TagJumpFiltered.txt.gz`) -# 2. Sequences in fasta (`Seq_not_filtered.fa.gz`) -# 3. de novo chimera scores (`DeNovo_Chimera.txt`) -# 4. sequence qualities (`SeqQualities.parquet`) - -# Outputs: -# - FASTA with filtered Seqs `Seqs.fa.gz` -# - Seq table in long format `Seqs.txt.gz` (with additional sequence info) -# - Data in Parquet format `Seqs.parquet` - - -## Function to load packages -load_pckg <- function(pkg = "data.table"){ - suppressPackageStartupMessages( library(package = pkg, character.only = TRUE) ) - cat(".. ", paste(pkg, packageVersion(pkg), "\n")) -} - -cat("Loading packages:\n") - -load_pckg("optparse") -load_pckg("data.table") -load_pckg("Biostrings") -load_pckg("plyr") -load_pckg("arrow") -# load_pckg("dplyr") -# load_pckg("openxlsx") - - -cat("\nParsing input options and arguments...\n") - -option_list <- list( - make_option("--seqtab", action="store", default=NA, type='character', help = "Sequence table (tab-delimited, long format)"), - make_option("--fasta", action="store", default=NA, type='character', help = "Sequences in FASTA format"), - make_option("--chimera", action="store", default=NA, type='character', help = "De novo chimera scores"), - make_option("--quality", action="store", default=NA, type='character', help = "Sequence qualities (Parquet format)"), - make_option("--threads", action="store", default=4, type='integer', help = "Number of CPU threads to use") -) - -opt <- parse_args(OptionParser(option_list=option_list)) - -## Function to convert text "NA"s to NA -to_na <- function(x){ - if(x %in% c("NA", "null", "Null")){ x <- NA } - return(x) -} - -## Replaces "null"s from Nextflow with NA -opt <- lapply(X = opt, FUN = to_na) - - -## Validation of the required arguments -required_args <- c("seqtab", "fasta", "quality") -missing_args <- required_args[ sapply(required_args, function(x) is.na(opt[[x]])) ] -if (length(missing_args) > 0) { - stop("Missing required arguments: ", paste(missing_args, collapse=", ")) -} - -## Assign variables -SEQTAB <- opt$seqtab -FASTA <- opt$fasta -CHIMERA <- opt$chimera -QUALITY <- opt$quality -CPUTHREADS <- as.numeric( opt$threads ) - -## Log assigned variables -cat(paste("Input sequence table: ", SEQTAB, "\n", sep="")) -cat(paste("Sequences in FASTA format: ", FASTA, "\n", sep="")) -cat(paste("De novo chimera scores: ", CHIMERA, "\n", sep="")) -cat(paste("Sequence qualities: ", QUALITY, "\n", sep="")) -cat(paste("Number of CPU threads to use: ", CPUTHREADS, "\n", sep="")) - -cat("\n") - - -## Debug: -# SEQTAB <- "Seq_tab_TagJumpFiltered.txt.gz" -# FASTA <- "Seq_not_filtered.fa.gz" -# CHIMERA <- "DeNovo_Chimera.txt" -# QUALITY <- "SeqQualities.parquet" -# CPUTHREADS <- 4 - - -## Set CPU thread number -cat("Setting number of CPU threads to: ", CPUTHREADS, "\n") -setDTthreads(threads = CPUTHREADS) # for data.table -set_cpu_count(CPUTHREADS) # for arrow - -###################################### -###################################### Load the data -###################################### - -## Load sequnece table -cat("\n\n..Loading sequence table\n") - -TAB <- fread( - file = SEQTAB, - sep = "\t", header = TRUE) - -## Load sequences in fasta format -cat("..Loading sequences in FASTA format\n") -SQS <- readDNAStringSet(filepath = FASTA) - -## Load de novo chimera scores -cat("..Loading de novo chimera scores\n") - -CHI <- try( - fread( - file = CHIMERA, - header = FALSE, sep = "\t", - col.names = c("SeqID", "DeNovo_Chimera_Score", "SampleID")) - ) - -if("try-error" %in% class(CHI)){ - cat("\nCould not read the file with de novo chimeric scores\n") - cat("Most likely, the file file is empty (no de novo chimeras)\n") - - ## Initialize empty data table - CHI <- data.table(SeqID = character(), DeNovo_Chimera_Score = numeric(), SampleID = character()) -} - - -## Load sequence quality scores -cat("..Loading sequence quality scores\n") -QLT <- arrow::open_dataset(QUALITY) |> - dplyr::select(Hash, Length, AvgPhredScore, MaxEE, MEEP) |> - dplyr::collect() |> - dplyr::filter(Hash %in% unique(TAB$SeqID)) |> - setDT() - -setnames(QLT, - old = c("Hash", "Length", "AvgPhredScore"), - new = c("SeqID", "SeqLen", "PhredScore")) - -## Quality data: -# old header: c("SampleID", "SeqID", "SeqLen", "PhredScore", "MaxEE", "MEEP") -# new header: c("SampleID", "Hash", "PacBioID", "PhredScore", "MaxEE", "MEEP", "Sequence", "Quality", "Length") - - -## Create SeqID___SampleID column -TAB[, SeqID___SampleID := paste0(SeqID, "___", SampleID) ] -# QLT[, SeqID___SampleID := paste0(SeqID, "___", SampleID) ] - - -###################################### -###################################### Add quality scores, for non-singleton use max score -###################################### - -cat("\n\n..Adding quality scores\n") - -cat("...Prepareing quality scores\n") -setorder(QLT, SeqID, -PhredScore) -QLT <- QLT[QLT[, .I[which.max(PhredScore)], by=SeqID]$V1] - -cat("...Adding data to the main table\n") -if(any(! TAB$SeqID %in% QLT$SeqID)){ - cat("WARNING: Some sequences are not present in the quality table\n") -} - -TAB <- merge(x = TAB, y = QLT, by = "SeqID", all.x = TRUE) - -# with(TAB, plot(Abundance, PhredScore)) - - -###################################### -###################################### Add chimera info -###################################### - -cat("..Adding info about de novo chimeric sequences\n") - -if(nrow(CHI) > 0){ - - TAB <- merge(x = TAB, y = CHI, - by = c("SeqID", "SampleID"), all.x = TRUE) - - ## Convert variables to numeric scores - TAB[ , DeNovo_Chimera_Score := as.numeric(DeNovo_Chimera_Score) ] - - ## Classify sequences into putative chimeras - TAB[ !is.na(DeNovo_Chimera_Score), DeNovo_Chimera := TRUE ] - TAB[ is.na(DeNovo_Chimera_Score), DeNovo_Chimera := FALSE ] - - cat("... ", sum( TAB$DeNovo_Chimera), " putative de novo chimeras found\n") - cat("... ", sum(!TAB$DeNovo_Chimera), " non-chimeric sequences\n") - -} else { - - ## No de novo chimeras - - TAB[ , DeNovo_Chimera_Score := as.numeric(NA) ] - TAB[ , DeNovo_Chimera := FALSE ] - - cat("... ", 0, " putative de novo chimeras found\n") - cat("... ", nrow(TAB), " non-chimeric sequences\n") - -} - - -###################################### -###################################### Add sequences -###################################### - -cat("\n\n..Processing sequences\n") - -SQTAB <- data.table( - SeqHeader = names(SQS), - Sequence = as.character(SQS)) - -## Split the header (`feb76b9;size=1;sample=ABCD;` ) -SQTAB[ , c("SeqID", "SampleID") := tstrsplit(x = SeqHeader, split = ";", keep = c(1,2)) ] -SQTAB[ , SeqHeader := NULL ] -SQTAB[ , SampleID := gsub(pattern = "sample=", replacement = "", x = SampleID) ] - - -SQTAB[ , SeqID___SampleID := paste0(SeqID, "___", SampleID) ] -SQTAB[ , c("SeqID", "SampleID") := NULL ] - -cat("..Adding sequences to the main table\n") - -TAB <- merge(x = TAB, y = SQTAB, - by = c("SeqID___SampleID"), all.x = TRUE) - - -cat("..Sorting table by abundance, quality score, and SampleID\n") - -setorder(x = TAB, -Abundance, -PhredScore, SampleID) - - -cat("..Preparing FASTA file with filtered sequences\n") - -SQF <- DNAStringSet(x = TAB$Sequence) -names(SQF) <- paste0(TAB$SeqID, ";size=", TAB$Abundance, ";sample=", TAB$SampleID, ";") - -## Export FASTA -cat("..Exporting FASTA file with filtered sequences\n") - -writeXStringSet(x = SQF, - filepath = "Seqs.fa.gz", - compress = TRUE, format = "fasta", width = 9999) - - - - -###################################### -###################################### Export results -###################################### - -# cat("..Reshaping sequence table into wide format\n") -# -# TABW <- dcast(data = TAB, -# formula = SeqID ~ SampleID, -# value.var = "Abundance", -# fill = 0) - - -cat("..Exporting result\n") - -setcolorder( - x = TAB, - neworder = c( - "SeqID___SampleID", "SampleID", "SeqID", - "Abundance", "SeqLen", "PhredScore", "MaxEE", "MEEP", - "DeNovo_Chimera", "DeNovo_Chimera_Score", - "Sequence")) - -# cat("...Exporting RData\n") -# saveRDS(object = TAB, file = "Seqs.RData", compress = "xz") - -cat("...Exporting Parquet\n") - -write_parquet( - x = TAB, - sink = "Seqs.parquet", - compression = "zstd", - compression_level = 10, - use_dictionary = TRUE) - - -## Long table -cat("...Exporting long table\n") - -TAB[ , SeqID___SampleID := NULL ] - -fwrite(x = TAB, file = "Seqs.txt.gz", sep = "\t", compress = "gzip") - -## Wide table -# cat("...Exporting wide table\n") - -# fwrite(x = TABW, file = "Seq_tab.txt.gz", sep = "\t", compress = "gzip") - - -cat("All done.") diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/substitute_compressed_seqs.R b/src/pipecraft-core/service_scripts/NextITS/bin/substitute_compressed_seqs.R deleted file mode 100755 index 0f66d715..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/substitute_compressed_seqs.R +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env Rscript - -## Script to replace homopolymer-comressed sequences with non-compressed seqs -## + update size annotation - -# Input is given as positional arguments: -# 1. Uncompressed sequences (`inp_tab.txt`) -# 2. Homopolymer-compressed sequences (`clust_tab.txt`) -# 3. Name of the output FASTA file (`res.fa`) - -suppressMessages(library(data.table)); setDTthreads(threads = 1) -suppressMessages(library(Biostrings)) - -args <- commandArgs(trailingOnly = TRUE) - -## Load data - Uncompressed (inp_tab.txt) -cat("..Loading original sequences\n") -d1 <- fread(file = args[1], - header=FALSE, sep = "\t", quote = F, col.names = c("SeqID", "Seq_OK"), selec = 1:2) - -## Load data - Compressed (clust_tab.txt) -cat("..Loading compressed sequences\n") -d2 <- fread(file = args[2], - header=FALSE, sep = "\t", quote = F, col.names = c("SeqID", "Seq_Compr"), selec = 1:2) - -cat("...Number of raw sequences: ", nrow(d1), "\n") -cat("...Number of compressed sequences: ", nrow(d2), "\n") - -cat("..Processing data\n") - -## Remove multiple separators -d1[, SeqID := gsub(pattern = ";;", replacement = ";", x = SeqID)] -d2[, SeqID := gsub(pattern = ";;", replacement = ";", x = SeqID)] - -## Split seq ID -d1[, c("Hash", "Size") := tstrsplit(SeqID, ";", keep=1:2)] -d2[, c("Hash", "Size") := tstrsplit(SeqID, ";", keep=1:2)] - -## Drop seq ID -d1[, SeqID := NULL ] -d2[, SeqID := NULL ] - -## Replace seqs -res <- merge( - x = d2[, .(Hash, Size)], - y = d1[, .(Seq_OK, Hash)], - by = "Hash", all.x = TRUE) - -res[, SeqID := do.call(paste, c(.SD, sep = ";")), .SDcols = c("Hash", "Size")] - -## Verify the number of reads - should be the same -# sum(as.numeric(gsub(pattern = "size=", replacement = "", x = d1$Size))) -# sum(as.numeric(gsub(pattern = "size=", replacement = "", x = d2$Size))) -# sum(as.numeric(gsub(pattern = "size=", replacement = "", x = res$Size))) - -cat("...Total number of reads: ", - sum(as.numeric(gsub(pattern = "size=", replacement = "", x = res$Size))), - "\n") - -## Prepare sequences -cat("..Exporting results\n") -sqs <- DNAStringSet(x = res$Seq_OK) -names(sqs) <- res$SeqID - -## Export FASTA -writeXStringSet(x = sqs, filepath = args[3], - compress=FALSE, format="fasta", width=9999) diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/summarize_clustered_data.R b/src/pipecraft-core/service_scripts/NextITS/bin/summarize_clustered_data.R deleted file mode 100755 index c6e0b6fc..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/summarize_clustered_data.R +++ /dev/null @@ -1,400 +0,0 @@ -#!/usr/bin/env Rscript - -## Script to pool remove low-quality singletons and summarize sequence abundance at OTU level (per sample) - -# Input: -# 1. Sequence tables in long format with de novo chimeras removed (`Seqs.parquet`) -# 2. UC file (`UC_Pooled.parquet`) -# 3. FASTA file with OTU sequences (`Clustered.fa.gz`) -# 4. Max MEEP score - -# Outputs: -# - OTU table in long format (`OTU_table_long.txt.gz` & `OTU_table_long.RData`) -# - OTU table in wide format (`OTU_table_wide.txt.gz` & `OTU_table_wide.RData`) -# - FASTA file with sequences (`OTUs.fa.gz`) - -## Usage: -# ./summarize_clustered_data.R \ -# --seqtab "Seqs.parquet" \ -# --uc "UC_Pooled.parquet" \ -# --otus "Clustered.fa.gz" \ -# --maxmeep 0.6 \ -# --recoversinglet TRUE \ -# --mergesamples TRUE \ -# --threads 4 - - -## Quality threshold: -# MEEP score of 0.6 corresponds approximately to the average Phred score of 22.2 - -## Singleton recovery: -# If enabled, then singleton OTUs with MEEP score <= 0.6 & will be preserved -# Otherwise, singleton OTUs will be removed - - -############################################## Parse input parameters - -## Check time -start_time <- Sys.time() - - -cat("Parsing input options and arguments:\n") - -suppressPackageStartupMessages(require(optparse)) - -## Parse arguments -option_list <- list( - make_option("--seqtab", action="store", default=NA, type='character', help="Sequence tables in long format with de novo chimeras removed (Parquet format)"), - make_option("--uc", action="store", default=NA, type='character', help="UC file (Parquet format)"), - make_option("--otus", action="store", default=NA, type='character', help="FASTA file with OTU sequences"), - make_option("--maxmeep", action="store", default=0.5, type='double', help="Max MEEP score"), - make_option("--recoversinglet", action="store", default=TRUE, type='logical', help="Recover singletons"), - make_option(c("-m", "--mergesamples"), action="store", default=FALSE, type='logical', help="Merge sample replicates (default, false)"), - make_option(c("-t", "--threads"), action="store", default=4L, type='integer', help="Number of CPU threads for arrow, default 4") - # make_option(c("-s", "--scriptdir"), action="store", default=getwd(), type='character', help="Directory containing source scripts") -) -opt <- parse_args(OptionParser(option_list=option_list)) - -## Function to convert text "NA"s to NA -to_na <- function(x){ - if(x %in% c("NA", "null", "Null")){ x <- NA } - return(x) -} - -## Replaces "null"s from Nextflow with NA -opt <- lapply(X = opt, FUN = to_na) - - -## Validation of the required argiments -if(is.na(opt$seqtab)){ - cat("Input file is not specified: sequence tables in Parquet format.\n", file=stderr()) - stop() -} -if(is.na(opt$uc)){ - cat("Input file is not specified: UC file is required.\n", file=stderr()) - stop() -} -if(is.na(opt$otus)){ - cat("Input file is not specified: FASTA file with OTU sequences.\n", file=stderr()) - stop() -} -if(opt$recoversinglet == TRUE && is.na(opt$maxmeep)){ - cat("For singleton recovery, the max MEEP score must be specified.\n", file=stderr()) - stop() -} - -## Assign variables -SEQTAB <- opt$seqtab -UCF <- opt$uc -MAXMEEP <- as.numeric( opt$maxmeep ) -RECOV_SINGLET <- as.logical(opt$recoversinglet) -MERGE_SAMPLES <- as.logical(opt$mergesamples) -OTUS <- opt$otus - -CPUTHREADS <- as.numeric( opt$threads ) -# SCRIPTDIR <- opt$scriptdir - -## Log assigned variables -cat(paste("Sequence tables (Parquet format): ", SEQTAB, "\n", sep="")) -cat(paste("UC file (Parquet format): ", UCF, "\n", sep="")) -cat(paste("Max MEEP score: ", MAXMEEP, "\n", sep="")) -cat(paste("Low-quality singleton recovery: ", RECOV_SINGLET, "\n", sep="")) -cat(paste("Merge sample replicates: ", MERGE_SAMPLES, "\n", sep="")) -cat(paste("OTU sequences: ", OTUS, "\n", sep="")) -cat(paste("Number of CPU threads to use: ", CPUTHREADS, "\n", sep="")) -# cat(paste("Directory containing source scripts: ", SCRIPTDIR, "\n", sep="")) - -cat("\n") - - - -############################################## Data for debugging - -# SEQTAB <- "Seqs.parquet" -# UCF <- "UC_Pooled.parquet" -# MAXMEEP <- 0.5 -# RECOV_SINGLET <- TRUE -# MERGE_SAMPLES <- TRUE -# OTUS <- "Clustered.fa.gz" -# CPUTHREADS <- 4 - - -############################################## Load packages and data - -cat("Loading R packages:\n") - -load_pckg <- function(pkg = "data.table"){ - suppressPackageStartupMessages( library(package = pkg, character.only = TRUE) ) - cat(paste(pkg, packageVersion(pkg), "\n")) -} - -load_pckg("data.table") -load_pckg("plyr") -load_pckg("metagMisc") -load_pckg("Biostrings") -load_pckg("arrow") - - -cat("\n") - - -# cat("Loading additional R funcitons...\n") -# source(file.path(SCRIPTDIR, "R_functions.R")) -# cat("\n") - - -## Set CPU thread number -cat("Setting number of CPU threads to: ", CPUTHREADS, "\n") -setDTthreads(threads = CPUTHREADS) # for data.table -set_cpu_count(CPUTHREADS) # for arrow - -###################################### -###################################### Load the data -###################################### - -## Load sequence tables -cat("\n..Loading sequence tables\n") -TAB <- arrow::read_parquet(SEQTAB) -setDT(TAB) -cat("... Total number of records: ", nrow(TAB), "\n") -cat("... Total number unique sequences: ", length(unique(TAB$Sequence)), "\n") -cat("... Total number unique samples (files): ", length(unique(TAB$SampleID)), "\n") - -## Load UC file for globally dereplicated sequences -cat("..Loading pooled UC file\n") -UC <- open_dataset(UCF) |> dplyr::collect() |> setDT() - -## Add OTU IDs to seq table -cat("... Adding OTU IDs to sequence table\n") -cat(".... Number of records in sequence table before merging: ", nrow(TAB), "\n") -TAB <- merge(x = TAB, y = UC, by = "SeqID", all.x = TRUE) -cat(".... Number of records in sequence table after merging: ", nrow(TAB), "\n") - -## Remove NA OTUs -- probably excluded seqs -if(any(is.na(TAB$OTU))){ - cat("WARNING: not all sequences were assigned to OTUs\n") - cat("..Removing missing/excluded sequences\n") - cat(".. ", sum(is.na(TAB$OTU)), " sequences with total abundance ", - sum(TAB[ is.na(OTU) ]$Abundance, na.rm = TRUE), " reads will be excluded\n") - TAB <- TAB[ ! is.na(OTU) ] -} - - -## Find singleton OTUs -cat("\n..Finding singleton OTUs\n") -SINGLETONS <- TAB[ , .(Abundance = sum(Abundance, na.rm = TRUE)), by = .(OTU) ][ Abundance < 2 ] -cat("... Number of singleton OTUs: ", nrow(SINGLETONS), "\n") - -## If singleton recovery is reqired -if(RECOV_SINGLET == TRUE && nrow(SINGLETONS) > 0){ - - ## Add quality scores - SINGLETONS <- merge(x = SINGLETONS, y = TAB[ , .(SeqID, MEEP)], by.x = "OTU", by.y = "SeqID", all.x = TRUE) - - ## Filter by MEEP score - SINGLETONS <- SINGLETONS[ MEEP > MAXMEEP ] - cat("... Number of singleton OTUs after filtering by MEEP score: ", nrow(SINGLETONS), "\n") - -} - -if(nrow(SINGLETONS) > 0){ - cat("..Removing singleton OTUs\n") - TAB <- TAB[ ! OTU %in% SINGLETONS$OTU ] - cat("... Number of records in sequence table after removing singleton OTUs: ", nrow(TAB), "\n") -} - - -## Summarize abundance by sample and OTU -cat("\n..Summarizing OTU abundance\n") -if(MERGE_SAMPLES == TRUE){ - - cat("\n... Merging sample replicates (e.g., re-sequenced samples)\n") - - ## Extract sample names - cat(".... Extracting sample names\n") - TAB[ , SampleName := tstrsplit(x = SampleID, split = "__", keep = 2) ] - - cat(".... Summarizing abundance by sample and OTU\n") - RES <- TAB[ , - .( Abundance = sum(Abundance, na.rm = TRUE) ), - by = c("OTU", "SampleName") ] - - setnames(x = RES, old = "SampleName", new = "SampleID") - -} else { - - cat("... Summarizing abundance by sample and OTU\n") - RES <- TAB[ , - .( Abundance = sum(Abundance, na.rm = TRUE) ), - by = c("OTU", "SampleID") ] - -} - -#### Reshape to wide table -cat("\nReshaping table into wide format\n") - -## Check if we can reshape the table in a single pass -n_otu <- length(unique(RES$OTU)) -n_smp <- length(unique(RES$SampleID)) -n_cll <- as.numeric(n_otu) * as.numeric(n_smp) -cat("...In total, there are ", n_otu, " OTUs and ", n_smp, " samples\n") -cat("...The total number of cells in the wide table will be ", n_cll, "\n") - -## Reshape data in one pass -if(n_cll < 50000000){ - REW <- dcast(data = RES, - formula = OTU ~ SampleID, - fun.aggregate = sum, fill = 0, value.var = "Abundance") -} else { -## Split data into chunks, reshape, and merge back - - cat("..The input table is too large to reshape in a single pass, reshaping by chunks\n") - - ## Function to split vector into N chunks - chunk <- function(x, n){ - if(n > 1) { res <- split(x, cut(seq_along(x), n, labels = FALSE)) } - if(n == 1){ res <- list(); res[[1]] <- x } - return(res) - } - - ## Choose the number of chunks - n_chunks <- data.table::fcase( - n_cll < 9e7, 2L, - n_cll >= 9e7 & n_cll < 5e8, 5L, - n_cll >= 5e8 & n_cll < 5e9, 6L, - n_cll >= 5e9 & n_cll < 5e10, 7L, - n_cll >= 5e10, 8L) - - cat("...The number of chunks to process: , ", n_chunks, "\n") - - ch <- chunk(x = sort(unique(RES$SampleID)), n = n_chunks) - - ## Chunk-and-reshape loop - REWL <- plyr::llply( - .data = ch, - .fun = function(x){ - - ## Reshape to wide - res <- dcast( - data = RES[ SampleID %in% x , ], - formula = OTU ~ SampleID, - fill = 0, fun.aggregate = sum, value.var = "Abundance") - - ## Create key on a data.table (should improve merging speed) - setkey(res, OTU) - - return(res) - }, - .progress = "text") - - cat("...Chunk reshaping finished\n") - cat("..Merging data into a single wide table\n") - - ## Merge chunks into a single wide table - merge_dt <- function(x,y){ data.table::merge.data.table(x, y, by = "OTU", all = TRUE) } - REW <- Reduce(f = merge_dt, x = REWL) - cat("...Merging finished\n") - - ## Clean up - cat("...Cleaning up\n") - rm(REWL); gc() - - ## Replace NAs with zeros - cat("...Filling missing values with zeros\n") - for (j in seq_len(ncol(REW))){ - set(REW, which(is.na(REW[[j]])), j, 0) - } - -} ## end of reshaping - -cat("...Reshaping to the wide format done!\n") - - -## Reorder OTU rows -cat("\n..Reordering OTU rows by total abundance\n") -otu_tots <- rowSums(REW[, -1], na.rm = TRUE) -REW <- REW[ order(otu_tots, decreasing = T), ] - -## Add attributes if samples were merged -setattr(x = RES, name = "Samples_merged", value = MERGE_SAMPLES) -setattr(x = REW, name = "Samples_merged", value = MERGE_SAMPLES) - - -cat("\nExporting results\n") - -## Export data -saveRDS.gz <- function(object, file, threads = parallel::detectCores()) { - con <- pipe(paste0("pigz -p",threads," > ",file),"wb") - saveRDS(object, file = con) - close(con) -} - -cat("..Exporting long table [R]\n") -saveRDS.gz(object = RES, - file = "OTU_table_long.RData", - threads = CPUTHREADS) - -cat("..Exporting wide table [R]\n") -saveRDS.gz(object = REW, - file = "OTU_table_wide.RData", - threads = CPUTHREADS) - -cat("..Exporting long table [tab-delimited]\n") -fwrite(x = RES, file = "OTU_table_long.txt.gz", sep = "\t", compress = "gzip") - -cat("..Exporting wide table [tab-delimited]\n") -fwrite(x = REW, file = "OTU_table_wide.txt.gz", sep = "\t", compress = "gzip") - - -cat("\nExporting OTU sequences to FASTA\n") - -cat("..Preparing sequences\n") - -## Take sequences from the data (NB! there are a several different sequence per OTU) -# SQS <- unique(RES[, .(OTU) ]) -# tmp_OTUs <- unique(TAB[ OTU %in% SQS$OTU & SeqID == OTU , .(OTU, Sequence) ]) -# SQS <- merge(x = SQS, y = tmp_OTUs, by = "OTU", all.x = TRUE) -# rm(tmp_OTUs) -# -# cat("...Preparing XStringSet object\n") -# SQF <- DNAStringSet(x = SQS$Sequence) -# names(SQF) <- SQS$OTU - -## Take sequnces from the OTU file -cat("... Loading FASTA file\n") -SQS <- readDNAStringSet(filepath = OTUS, format="fasta") -cat("... Extracting sequence IDs\n") -names(SQS) <- tstrsplit(x = names(SQS), split = ";", keep = 1)[[1]] - -if(any(duplicated(names(SQS)))){ - cat("WARNING: duplicated OTU names detected!\n") -} - -cat("... Subsetting OTUs\n") -SQF <- SQS[ names(SQS) %in% unique(REW$OTU) ] - -cat("....Total number of OTUs in input sequences: ", length(SQS), "\n") -cat("....Number of OTUs to export: ", length(SQF), "\n") -cat("....Number of OTUs in the OTU table: ", nrow(REW), "\n") - -cat("... Writing FASTA file\n") -writeXStringSet(x = SQF, - filepath = "OTUs.fa.gz", - compress = TRUE, format = "fasta", width = 9999) - - -cat("\nAll done.\n") - - -##################### Session info - -## Check time -end_time <- Sys.time() - -tmm <- as.numeric(difftime(end_time, start_time, units = "min")) -cat("\nElapsed time: ", tmm, " minutes\n") - -cat("\n") -cat("Session info:\n") -sessionInfo() -cat("\n") diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/summarize_dereplicated_data.R b/src/pipecraft-core/service_scripts/NextITS/bin/summarize_dereplicated_data.R deleted file mode 100755 index 5b1bb285..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/summarize_dereplicated_data.R +++ /dev/null @@ -1,390 +0,0 @@ -#!/usr/bin/env Rscript - -## Script to pool dereplicated sequences (non-clustered or denoised), remove low-quality data, and summarize sequence abundance per sample - -# Input: -# 1. Sequence tables in long format with de novo chimeras removed (`Seqs.parquet`) -# 2. UC file from dereplication (`UC_Pooled.parquet`) -# 3. FASTA file with sequences (`Dereplicated.fa.gz`) -# 4. Max MEEP score - -# Outputs: -# - OTU table in long format (`OTU_table_long.txt.gz` & `OTU_table_long.RData`) -# - OTU table in wide format (`OTU_table_wide.txt.gz` & `OTU_table_wide.RData`) -# - FASTA file with sequences (`OTUs.fa.gz`) - -## Usage: -# ./summarize_dereplicated_data.R \ -# --seqtab "Seqs.parquet" \ -# --uc "UC_Pooled.parquet" \ -# --seqs "Dereplicated.fa.gz" \ -# --maxmeep 0.6 \ -# --recoversinglet TRUE \ -# --mergesamples TRUE \ -# --threads 4 - - -## Quality threshold: -# MEEP score of 0.6 corresponds approximately to the average Phred score of 22.2 - -## Singleton recovery: -# If enabled, then singleton sequences with MEEP score <= 0.6 & will be preserved -# Otherwise, low-quality singleton sequences will be removed - - -############################################## Parse input parameters - -## Check time -start_time <- Sys.time() - - -cat("Parsing input options and arguments:\n") - -suppressPackageStartupMessages(require(optparse)) - -## Parse arguments -option_list <- list( - make_option("--seqtab", action="store", default=NA, type='character', help="Sequence tables in long format with de novo chimeras removed (Parquet format)"), - make_option("--uc", action="store", default=NA, type='character', help="UC file (Parquet format)"), - make_option("--seqs", action="store", default=NA, type='character', help="FASTA file with sequences"), - make_option("--maxmeep", action="store", default=0.5, type='double', help="Max MEEP score"), - make_option("--recoversinglet", action="store", default=TRUE, type='logical', help="Recover singletons"), - make_option(c("-m", "--mergesamples"), action="store", default=FALSE, type='logical', help="Merge sample replicates (default, false)"), - make_option(c("-t", "--threads"), action="store", default=4L, type='integer', help="Number of CPU threads for arrow, default 4") - # make_option(c("-s", "--scriptdir"), action="store", default=getwd(), type='character', help="Directory containing source scripts") -) -opt <- parse_args(OptionParser(option_list=option_list)) - -## Function to convert text "NA"s to NA -to_na <- function(x){ - if(x %in% c("NA", "null", "Null")){ x <- NA } - return(x) -} - -## Replaces "null"s from Nextflow with NA -opt <- lapply(X = opt, FUN = to_na) - - -## Validation of the required argiments -if(is.na(opt$seqtab)){ - cat("Input file is not specified: sequence tables in Parquet format.\n", file=stderr()) - stop() -} -if(is.na(opt$uc)){ - cat("Input file is not specified: UC file is required.\n", file=stderr()) - stop() -} -if(is.na(opt$seqs)){ - cat("Input file is not specified: FASTA file with sequences.\n", file=stderr()) - stop() -} -if(opt$recoversinglet == TRUE && is.na(opt$maxmeep)){ - cat("For singleton recovery, the max MEEP score must be specified.\n", file=stderr()) - stop() -} - -## Assign variables -SEQTAB <- opt$seqtab -UCF <- opt$uc -MAXMEEP <- as.numeric( opt$maxmeep ) -RECOV_SINGLET <- as.logical(opt$recoversinglet) -MERGE_SAMPLES <- as.logical(opt$mergesamples) -SEQS <- opt$seqs - -CPUTHREADS <- as.numeric( opt$threads ) -# SCRIPTDIR <- opt$scriptdir - -## Log assigned variables -cat(paste("Sequence tables (Parquet format): ", SEQTAB, "\n", sep="")) -cat(paste("UC file (Parquet format): ", UCF, "\n", sep="")) -cat(paste("Max MEEP score: ", MAXMEEP, "\n", sep="")) -cat(paste("Low-quality singleton recovery: ", RECOV_SINGLET, "\n", sep="")) -cat(paste("Merge sample replicates: ", MERGE_SAMPLES, "\n", sep="")) -cat(paste("Sequences: ", SEQS, "\n", sep="")) -cat(paste("Number of CPU threads to use: ", CPUTHREADS, "\n", sep="")) -# cat(paste("Directory containing source scripts: ", SCRIPTDIR, "\n", sep="")) - -cat("\n") - - - -############################################## Data for debugging - -# SEQTAB <- "Seqs.parquet" -# UCF <- "UC_Pooled.parquet" -# MAXMEEP <- 0.5 -# RECOV_SINGLET <- TRUE -# MERGE_SAMPLES <- TRUE -# SEQS <- "Dereplicated.fa.gz" -# CPUTHREADS <- 4 - - -############################################## Load packages and data - -cat("Loading R packages:\n") - -load_pckg <- function(pkg = "data.table"){ - suppressPackageStartupMessages( library(package = pkg, character.only = TRUE) ) - cat(paste(pkg, packageVersion(pkg), "\n")) -} - -load_pckg("data.table") -load_pckg("plyr") -load_pckg("metagMisc") -load_pckg("Biostrings") -load_pckg("arrow") - - -cat("\n") - - -# cat("Loading additional R funcitons...\n") -# source(file.path(SCRIPTDIR, "R_functions.R")) -# cat("\n") - - -## Set CPU thread number -cat("Setting number of CPU threads to: ", CPUTHREADS, "\n") -setDTthreads(threads = CPUTHREADS) # for data.table -set_cpu_count(CPUTHREADS) # for arrow - -###################################### -###################################### Load the data -###################################### - -## Load sequence tables -cat("\n..Loading sequence tables\n") -TAB <- arrow::read_parquet(SEQTAB) |> setDT() -cat("... Total number of records: ", nrow(TAB), "\n") -cat("... Total number unique sequences: ", length(unique(TAB$Sequence)), "\n") -cat("... Total number unique samples (files): ", length(unique(TAB$SampleID)), "\n") - -## Load UC file for globally dereplicated sequences -cat("..Loading pooled UC file\n") -UC <- open_dataset(UCF) |> dplyr::collect() |> setDT() -setnames(UC, new = c("SeqID", "DerepID")) - -## Add dereplicated IDs to seq table -cat("... Adding dereplicated IDs to sequence table\n") -cat(".... Number of records in sequence table before merging: ", nrow(TAB), "\n") -TAB <- merge(x = TAB, y = UC, by = "SeqID", all.x = TRUE) -cat(".... Number of records in sequence table after merging: ", nrow(TAB), "\n") - -## Remove NA IDs -- probably excluded seqs -if(any(is.na(TAB$DerepID))){ - cat("WARNING: not all sequences are present in the dereplicated data (could be due to length-filteing)\n") - cat("..Removing missing/excluded sequences\n") - cat(".. ", sum(is.na(TAB$DerepID)), " sequences with total abundance ", - sum(TAB[ is.na(DerepID) ]$Abundance, na.rm = TRUE), " reads will be excluded\n") - TAB <- TAB[ ! is.na(DerepID) ] -} - - -## Find singleton sequences -cat("\n..Finding singleton sequences\n") -SINGLETONS <- TAB[ , .(Abundance = sum(Abundance, na.rm = TRUE)), by = .(DerepID) ][ Abundance < 2 ] -cat("... Number of singleton sequences: ", nrow(SINGLETONS), "\n") - -## If singleton recovery is reqired -if(RECOV_SINGLET == TRUE && nrow(SINGLETONS) > 0){ - - ## Add quality scores - SINGLETONS <- merge(x = SINGLETONS, y = TAB[ , .(SeqID, MEEP)], by.x = "DerepID", by.y = "SeqID", all.x = TRUE) - - ## Filter by MEEP score - SINGLETONS <- SINGLETONS[ MEEP > MAXMEEP ] - cat("... Number of singleton sequences after filtering by MEEP score: ", nrow(SINGLETONS), "\n") - -} - -if(nrow(SINGLETONS) > 0){ - cat("..Removing singleton sequences\n") - TAB <- TAB[ ! DerepID %in% SINGLETONS$DerepID ] - cat("... Number of records in sequence table after removing singleton sequences: ", nrow(TAB), "\n") -} - - -## Summarize abundance by sample and dereplicated ID -cat("\n..Summarizing sequence abundance\n") -if(MERGE_SAMPLES == TRUE){ - - cat("\n... Merging sample replicates (e.g., re-sequenced samples)\n") - - ## Extract sample names - cat(".... Extracting sample names\n") - TAB[ , SampleName := tstrsplit(x = SampleID, split = "__", keep = 2) ] - - cat(".... Summarizing abundance by sample and OTU\n") - RES <- TAB[ , - .( Abundance = sum(Abundance, na.rm = TRUE) ), - by = c("DerepID", "SampleName") ] - - setnames(x = RES, old = "SampleName", new = "SampleID") - -} else { - - cat("... Summarizing abundance by sample and dereplicated ID\n") - RES <- TAB[ , - .( Abundance = sum(Abundance, na.rm = TRUE) ), - by = c("DerepID", "SampleID") ] - -} - -#### Reshape to wide table -cat("\nReshaping table into wide format\n") - -## Check if we can reshape the table in a single pass -n_seq <- length(unique(RES$DerepID)) -n_smp <- length(unique(RES$SampleID)) -n_cll <- as.numeric(n_seq) * as.numeric(n_smp) -cat("...In total, there are ", n_seq, " sequences and ", n_smp, " samples\n") -cat("...The total number of cells in the wide table will be ", n_cll, "\n") - -## Reshape data in one pass -if(n_cll < 50000000){ - REW <- dcast(data = RES, - formula = DerepID ~ SampleID, - fun.aggregate = sum, fill = 0, value.var = "Abundance") -} else { -## Split data into chunks, reshape, and merge back - - cat("..The input table is too large to reshape in a single pass, reshaping by chunks\n") - - ## Function to split vector into N chunks - chunk <- function(x, n){ - if(n > 1) { res <- split(x, cut(seq_along(x), n, labels = FALSE)) } - if(n == 1){ res <- list(); res[[1]] <- x } - return(res) - } - - ## Choose the number of chunks - n_chunks <- data.table::fcase( - n_cll < 9e7, 2L, - n_cll >= 9e7 & n_cll < 5e8, 5L, - n_cll >= 5e8 & n_cll < 5e9, 6L, - n_cll >= 5e9 & n_cll < 5e10, 7L, - n_cll >= 5e10, 8L) - - cat("...The number of chunks to process: , ", n_chunks, "\n") - - ch <- chunk(x = sort(unique(RES$SampleID)), n = n_chunks) - - ## Chunk-and-reshape loop - REWL <- plyr::llply( - .data = ch, - .fun = function(x){ - - ## Reshape to wide - res <- dcast( - data = RES[ SampleID %in% x , ], - formula = DerepID ~ SampleID, - fill = 0, fun.aggregate = sum, value.var = "Abundance") - - ## Create key on a data.table (should improve merging speed) - setkey(res, DerepID) - - return(res) - }, - .progress = "text") - - cat("...Chunk reshaping finished\n") - cat("..Merging data into a single wide table\n") - - ## Merge chunks into a single wide table - merge_dt <- function(x,y){ data.table::merge.data.table(x, y, by = "DerepID", all = TRUE) } - REW <- Reduce(f = merge_dt, x = REWL) - cat("...Merging finished\n") - - ## Clean up - cat("...Cleaning up\n") - rm(REWL); gc() - - ## Replace NAs with zeros - cat("...Filling missing values with zeros\n") - for (j in seq_len(ncol(REW))){ - set(REW, which(is.na(REW[[j]])), j, 0) - } - -} ## end of reshaping - -cat("...Reshaping to the wide format done!\n") - - -## Reorder OTU rows -cat("\n..Reordering OTU rows by total abundance\n") -otu_tots <- rowSums(REW[, -1], na.rm = TRUE) -REW <- REW[ order(otu_tots, decreasing = T), ] - -## Add attributes if samples were merged -setattr(x = RES, name = "Samples_merged", value = MERGE_SAMPLES) -setattr(x = REW, name = "Samples_merged", value = MERGE_SAMPLES) - - -cat("\nExporting results\n") - -## Export data -saveRDS.gz <- function(object, file, threads = parallel::detectCores()) { - con <- pipe(paste0("pigz -p",threads," > ",file),"wb") - saveRDS(object, file = con) - close(con) -} - -cat("..Exporting long table [R]\n") -saveRDS.gz(object = RES, - file = "OTU_table_long.RData", - threads = CPUTHREADS) - -cat("..Exporting wide table [R]\n") -saveRDS.gz(object = REW, - file = "OTU_table_wide.RData", - threads = CPUTHREADS) - -cat("..Exporting long table [tab-delimited]\n") -fwrite(x = RES, file = "OTU_table_long.txt.gz", sep = "\t", compress = "gzip") - -cat("..Exporting wide table [tab-delimited]\n") -fwrite(x = REW, file = "OTU_table_wide.txt.gz", sep = "\t", compress = "gzip") - - -cat("\nExporting sequences to FASTA\n") - -cat("..Preparing sequences\n") - -## Take sequnces from the FASTA file -cat("... Loading FASTA file\n") -SQS <- readDNAStringSet(filepath = SEQS, format="fasta") -cat("... Extracting sequence IDs\n") -names(SQS) <- tstrsplit(x = names(SQS), split = ";", keep = 1)[[1]] - -if(any(duplicated(names(SQS)))){ - cat("WARNING: duplicated OTU names detected!\n") -} - -cat("... Subsetting OTUs\n") -SQF <- SQS[ names(SQS) %in% unique(REW$DerepID) ] - -cat("....Total number of sequences in input FASTA: ", length(SQS), "\n") -cat("....Number of sequences to export: ", length(SQF), "\n") -cat("....Number of sequences in the abundance table: ", nrow(REW), "\n") - -cat("... Writing FASTA file\n") -writeXStringSet(x = SQF, - filepath = "OTUs.fa.gz", - compress = TRUE, format = "fasta", width = 9999) - - -cat("\nAll done.\n") - - -##################### Session info - -## Check time -end_time <- Sys.time() - -tmm <- as.numeric(difftime(end_time, start_time, units = "min")) -cat("\nElapsed time: ", tmm, " minutes\n") - -cat("\n") -cat("Session info:\n") -sessionInfo() -cat("\n") diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/tag_jump_removal.R b/src/pipecraft-core/service_scripts/NextITS/bin/tag_jump_removal.R deleted file mode 100755 index c6fb3e56..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/tag_jump_removal.R +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/env Rscript - -## Script to perform tag-jump removal - -# Input is given as positional arguments: -# 1. OTU table (`OTU_tab_not_filtered.txt.gz`) -# 2. f-parameter of UNCROSS (e.g., 0.01) -# 3. p-parameter (e.g., 1.0) - -# Outputs: -# - Tag-jumpfiltered OTU table (`OTU_tab_TagJumpFiltered.txt.gz`) -# - Table with tag-jumps (`TagJump_OTUs.RData`) -# - Plot (`TagJump_plot.pdf`) - -args <- commandArgs(trailingOnly = TRUE) - -suppressMessages(library(data.table)) -suppressMessages(library(ggplot2)) -# library(openxlsx) - -theme_set(theme_classic(base_size = 14)) - -## Load OTU table -cat("..Loading OTU table\n") -OTUTABW <- fread( - file = args[1], - sep = "\t", header = TRUE) - -colnames(OTUTABW)[1] <- "OTU" - -cat("...Number of OTUs: ", nrow(OTUTABW), "\n") -cat("...Number of samples: ", ncol(OTUTABW) - 1, "\n") - -## Convert to long format -cat("..Converting OTU table to long format\n") -OTUTAB <- melt(data = OTUTABW, id.vars = "OTU", - variable.name = "SampleID", value.name = "Abundance") - -## Remove zero-OTUs -OTUTAB <- OTUTAB[ Abundance > 0 ] -cat("...Number of non-zero records: ", nrow(OTUTAB), "\n") - - -## Estimate total abundance of sequence per plate -cat("..Estimating total OTU abundance\n") -OTUTAB[ , Total := sum(Abundance, na.rm = TRUE), by = "OTU" ] - -## UNCROSS score (with original parameter - take a root from the exp in denominator, to make curves more steep) -uncross_score <- function(x, N, n, f = 0.01, tmin = 0.1, p = 1){ - # x = OTU abundance in a sample - # N = total OTU abundance - # n = number of samples - # f = expected cross-talk rate, e.g. 0.01 - # tmin = min score to be considered as cross-talk - # p = power to rise the exponent (default, 1; use 1/2 or 1/3 to make cureves more stepp) - - z <- f * N / n # Expected treshold - sc <- 2 / (1 + exp(x/z)^p) # t-score - res <- data.table(Score = sc, TagJump = sc >= tmin) - return(res) -} - -## Esimate UNCROSS score -cat("..Estimating UNCROSS score\n") -OTUTAB <- cbind( - OTUTAB, - uncross_score( - x = OTUTAB$Abundance, - N = OTUTAB$Total, - n = length(unique(OTUTAB$SampleID)), - f = as.numeric(args[2]), - p = as.numeric(args[3]) - ) - ) - -## Truncate singletons with total OTU abundance > 99 reads -# OTUTAB[ Abundance == 1 & Total > 99 , TagJump := TRUE ] -# OTUTAB[ Abundance == 2 & Total > 999 , TagJump := TRUE ] - -cat("...Number of tag-jumps: ", sum(OTUTAB$TagJump, na.rm = TRUE), "\n") - - -## Plot -cat("..Making a plot\n") -PP <- ggplot(data = OTUTAB, aes(x = Total, y = Abundance, color = TagJump)) + - geom_point() + scale_x_log10() + scale_y_log10() + - scale_color_manual(values = c("#0C7C59", "#D64933")) + - labs(x = "Total abundance of OTU, reads", y = "Abundance of OTU in a sample, reads") - -cat("..Exporting a plot\n") -pdf(file = "TagJump_plot.pdf", width = 12, height = 9.5, useDingbats = FALSE) - PP -dev.off() - - -## TJ stats -cat("..Calculating tag-jump summary\n") -TJ <- data.table( - Total_reads = sum(OTUTAB$Abundance), - Number_of_TagJump_Events = sum(OTUTAB$TagJump), - TagJump_reads = sum(OTUTAB[ TagJump == TRUE ]$Abundance, na.rm = T) - ) - -TJ$ReadPercent_removed <- with(TJ, (TagJump_reads / Total_reads * 100)) - -fwrite(x = TJ, file = "TagJump_stats.txt", sep = "\t") - - -## Exporting tag-jump data -cat("..Exporting tag-jump data\n") -JMPS <- OTUTAB[ TagJump == TRUE, .(OTU, SampleID) ] - -saveRDS(object = JMPS, - file = "TagJump_OTUs.RData", - compress = "xz") - - -## Prepare OTU tables, remove tag-jumps -cat("..Removing tag-jumps\n") - -OTUTAB <- OTUTAB[ TagJump == FALSE ] - -## Convert to wide format -RES <- dcast(data = OTUTAB, - formula = OTU ~ SampleID, - value.var = "Abundance", fill = 0) - -## Sort rows (by total abundance) -clz <- colnames(RES)[-1] -otu_sums <- rowSums(RES[, ..clz], na.rm = TRUE) -RES <- RES[ order(otu_sums, decreasing = TRUE) ] - - -cat("..Exporting tag-jump filtered table\n") - -fwrite(x = RES, - file = "OTU_tab_TagJumpFiltered.txt.gz", - sep = "\t", compress = "gzip") diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/tag_jump_removal_longtab.R b/src/pipecraft-core/service_scripts/NextITS/bin/tag_jump_removal_longtab.R deleted file mode 100755 index c7bd52ee..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/tag_jump_removal_longtab.R +++ /dev/null @@ -1,247 +0,0 @@ -#!/usr/bin/env Rscript - -## Script to perform tag-jump removal - -# Input arguments: -# 1. Sequence table in long format, no header (`Seq_tab_not_filtered.txt.gz`), -# with columns: `SeqID`, `Abundance`, `SampleID` -# 2. Dereplicated or pre-clustered membership table (`TJPreclust.uc.parquet`) -# 2. f-parameter of UNCROSS (e.g., 0.01) -# 3. p-parameter (e.g., 1.0) - -# Outputs: -# - Tag-jump-filtered sequence table (`Seq_tab_TagJumpFiltered.txt.gz`) -# - Table with tag-jump scores (`TagJump_scores.qs`) -# - Plot (`TagJump_plot.pdf`) - -cat("\nParsing input options and arguments...\n") - -suppressPackageStartupMessages(require(optparse)) - -## Parse arguments -option_list <- list( - make_option(c("-s", "--seqtab"), action="store", default="seqtab.txt.gz", type='character', help="Sequence table in long format"), - make_option(c("-c", "--precls"), action="store", default="precls.txt.gz", type='character', help="Table with pre-clustered sequence membership"), - make_option(c("-f", "--uncross_f"), action="store", default=0.01, type='numeric', help="f-parameter of UNCROSS"), - make_option(c("-p", "--uncross_p"), action="store", default=1, type='numeric', help="Additional p-parameter for UNCROSS") -) -opt <- parse_args(OptionParser(option_list=option_list)) - -## Function to convert text "NA"s to NA -to_na <- function(x){ - if(x %in% c("NA", "null", "Null")){ x <- NA } - return(x) -} - -## Replaces "null"s from Nextflow with NA -opt <- lapply(X = opt, FUN = to_na) - -## Validation of the required arguments -if(is.na(opt$seqtab)){ - stop("Input file with sequence table is not specified\n") -} -if(is.na(opt$precls)){ - stop("Input file with pre-clustered membership table is not specified\n") -} - -## Set default params if not specified -if(is.na(opt$uncross_f) | is.null(opt$uncross_f) | is.nan(opt$uncross_f) | !is.numeric(opt$uncross_f)){ - cat("f-parameter is not specified, using default value: 0.01\n") - opt$uncross_f <- 0.01 -} -if(is.na(opt$uncross_p) | is.null(opt$uncross_p) | is.nan(opt$uncross_p) | !is.numeric(opt$uncross_p)){ - cat("p-parameter is not specified, using default value: 1\n") - opt$uncross_p <- 1 -} - -## Assign variables -SEQTAB <- opt$seqtab -PRECLS <- opt$precls -F <- opt$uncross_f -P <- opt$uncross_p - -## Log assigned variables -cat("\nParameters specified:\n") -cat(paste("Sequence table: " , SEQTAB, "\n", sep = "")) -cat(paste("Pre-clustered membership table: ", PRECLS, "\n", sep = "")) -cat(paste("f-parameter of UNCROSS: ", F, "\n", sep = "")) -cat(paste("p-parameter of UNCROSS: ", P, "\n", sep = "")) - -cat("\n") - - -############################################## Data for debugging - -# SEQTAB <- "Seq_tab_not_filtered.txt.gz" -# PRECLS <- "TJPreclust.uc.parquet" -# F <- 0.01 -# P <- 1 - - -############################################## Load packages - -cat("Loading R packages...\n") - -load_pckg <- function(pkg = "data.table"){ - suppressPackageStartupMessages( library(package = pkg, character.only = TRUE) ) - cat(paste(pkg, packageVersion(pkg), "\n")) -} - -load_pckg("data.table") -load_pckg("arrow") -load_pckg("ggplot2") -# load_pckg("qs") - -theme_set(theme_classic(base_size = 14)) - -cat("\n") - -############################################## Workflow - -## Load sequence table -cat("..Loading sequence table\n") -SEQTAB <- fread(file = SEQTAB, - sep = "\t", header = TRUE) # "SeqID", "SampleID", "Abundance" - -## Load sequence membership table -cat("..Loading sequence membership table\n") -PRECLS <- read_parquet(file = PRECLS) -setDT(PRECLS) -setnames(PRECLS, new = c("SeqID", "OTU")) - -## Remove ambiguous mappings (should not happen, but just in case) -if(any(duplicated(PRECLS$SeqID))){ - cat("WARNING: ambiguous mapping of sequences in membership table detected - excluding duplicates\n") - PRECLS <- unique(PRECLS, by = "SeqID") -} - -## Add cluster membership to the sequence table -SEQTAB <- merge(x = SEQTAB, y = PRECLS, by = "SeqID", all.x = TRUE) - -if(any(is.na(SEQTAB$OTU))){ - cat("WARNING: Sequences without cluster membership detected\n") - cat("WARNING: Excluding these records from the analysis\n") - SEQTAB <- SEQTAB[ !is.na(OTU) ] -} - -cat("...Number of unique sequences: ", length(unique(SEQTAB$SeqID)), "\n") -cat("...Number of clusters: ", length(unique(SEQTAB$OTU)), "\n") -cat("...Number of samples: ", length(unique(SEQTAB$SampleID)), "\n") - -## Summarize by sequence clusters -cat("..Summarizing by sequence clusters\n") -OTUTAB <- SEQTAB[ , .(Abundance = sum(Abundance, na.rm = TRUE)), by = c("OTU", "SampleID") ] - -## Estimate total abundance of sequence per plate -cat("..Estimating total OTU abundance\n") -OTUTAB[ , Total := sum(Abundance, na.rm = TRUE), by = "OTU" ] - -## UNCROSS score (with original parameter - take a root from the exp in denominator, to make curves more steep) -uncross_score <- function(x, N, n, f = 0.01, tmin = 0.1, p = 1){ - # x = OTU abundance in a sample - # N = total OTU abundance - # n = number of samples - # f = expected cross-talk rate, e.g. 0.01 - # tmin = min score to be considered as cross-talk - # p = power to rise the exponent (default, 1; use 1/2 or 1/3 to make cureves more stepp) - - z <- f * N / n # Expected treshold - sc <- 2 / (1 + exp(x/z)^p) # t-score - res <- data.table(Score = sc, TagJump = sc >= tmin) - return(res) -} - -## Esimate UNCROSS score -cat("..Estimating UNCROSS score\n") -OTUTAB <- cbind( - OTUTAB, - uncross_score( - x = OTUTAB$Abundance, - N = OTUTAB$Total, - n = length(unique(OTUTAB$SampleID)), - f = as.numeric(F), - p = as.numeric(P) - ) - ) - -## Truncate singletons with total OTU abundance > 99 reads -# OTUTAB[ Abundance == 1 & Total > 99 , TagJump := TRUE ] -# OTUTAB[ Abundance == 2 & Total > 999 , TagJump := TRUE ] - -cat("...Number of tag-jumps: ", sum(OTUTAB$TagJump, na.rm = TRUE), "\n") - -## Export tag-jump scores -setcolorder(OTUTAB, - c("OTU", "SampleID", "TagJump", "Score", "Abundance", "Total")) - -setorder(OTUTAB, OTU, -Abundance, SampleID) - -cat("..Exporting tag-jump scores\n") -qs::qsave(OTUTAB, - "TagJump_scores.qs", - preset = "custom", algorithm = "zstd", compress_level = 5L, nthreads = 1L) - - -## Plot -cat("..Making a plot\n") -PP <- ggplot(data = OTUTAB, aes(x = Total, y = Abundance, color = TagJump)) + - geom_point() + scale_x_log10() + scale_y_log10() + - scale_color_manual(values = c("#0C7C59", "#D64933")) + - labs(x = "Total abundance of OTU, reads", y = "Abundance of OTU in a sample, reads") - -cat("..Exporting a plot\n") -pdf(file = "TagJump_plot.pdf", width = 12, height = 9.5, useDingbats = FALSE) - PP -dev.off() - - - -## Exporting tag-jump data -# cat("..Exporting tag-jump data\n") -# JMPS <- OTUTAB[ TagJump == TRUE, .(OTU, SampleID) ] -# -# saveRDS(object = JMPS, -# file = "TagJump_OTUs.RData", -# compress = "xz") - - -## Prepare filtered sequence table, remove tag-jumps -cat("..Removing tag-jumps\n") - -## Add tag-jump info to the sequence table -n1 <- nrow(SEQTAB) - -RES <- merge( - x = SEQTAB, - y = OTUTAB[ , .(OTU, SampleID, TagJump) ], - by = c("OTU", "SampleID"), all.x = TRUE) - -n2 <- nrow(RES) -if(n1 != n2){ - cat("WARNING: merging went wrong likely\n") - cat("WARNING: There might be duplicated sequences\n") -} - - -## TJ stats -cat("..Calculating tag-jump summary\n") -TJ <- data.table( - Total_reads = sum(RES$Abundance), - Number_of_TagJump_Events = sum(RES$TagJump), - TagJump_reads = sum(RES[ TagJump == TRUE ]$Abundance, na.rm = T) - ) - -TJ$ReadPercent_removed <- with(TJ, (TagJump_reads / Total_reads * 100)) - -fwrite(x = TJ, file = "TagJump_stats.txt", sep = "\t") - - -## Keep only non-tag-jump reads -RES <- RES[ TagJump == FALSE , .(SampleID, SeqID, Abundance) ] -setorder(RES, SampleID, -Abundance) - -cat("..Exporting tag-jump filtered table\n") - -fwrite(x = RES, - file = "Seq_tab_TagJumpFiltered.txt.gz", - sep = "\t", compress = "gzip") diff --git a/src/pipecraft-core/service_scripts/NextITS/bin/validate_tags.R b/src/pipecraft-core/service_scripts/NextITS/bin/validate_tags.R deleted file mode 100755 index 6cb4d4ee..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/bin/validate_tags.R +++ /dev/null @@ -1,488 +0,0 @@ -#!/usr/bin/env Rscript - -## Script to validate tags (barcodes) used during sample multiplexing -## - Tags should be unique -## - Tag names should be unique -## - Tag names must be alphanumeric (ASCII-only) and must not contain whitespace, dot, comma, semicolon, or dash -## - Sequencing run ID could be present in tag names (before double underscore) -## - Checks the presence of positive and negative controls -## - Estimates number of unqiue tags and their length -## - For dual assymetric tags, -## unique barcodes are converted into a "long" format, -## a biosample tables (`biosamples_asym.csv` and `biosamples_sym.csv`), -## file naming scheme (`file_renaming.tsv`), -## and `unknown_combinations.tsv` are exported as well - -## Usage: -# validate_tags.R \ -# --tags tags.fasta \ -# --output tags_validated.fasta - - - -cat("Parsing input options and arguments...\n") - -suppressPackageStartupMessages(require(optparse)) - -## Parse arguments -option_list <- list( - make_option("--tags", action="store", default=NA, type='character', help="FASTA file with tags"), - make_option("--output", action="store", default=NA, type='character', help="FASTA file with validated tags") -) -opt <- parse_args(OptionParser(option_list=option_list)) - - -## Validation of the required argiments -if(is.na(opt$tags)){ - cat("Input file is not specified!\n", file=stderr()) - stop() -} -if(is.na(opt$output)){ - cat("Output file is not specified!\n", file=stderr()) - stop() -} - -## Assign variables -TAGS <- opt$tags -OUTP <- opt$output - -## Log assigned variables -cat(paste("FASTA file with tags: ", TAGS, "\n", sep="")) -cat(paste("Output file with validated tags: ", OUTP, "\n", sep="")) - -cat("\n") - - - -############## - -cat("Loading R packages...\n") - -load_pckg <- function(pkg = "data.table"){ - suppressPackageStartupMessages( library(package = pkg, character.only = TRUE) ) - cat(paste(pkg, packageVersion(pkg), "\n")) -} - -load_pckg("data.table") -load_pckg("Biostrings") - -cat("\n") - - -## Load FASTA sequences -cat("..Loading sequence tables\n") -TAGS <- try( readDNAStringSet(filepath = TAGS, format="fasta") ) - -if("try-error" %in% class(TAGS)){ - cat("Error in reading FASTA file!\n", file=stderr()) - stop(TAGS) -} - -cat("Number of records in the file: ", length(TAGS), "\n") - - -######################## -######################## Validate sample names -######################## - -cat("\n\n===== Validating sample names =====\n\n") - -cat("Positive control: ", - ifelse(test = any(grepl(pattern = "PosC", x = names(TAGS))), - yes = "Present", no = "Absent"), "\n") - -cat("Negative control: ", - ifelse(test = any(grepl(pattern = "NegC", x = names(TAGS))), - yes = "Present", no = "Absent"), "\n") - -## Validate names -cat("\nValidating tag names\n") -newnames <- names(TAGS) - -cat("..Replacing leading and trailing spaces and tabs\n") -newnames <- trimws(x = newnames, which = "both") - -cat("..Replacing duplicated spaces, dashes, dots, commas, or semicolons\n") -newnames <- gsub(pattern = "\\s+", replacement = " ", x = newnames) -newnames <- gsub(pattern = "\\-+", replacement = "-", x = newnames) -newnames <- gsub(pattern = "\\.+", replacement = ".", x = newnames) -newnames <- gsub(pattern = ",+", replacement = ",", x = newnames) -newnames <- gsub(pattern = ";+", replacement = ";", x = newnames) - -cat("..Replacing disallowed symbols\n") -newnames <- iconv(newnames, from = "UTF-8", to = "ASCII//TRANSLIT") -newnames <- gsub(pattern = "[^[:alnum:]]", replacement = "_", x = newnames) - -cat("..Replacing the second occurrence of double underscore\n") -## need something like `sed 's/__/_/2g'` -newnames <- sub(pattern = "__", replacement = "TEMPTEMPTEMPTEMP", x = newnames) -newnames <- gsub(pattern = "_+", replacement = "_", x = newnames) -newnames <- sub(pattern = "TEMPTEMPTEMPTEMP", replacement = "__", x = newnames) - -## test: c("A__B", "B__C__D", "E__F__G__H", "A___3", "B___4___E") - - -## Find out which names were changed -renamed <- data.table( - OriginalName = names(TAGS), - NewName = newnames) - -renamed[ , Renamed := OriginalName != NewName ] -renamed <- renamed[ Renamed == TRUE ] - -if(nrow(renamed) > 0){ - cat("..The following tag names were corrected:\n") - print( - renamed[, .(OriginalName, NewName)], - nrows = nrow(renamed), trunc.cols = FALSE) - - cat("...Exporting renamed tag names\n") - fwrite(x = renamed[ , .(OriginalName, NewName)], - file = "tag_names_renamed.tsv", quote = FALSE, sep = "\t", col.names = FALSE) -} - -names(TAGS) <- newnames - - -## Check tag name uniqness -nuniq <- length(unique(names(TAGS))) -cat("\nAll tag names unique: ", - ifelse(test = (length(TAGS) == nuniq), - yes = "TRUE", no = "FALSE"), "\n") - -if(length(TAGS) != nuniq){ - cat("..Not all tag names are unique!\n") - cat("..Resolving tag name uniqness by adding sequential numbers to non-unique names\n") - - dups <- unique(names(TAGS)[ which(duplicated(names(TAGS))) ]) - cat("..Number of duplicates: ", length(dups), "\n") - cat("..Duplicated names: ", paste(dups, collapse = ", "), "\n") - - dtt <- data.table(ID = 1:length(TAGS), TagName = names(TAGS)) - dtt[ , Duplicated := TagName %in% dups ] - dtt[ , NewName := TagName ] - dtt[ - Duplicated == TRUE, - NewName := paste0(TagName, "_", 1:.N), - by = "TagName" ] - setorder(x = dtt, ID) - - names(TAGS) <- dtt$NewName - - rm(dtt) -} - - -## Check run name -TESTRUN <- grepl(pattern = "__", x = names(TAGS)) - -cat("\nTag names contain sequencing run ID: ", - ifelse(test = any(TESTRUN), - yes = "TRUE", no = "FALSE"), "\n") - -if(any(TESTRUN)){ - - ## Check that all samples contain RunID - cat("\nAll samples contain sequencing run ID: ", - ifelse(test = sum(TESTRUN) == length(names(TAGS)), - yes = "TRUE", no = "FALSE"), "\n") - - ## Check run name uniqness - dtt <- data.table(TagName = names(TAGS)) - dtt[ , c("RunID", "SampleID") := tstrsplit(x = TagName, split = "__", keep = 1:2) ] - - cat("Number of run IDs in tag names (ideally, should be = 1): ", length(unique(dtt$RunID)), "\n") - cat("Run IDs detected: ", paste(unique(dtt$RunID), collapse = ", "), "\n") - - rm(dtt) -} - - -######################## -######################## Validate sequences -######################## - -cat("\n\n===== Validating sequences =====\n\n") - - -DUAL <- grepl(pattern = "\\.\\.\\.", x = as.character(TAGS)) - -if(any(DUAL)){ - cat("Barcode type detected: Dual\n") - - if(any(!DUAL)){ - cat("WARNING: mixture of single and dual tags detected!\n") - print(names(TAGS)[ !DUAL ]) - stop("\nPlease fix the tag sequences (remove single tags or add double dots to dual tags)!\n") - } - -} else { - cat("Barcode type detected: Single (or dual symmetric)\n") -} - - -##### Single tag - -if(any(DUAL) == FALSE){ - - cat("Tag length: ", paste(unique(width(TAGS)), collapse = ", "), "\n") - - suniq <- length(unique(as.character(TAGS))) - cat("\nAll tag sequences unique: ", - ifelse(test = (length(TAGS) == suniq), - yes = "TRUE", no = "FALSE"), "\n") - - if(length(TAGS) != suniq){ - cat("..Not all tag sequences are unique!\n") - cat("..This should be resolved manually!\n") - - dup_name <- unique( names(TAGS)[ duplicated(as.character(TAGS)) ]) - dup_tags <- as.character(TAGS[ dup_name ]) - - cat("..Number of duplicated tags: ", length(dup_name), "\n") - - dupss <- TAGS[ TAGS %in% dup_tags ] - dups <- data.table( - TagNames = names(dupss), - Tags = as.character(dupss)) - - dup_smr <- dups[ , .( - TagNames = paste0("[ ", paste(TagNames, collapse = ", "), " ]") - ), - by = "Tags"] - - cat("..Duplicates: \n") - print(dup_smr, nrows = length(TAGS), trunc.cols = FALSE) - - stop("\nPlease fix the tag sequences!\n") - } - - ## Export FASTA - cat("Exporting validated tags in FASTA format\n") - - writeXStringSet( - x = TAGS, - filepath = OUTP, - compress = FALSE, - format = "fasta", - width = 9999) - -} # end of single tag - - - -##### Dual tags - -if(any(DUAL) == TRUE){ - - ## Convert to tabular format - dtt <- data.table( - SampleID = names(TAGS), - Tags = as.character(TAGS)) - - ## Split dual tags - dtt[ , c("Tag1", "Tag2") := tstrsplit(x = Tags, split = "\\.\\.\\.", keep = c(1,2)) ] - - ## Check if there are any missing tags - missing_tags <- dtt[ is.na(Tag1) | is.na(Tag2) ] - if(nrow(missing_tags) > 0){ - cat("WARNING: missing dual tags detected!\n") - print(missing_tags) - stop("\nPlease fix the tag sequences!\n") - } - - cat("..Forward tag length: ", paste(sort(unique(nchar(dtt$Tag1))), collapse = ", "), "\n") - cat("..Reverse tag length: ", paste(sort(unique(nchar(dtt$Tag2))), collapse = ", "), "\n") - - cat("\n") - cat("..Number of unique forward tags: ", length(unique(dtt$Tag1)), "\n") - cat("..Number of unique reverse tags: ", length(unique(dtt$Tag2)), "\n") - - ## Find unique barcodes - bu <- data.table(Sequence = unique(c(dtt$Tag1, dtt$Tag2))) - - ## Name unique barcodes - len <- nchar(nrow(bu)) - bu[ , ID := .I ] - bu[ , ID := sprintf(paste("%0", len, "d", sep = ""), ID) ] - bu[ , ID := paste0("bc", ID) ] - - ## Convert to FASTA - seqs <- DNAStringSet(x = bu$Sequence) - names(seqs) <- bu$ID - - - - ## Add bacrode IDs - dtt <- merge(x = dtt, y = bu, by.x = "Tag1", by.y = "Sequence", all.x = TRUE) - setnames(x = dtt, old = "ID", new = "ID1") - - dtt <- merge(x = dtt, y = bu, by.x = "Tag2", by.y = "Sequence", all.x = TRUE) - setnames(x = dtt, old = "ID", new = "ID2") - - dtt[ , Barcodes := paste0(ID1, "--", ID2)] - - - dtt[ , TagSymmetry := fifelse(Tag1 == Tag2, yes = "symmetric", no = "asymmetric", na = NA) ] - cat("Number of symmetric tag combinations: ", sum(dtt$TagSymmetry %in% "symmetric"), "\n") - cat("Number of asymmetric tag combinations: ", sum(dtt$TagSymmetry %in% "asymmetric"), "\n") - - ## Validate barcode combination uniqness - if(nrow(dtt) != length(unique(dtt$Barcodes))){ - cat("WARNING: non-unique barcode combination detected!\n") - - dups <- dtt$Barcodes[ duplicated(dtt$Barcodes) ] - print( dtt[ Barcodes %in% dups ] ) - - stop("\nPlease fix the tag sequences!\n") - } - - - ## Find unique barcode combinations (taking into account reverse complements) - dtt[, tag_pair_unordered := paste( - pmin(Tag1, Tag2), - pmax(Tag1, Tag2), - sep="|") ] - - dtt[, tag_pair_ordered := paste(Tag1, Tag2, sep="|") ] - - ## Swapped-pair ambiguity: X-Y exists AND Y-X exists (cannot disambiguate if you don't know direction) - swapped <- dtt[, .( - n_samples = .N, - n_ordered = uniqueN(tag_pair_ordered), - samples = paste(sort(SampleID), collapse=", "), - ordered_set = paste(sort(unique(tag_pair_ordered)), collapse=", ") - ), by = tag_pair_unordered][ n_samples > 1 ] - - if(nrow(swapped) > 0){ - cat("\nWARNING: swapped-pair tag ambiguity detected!\n") - cat("..Number of swapped-pair tag combinations: ", nrow(swapped), "\n") - cat("..Swapped-pair tag combinations: ", "\n") - print(swapped[ , .(samples, tag_pair_unordered) ]) - stop("\nIt is impossible to assign sample ID to sequences with swapped-pair tag combinations!\nPlease fix the tag sequences (e.g., combine primer sequence with the tag sequence!\n") - } - - - ## Prepare biosample table for LIMA - # https://lima.how/faq/biosample.html - cat("\nExporting biosample tables: 'biosamples_sym.csv' and 'biosamples_asym.csv'\n") - - res <- data.table( - Barcodes = dtt$Barcodes, - `Bio Sample` = dtt$SampleID, - TagSymmetry = dtt$TagSymmetry) - - setorder(x = res, `Bio Sample`) - - fwrite( - x = res[ TagSymmetry %in% "symmetric", .(Barcodes, `Bio Sample`) ] , - file = "biosamples_sym.csv", - quote = FALSE, sep = ",", col.names = TRUE) - - fwrite( - x = res[ TagSymmetry %in% "asymmetric", .(Barcodes, `Bio Sample`) ] , - file = "biosamples_asym.csv", - quote = FALSE, sep = ",", col.names = TRUE) - - - ## File for sample renaming - res[ , OldName := paste0("lima.", Barcodes, ".fq.gz") ] - res[ , NewName := paste0(`Bio Sample`, ".fq.gz") ] - - ## The order of tags can be different in the FASTQ file names - ## Ensure that we keep track of both options (x--y and y--x) - tmp <- copy(res) - tmp[ , c("ID1", "ID2") := tstrsplit(x = Barcodes, split = "--", keep = 1:2) ] - tmp[ , Barcodes := paste0(ID2, "--", ID1) ] - tmp[ , OldName := paste0("lima.", Barcodes, ".fq.gz") ] - tmp[ , ID1 := NULL ] - tmp[ , ID2 := NULL ] - - res <- rbind(res, tmp) - rm(tmp) - res <- unique(res, by = "OldName") - setorder(x = res, Barcodes) - - cat("Exporting file naming scheme: 'file_renaming.tsv'\n") - - fwrite(x = res[ , .(OldName, NewName)], - file = "file_renaming.tsv", quote = F, sep = "\t", col.names = FALSE) - - ## Export unique barcodes - cat("Exporting unique tags in FASTA format\n") - - writeXStringSet( - x = seqs, - filepath = OUTP, - compress = FALSE, - format = "fasta", - width = 9999) - - - ## Prepare unknown combinations - cat("Preparing unknown tag combinations\n") - - UNKN <- CJ( - Tag1 = unique(dtt$Tag1), - Tag2 = unique(dtt$Tag2)) - - UNKN <- merge(x = UNKN, y = bu, by.x = "Tag1", by.y = "Sequence", all.x = TRUE) - setnames(x = UNKN, old = "ID", new = "ID1") - - UNKN <- merge(x = UNKN, y = bu, by.x = "Tag2", by.y = "Sequence", all.x = TRUE) - setnames(x = UNKN, old = "ID", new = "ID2") - - ## Trying to parse RunID from the first sample - if(any(TESTRUN)){ - cat("WARNING: in assumption that there is a single sequencing run, RunID of the first sample will be used!\n") - RUNID <- tstrsplit(dtt$SampleID[1], split = "__", keep = 1)[[1]] - if(is.na(RUNID)){ - cat("WARNING: RunID is not found in the sample name\n") - RUNID <- "unknown" - } - } else { - RUNID <- "unknown" - } - - UNKN1 <- copy(UNKN) - UNKN1[ , IDS := paste0(ID1, "--", ID2) ] - UNKN1[ , OldName := paste0("lima.", IDS, ".fq.gz") ] - UNKN1[ , Barcodes := paste0(Tag1, "_", Tag2) ] - UNKN1[ , NewName := paste0(RUNID, "__", Barcodes, ".fq.gz") ] - - UNKN2 <- copy(UNKN) - UNKN2[ , IDS := paste0(ID2, "--", ID1) ] - UNKN2[ , OldName := paste0("lima.", IDS, ".fq.gz") ] - UNKN2[ , Barcodes := paste0(Tag2, "_", Tag1) ] - UNKN2[ , NewName := paste0(RUNID, "__", Barcodes, ".fq.gz") ] - - UNKN <- rbind(UNKN1, UNKN2) - rm(UNKN1, UNKN2) - UNKN <- unique(UNKN, by = "OldName") - setorder(x = UNKN, OldName) - - ## Remove known combinations - UNKN <- UNKN[ !IDS %in% res$Barcodes ] - - cat("Number of possible unknown combinations: ", nrow(UNKN), "\n") - - ## Export unknown combinations - cat("Exporting unknown combinations\n") - - fwrite(x = UNKN[ , .(OldName, NewName)], - file = "unknown_combinations.tsv", quote = F, sep = "\t", col.names = FALSE) - -} # end of dual tags - - -cat("\nValidation finished\n") - - -##################### Session info - -cat("\nAll done.\n") -cat("\n") -cat("Session info:\n") -sessionInfo() -cat("\n") - diff --git a/src/pipecraft-core/service_scripts/NextITS/conf/docker.config b/src/pipecraft-core/service_scripts/NextITS/conf/docker.config deleted file mode 100644 index 16c0eab8..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/conf/docker.config +++ /dev/null @@ -1,12 +0,0 @@ -// Container specifications for the Docker profile - -process { - - // Container from Docker Hub - container = 'vmikk/nextits:1.2.0' - - //// Container from Quay.io registry - // container = 'docker://quay.io/vmikk/nextits:1.1.0' - -} - diff --git a/src/pipecraft-core/service_scripts/NextITS/conf/hpc.config b/src/pipecraft-core/service_scripts/NextITS/conf/hpc.config deleted file mode 100644 index 07877a47..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/conf/hpc.config +++ /dev/null @@ -1,214 +0,0 @@ -/* - * ----------------------------------------------------------- - * Config file for execution on HPC - * ----------------------------------------------------------- - * Specification for a larger resource amount (e.g., CPU number) for each process - * - */ - -// HPC-specific params -params { - max_cpus = 60 - max_memory = "200.GB" - max_time = "240.h" -} - -// Process configuration -process { - - ////////// Step-1 processes - - // Converting BAM to FASTQ - withName: 'S1:bam2fastq' { - cpus = 12 - } - - // Primer disambiguation - withName: 'S1:disambiguate' { - cpus = 1 - } - - // Validate tags for demultiplexing - withName: 'S1:tag_validation' { - cpus = 1 - } - - // QC - PacBio single-end reads - // vsearch currently does not suppot multithreading for `--fastq_filter` - // see https://github.com/torognes/vsearch/issues/466 - withName: 'S1:qc_se' { - cpus = 1 - } - - // QC - Illumina paired-end reads - withName: 'S1:qc_pe' { - // max threads for fastp = 16 - cpus = 12 - } - - // Demultiplexing of PacBio reads (with LIMA) - withName: 'S1:demux' { - cpus = 12 - } - - // Demultiplexing merged Illumina reads - withName: 'S1:demux_illumina' { - cpus = 16 - } - - // Demultiplexing non-merged Illumina reads - withName: 'S1:demux_illumina_notmerged' { - cpus = 10 - } - - // Merging of Illumina PE reads - withName: 'S1:merge_pe' { - cpus = 8 - } - - // Demultiplexing of Illumina reads (with cutadapt) - withName: 'S1:demux_illumina' { - cpus = 16 - } - - // Check primers - withName: 'S1:primer_check' { - cpus = 1 - } - - // ITSx - withName: 'S1:itsx' { - cpus = 6 - } - - // Collect all ITS parts extracted by ITSx - withName: 'S1:itsx_collect' { - cpus = 1 - } - - // Sequence quality tables - withName: 'S1:seq_qual' { - cpus = 6 - memory = null - } - - // Homopolymer compression - withName: 'S1:homopolymer' { - cpus = 2 - } - - // Reference-based chimera removal - withName: 'S1:CHIMERA_REMOVAL:chimera_ref' { - cpus = 4 - } - - // Chimera rescue - withName: 'S1:CHIMERA_REMOVAL:chimera_rescue' { - cpus = 1 - } - - // De novo chimera search - withName: 'S1:CHIMERA_REMOVAL:chimera_denovo' { - cpus = 1 - } - - // Aggregate de novo chimeras - withName: 'S1:CHIMERA_REMOVAL:chimera_denovo_agg' { - cpus = 1 - } - - // Pool sequences (for tag-jump removal and final sequence table creation) - withName: 'S1:pool_seqs' { - cpus = 3 - } - - // Dereplication or pre-clustering prior to tag-jump removal - withName: 'S1:tj_preclust' { - cpus = 20 - } - - // Tag-jump removal - withName: 'S1:tj' { - cpus = 1 - } - - // Create sequence table - withName: 'S1:prep_seqtab' { - cpus = 8 - } - - // Read count summary - withName: 'S1:read_counts' { - cpus = 4 - } - - - ////////// Step-2 processes - - // Aggregate sequences, remove de novo chimeras - withName: 'S2:aggregate_sequences' { - cpus = 8 - } - - // Dereplication (only comression is multithreaded) - withName: 'S2:dereplication' { - cpus = 20 - } - - // 100% clustering with sequence length variation allowed (UNITE-style) - withName: 'S2:dereplication_unite' { - cpus = 20 - } - - // Pre-clustering - withName: 'S2:linclust' { - cpus = 8 - } - - // Bucketize sequences into clusters - withName: 'S2:bucketize' { - cpus = 6 - } - - // UNOISE - withName: 'S2:CLUSTERING:unoise' { - cpus = 20 - } - - // DADA2 - withName: 'S2:CLUSTERING:dada2' { - cpus = 20 - } - - // VSEARCH clustering - withName: 'S2:CLUSTERING:cluster_vsearch' { - cpus = 20 - } - - // SWARM clustering - withName: 'S2:CLUSTERING:cluster_swarm' { - cpus = 20 - } - - // Bucketizing workflow - merge chunks into a single file - withName: 'S2:merge_buckets' { - cpus = 4 - } - - // Merge UC files - withName: 'S2:merge_uc' { - cpus = 4 - } - - // Summarize sequence abundance by OTU - withName: 'S2:summarize' { - cpus = 12 - } - - // Post-clustering curation with LULU - withName: 'S2:lulu' { - cpus = 20 - } - -} - diff --git a/src/pipecraft-core/service_scripts/NextITS/conf/hpc_utslurm.config b/src/pipecraft-core/service_scripts/NextITS/conf/hpc_utslurm.config deleted file mode 100644 index 676654fc..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/conf/hpc_utslurm.config +++ /dev/null @@ -1,340 +0,0 @@ -/* - * ----------------------------------------------------------- - * Config file for execution on HPC - * ----------------------------------------------------------- - * Specification for a larger resource amount (e.g., CPU number) for each process - * - */ - -// HPC-specific params -executor { - name = 'slurm' - queueSize = 200 - submitRateLimit = '5 sec' - pollInterval = '1sec' - retry.delay = '1sec' -} - -env { - OPENBLAS_NUM_THREADS=1 - OMP_NUM_THREADS=1 -} - -// Process configuration -process { - - // Cluster-specific options (e.g., partition name, billing account, etc.) - clusterOptions = '-p amd' - - // Error strategy - errorStrategy = { task.exitStatus in [1,104,125,130,134,135,137,139,140,143,255] ? 'retry' : 'finish' } - maxRetries = 3 - maxErrors = '-1' - - // Default resources - cpus = { check_max( 1 * task.attempt, 'cpus' ) } - memory = { check_max( 2.GB * task.attempt, 'memory' ) } - time = { check_max( 3.h * task.attempt, 'time' ) } - - ////////// Step-1 processes - - // Converting BAM to FASTQ - withName: 'S1:bam2fastq' { - cpus = 12 - memory = 2.GB - time = { check_max( 4.h * task.attempt, 'time' ) } - } - - // Primer disambiguation - withName: 'S1:disambiguate' { - cpus = 1 - memory = 1.GB - time = 20.m - } - - // Validate tags for demultiplexing - withName: 'S1:tag_validation' { - cpus = 1 - memory = 1.GB - time = 20.m - } - - // QC - PacBio single-end reads - // vsearch currently does not suppot multithreading for `--fastq_filter` - // see https://github.com/torognes/vsearch/issues/466 - withName: 'S1:qc_se' { - cpus = 2 // for pipes - memory = 1.GB - time = { check_max( 2.h * task.attempt, 'time' ) } - } - - // QC - Illumina paired-end reads - withName: 'S1:qc_pe' { - // max threads for fastp = 16 - cpus = 12 - memory = 20.GB - time = 4.h - } - - // Demultiplexing of PacBio reads (with LIMA) - withName: 'S1:demux' { - cpus = 12 - memory = { check_max( 3.GB * task.attempt, 'memory' ) } - time = { check_max( 1.h * task.attempt, 'time' ) } - } - - // Demultiplexing merged Illumina reads - withName: 'S1:demux_illumina' { - cpus = 16 - memory = 20.GB - time = 4.h - } - - // Demultiplexing non-merged Illumina reads - withName: 'S1:demux_illumina_notmerged' { - cpus = 10 - memory = 20.GB - time = 4.h - } - - // Merging of Illumina PE reads - withName: 'S1:merge_pe' { - cpus = 8 - memory = 20.GB - time = 4.h - } - - // Demultiplexing of Illumina reads (with cutadapt) - withName: 'S1:demux_illumina' { - cpus = 16 - memory = 20.GB - time = 4.h - } - - // Check primers - withName: 'S1:primer_check' { - cpus = 1 - memory = 1.GB - time = { check_max( 30.m * task.attempt, 'time' ) } - } - - // ITSx - withName: 'S1:itsx' { - cpus = 20 - memory = { check_max( 10.GB * task.attempt, 'memory' ) } - time = { check_max( 20.h * task.attempt, 'time' ) } - } - - // Collect all ITS parts extracted by ITSx - withName: 'S1:itsx_collect' { - cpus = 1 - memory = 1.GB - time = { check_max( 30.m * task.attempt, 'time' ) } - } - - // Sequence quality tables - withName: 'S1:seq_qual' { - cpus = 6 - memory = { check_max( 50.GB * task.attempt, 'memory' ) } - time = { check_max( 1.h * task.attempt, 'time' ) } - } - - // Homopolymer compression - withName: 'S1:homopolymer' { - cpus = 2 - memory = 2.GB - time = { check_max( 3.h * task.attempt, 'time' ) } - } - - // Reference-based chimera removal - withName: 'S1:CHIMERA_REMOVAL:chimera_ref' { - cpus = 4 - memory = { check_max( 3.GB * task.attempt, 'memory' ) } - time = { check_max( 1.h * task.attempt, 'time' ) } - } - - // Chimera rescue - withName: 'S1:CHIMERA_REMOVAL:chimera_rescue' { - cpus = 1 - memory = { check_max( 1.GB * task.attempt, 'memory' ) } - time = { check_max( 1.h * task.attempt, 'time' ) } - } - - // De novo chimera search - withName: 'S1:CHIMERA_REMOVAL:chimera_denovo' { - cpus = 1 - memory = { check_max( 1.GB * task.attempt, 'memory' ) } - time = { check_max( 1.h * task.attempt, 'time' ) } - } - - // Aggregate de novo chimeras - withName: 'S1:CHIMERA_REMOVAL:chimera_denovo_agg' { - cpus = 1 - memory = 1.GB - time = { check_max( 1.h * task.attempt, 'time' ) } - } - - // Pool sequences (for tag-jump removal and final sequence table creation) - withName: 'S1:pool_seqs' { - cpus = 3 - memory = { check_max( 1.GB * task.attempt, 'memory' ) } - time = { check_max( 30.m * task.attempt, 'time' ) } - } - - // Dereplication or pre-clustering prior to tag-jump removal - withName: 'S1:tj_preclust' { - cpus = 12 - memory = { check_max( 12.GB * task.attempt, 'memory' ) } - time = { check_max( 4.h * task.attempt, 'time' ) } - } - - // Tag-jump removal - withName: 'S1:tj' { - cpus = 1 - memory = { check_max( 2.GB * task.attempt, 'memory' ) } - time = { check_max( 30.m * task.attempt, 'time' ) } - } - - // Create sequence table - withName: 'S1:prep_seqtab' { - cpus = 8 - memory = { check_max( 50.GB * task.attempt, 'memory' ) } - time = { check_max( 30.m * task.attempt, 'time' ) } - } - - // Read count summary - withName: 'S1:read_counts' { - cpus = 4 - memory = { check_max( 1.GB * task.attempt, 'memory' ) } - time = { check_max( 30.m * task.attempt, 'time' ) } - } - - - ////////// Step-2 processes - - // Aggregate sequences, remove de novo chimeras - withName: 'S2:aggregate_sequences' { - cpus = 12 - memory = { check_max( 40.GB * task.attempt, 'memory' ) } - time = { check_max( 1.h * task.attempt, 'time' ) } - } - - // Dereplication (only comression is multithreaded) - withName: 'S2:dereplication' { - cpus = 20 - memory = { check_max( 4.GB * task.attempt, 'memory' ) } - time = { check_max( 5.h * task.attempt, 'time' ) } - } - - // 100% clustering with sequence length variation allowed (UNITE-style) - withName: 'S2:dereplication_unite' { - cpus = 20 - memory = { check_max( 50.GB * task.attempt, 'memory' ) } - time = { check_max( 24.h * task.attempt, 'time' ) } - } - - // Pre-clustering - withName: 'S2:linclust' { - cpus = 20 - memory = { check_max( 50.GB * task.attempt, 'memory' ) } - time = { check_max( 1.h * task.attempt, 'time' ) } - } - - // Bucketize sequences into clusters - withName: 'S2:bucketize' { - cpus = 6 - memory = { check_max( 50.GB * task.attempt, 'memory' ) } - time = { check_max( 2.h * task.attempt, 'time' ) } - } - - // UNOISE - withName: 'S2:CLUSTERING:unoise' { - cpus = 30 - memory = { check_max( 40.GB * task.attempt, 'memory' ) } - time = { check_max( 30.h * task.attempt, 'time' ) } - } - - // DADA2 - withName: 'S2:CLUSTERING:dada2' { - cpus = 30 - memory = { check_max( 60.GB * task.attempt, 'memory' ) } - time = { check_max( 30.h * task.attempt, 'time' ) } - } - - // VSEARCH clustering - withName: 'S2:CLUSTERING:cluster_vsearch' { - cpus = 30 - memory = { check_max( 80.GB * task.attempt, 'memory' ) } - time = { check_max( 30.h * task.attempt, 'time' ) } - } - - // SWARM clustering - withName: 'S2:CLUSTERING:cluster_swarm' { - cpus = 30 - memory = { check_max( 80.GB * task.attempt, 'memory' ) } - time = { check_max( 24.h * task.attempt, 'time' ) } - } - - // Bucketizing workflow - merge chunks into a single file - withName: 'S2:merge_buckets' { - cpus = 4 - memory = { check_max( 40.GB * task.attempt, 'memory' ) } - time = { check_max( 4.h * task.attempt, 'time' ) } - } - - // Merge UC files - withName: 'S2:merge_uc' { - cpus = 4 - memory = { check_max( 40.GB * task.attempt, 'memory' ) } - time = { check_max( 4.h * task.attempt, 'time' ) } - } - - // Summarize sequence abundance by OTU - withName: 'S2:summarize' { - cpus = 2 - memory = { check_max( 20.GB * task.attempt, 'memory' ) } - time = { check_max( 5.h * task.attempt, 'time' ) } - } - - // Post-clustering curation with LULU - withName: 'S2:lulu' { - cpus = 30 - memory = { check_max( 60.GB * task.attempt, 'memory' ) } - time = { check_max( 24.h * task.attempt, 'time' ) } - } - -} - - -// Function to ensure that resource requirements don't go beyond a maximum limit -def check_max(obj, type) { - if (type == 'memory') { - try { - if (obj.compareTo(params.max_memory as nextflow.util.MemoryUnit) == 1) - return params.max_memory as nextflow.util.MemoryUnit - else - return obj - } catch (all) { - println " ### ERROR ### Max memory '${params.max_memory}' is not valid! Using default value: $obj" - return obj - } - } else if (type == 'time') { - try { - if (obj.compareTo(params.max_time as nextflow.util.Duration) == 1) - return params.max_time as nextflow.util.Duration - else - return obj - } catch (all) { - println " ### ERROR ### Max time '${params.max_time}' is not valid! Using default value: $obj" - return obj - } - } else if (type == 'cpus') { - try { - return Math.min( obj, params.max_cpus as int ) - } catch (all) { - println " ### ERROR ### Max cpus '${params.max_cpus}' is not valid! Using default value: $obj" - return obj - } - } -} - diff --git a/src/pipecraft-core/service_scripts/NextITS/conf/params.config b/src/pipecraft-core/service_scripts/NextITS/conf/params.config deleted file mode 100644 index 7d34acb3..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/conf/params.config +++ /dev/null @@ -1,307 +0,0 @@ -/* -This file includes default values for the NextITS pipeline parameters -*/ - - -params { - - step = "Step1" // default pipeline step to run - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Step-1 parameters - - // Input files - input = null // FASTQ file or directory - input_R1 = null - input_R2 = null - barcodes = null // FASTA file - - // Output - outdir = "${launchDir}/results" - tracedir = "${params.outdir}/pipeline_info" - - // Which sequencing platform was used? - seqplatform = "PacBio" - // seqplatform = "Illumina" - - // ITS part selector - its_region = "full" - // "full" = default (full-length ITS sequence, after trimming SSU and LSU regions by ITSx) - // "ITS1" or "ITS2" - // "none" = just trim primers - // "ITS1_5.8S_ITS2" = assemble near-full-length ITS from ITSx output (useful in the case if primers are too close to SSU or LSU, and ITSx is not able to detect full-length sequence) - // "SSU" or "LSU" - - // Quality control - qc_maxee = null // only for single-end reads - qc_maxeerate = 0.01 // only for single-end reads - qc_maxhomopolymerlen = 25 // max len of homopolymer regions (if >=, sequence will be removed) - qc_maxn = 4 - - qc_avgphred = null // Only for PE reads - qc_twocolor = false // reduced resolution Phred-scores (two-color Illumina chemistry) - - //// Conditional parameters are in separate configs - // if(qc_twocolor == true){ - // // params_illumina_2color.config - // qc_phredmin = 24 - // qc_phredperc = 30 - // qc_polyglen = 8 - // } else { - // // params_illumina_4color.config - // qc_phredmin = null - // qc_phredperc = null - // qc_polyglen = null - // } - - - // Is data demultiplexed? - // If false (default), input = 1 fastq file and 1 fasta file - // If true, input = multiple fastq files - demultiplexed = false - - // Demultiplexing - PacBio & LIMA - lima_barcodetype = "dual_symmetric" // "single", "dual", "dual_symmetric", "dual_asymmetric" - lima_minscore = 93 // minimum barcode score (93 is for 12bp-long barcodes) - lima_minendscore = 50 // only useful for asymmetric barcoding schemes with different barcodes in a pair - lima_minrefspan = 0.75 // min read span relative to the barcode length - lima_minscoringregions = 2 // for dual barcodes only (2 = requires both barcodes) - lima_windowsize = 70 // window size (in base pairs) - lima_minlen = 40 // minimum sequence length after clipping - lima_remove_unknown = false // remove unknown barcode combinations (in dual-barcoding modes) - - // Demultiplexing - Illumina & cutadapt - barcode_window = 30 - barcode_errors = 1 - barcode_overlap = 11 - - // Illimina pair-end read assembly - pe_minoverlap = 20 - pe_difflimit = 5 - pe_diffperclimit = 20 - pe_nlimit = 10 // disabled - pe_minlen = 30 - - // What to do with not merged reads (Illumina-only) - illumina_keep_notmerged = true - illumina_joinpadgap = "NNNNNNNNNN" - illumina_joinpadqual = "IIIIIIIIII" // quality score of 40 - - - // Primer checks - primer_forward = "TACACACCGCCCGTCG" // ITS9mun - primer_reverse = "CCTSCSCTTANTDATATGC" // ITS4ngsUni - primer_mismatches = 2 - // primer_mismatches_insertions = 1 - // primer_mismatches_deletions = 1 - primer_foverlap = primer_forward.length() - 2 - primer_roverlap = primer_reverse.length() - 2 - - // ITSx - ITSx_evalue = 1e-1 - ITSx_partial = 0 // off, otherwise specify min length cutoff for partial ITS sequences to keep - ITSx_tax = "all" - ITSx_complement = "F" // "F" (check single strand) or "T" (check both DNA strands for matches to HMM-profiles) - /// ITSx_singledomain = true .... optional arguments - ITSx_to_parquet = true // convert ITSx output (FASTA files) to Parquet - ITSx_chunk_size = 10000 // chunk size (number of dereplicated sequences per sample) for distributed ITSx processing; set to 0 to disable chunking - - // Primer trimming (for Illumina) - trim_minlen = 10 - - // Homopolymer compression - hp = true - hp_similarity = 0.999 - hp_iddef = 2 - - // Which chimera removal methods to use - chimera_methods = "ref,denovo" // null or "none" also supported - - // Reference-based chimera removal - chimera_db = "Eukaryome_1.9.3_241222_FullITS_100-800.udb" - chimera_rescueoccurrence = 2 - - // De novo chimera identification (UCHIME1) - chimeranov_abskew = 2.0 - chimeranov_dn = 1.4 - chimeranov_mindiffs = 3 - chimeranov_mindiv = 0.8 - chimeranov_minh = 0.28 - chimeranov_xn = 8.0 - - // Tag-jump removal - tj = true // run tag-jump removal - tj_id = 1 // depreplicate or pre-cluster: 1 = just dereplicate, < 1 (e.g., 0.99) = cluster at 99% similarity - tj_iddef = 2 - tj_f = 0.01 // UNCROSS parameter f - tj_p = 1 - - // Singleton removal - // singleton_minrelabundance = 1 // % of sample abundance // not implemented yet - TODO - - // Collapsing similar sequences - // coverage - - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Step-2 parameters - - // Path to the Step-1 results [input for Step-2] - data_path = "${launchDir}/Step1_Results" - - // Pool sample replicates (e.g., re-sequenced samples) in the final OTU table - merge_replicates = false - - // Filtering sequences (trimmed amplicons) by length - ampliconlen_min = null - ampliconlen_max = null - // if(ampliconlen_min != null | ampliconlen_max != null){ - // length_filtering = true - // } else { - // length_filtering = false - // } - - - // Default thresholds for singleton and de novo chimera removal - max_MEEP = 0.5 - max_ChimeraScore = 0.6 - recover_lowqsingletons = true - recover_denovochimeras = true - - // Number of chunks to split the dataset into prior clustering - chunking_n = null // number of chunks - chunking_id = 0.6 // minimum sequence identity for clustering - - - // Sequence denoising or pre-clustering ("none", "unoise", "dada2", "swarm_d1", "homopolymer") - preclustering = "none" - - // Denoising with UNOISE - unoise_alpha = 6.0 - unoise_minsize = 1 - - // Denoising with DADA2 - dada2_pooling = "global" // "global" or "byrun" (not implemented yet) - dada2_nbases = 1e6 - dada2_bandsize = 16 - dada2_detectsingletons = true - dada2_omegaA = 1e-20 - dada2_omegaC = 1e-40 - dada2_omegaP = 1e-4 - dada2_maxconsist = 10 - dada2_match = 4 - dada2_mismatch = -5 - dada2_gappenalty = -8 - - - // Sequence clustering method ("none" / "vsearch" / "swarm" / "shmatching") - clustering = "vsearch" - - // VSEARCH clustering - otu_id = 0.98 - otu_iddef = 2 // also for UNOISE - otu_qmask = "dust" // also for UNOISE - - // SWARM clustering - swarm_d = 1 - swarm_fastidious = true - swarm_d1boundary = 3 // min mass of large OTUs, only for Fastidious + d=1 - - // Alignment parameters - // NB. vsearch scores = 2 * usearch scores !! - // E.g., "20I/2E" = penalty 20 for opening internal gaps, and 2 for opening terminal gaps (left or right) - - alignment_penalties = "default" // alternatively, "UNITE" - - if(alignment_penalties == "UNITE"){ - - // Alternative dereplication as in UNITE - // Allow query sequences vary 4% in length at 100% similarity - unite_querycov = 0.96 - unite_targetcov = 0.96 - - // VSEARCH - vsearch_gapopen = "0I/0E" // penalties for gap opening (usearch, "0.0/0.0E") - vsearch_gapext = "2I/1E" // penalties for gap extension (usearch, "1.0/0.5E") - - } - if(alignment_penalties == "default"){ - - // VSEARCH - vsearch_gapopen = "20I/2E" - vsearch_gapext = "2I/1E" - } - - - // LULU - lulu = true - lulu_match = 95.0 // minimum similarity threshold (default, 84.0) - lulu_ratio = 1.0 // minimum abundance ratio (default, 1.0) - lulu_ratiotype = "min" // abundance ratio type - "min" or "avg" (default, "min") - lulu_relcooc = 0.95 // relative co-occurrence (default, 0.95) - lulu_maxhits = 0 // maximum number of hits (0 = unlimited; default, 10?) - - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Generic parameters - - - // GZIP compression level - gzip_compression = 7 - - // The file publishing method - storagemode = "rellink" // "symlink", "copy", "rellink", "link", or "move" - - - - // Help message flag - helpMsg = null - help = null - - // Generic Parameters - validate_params = true - // sleep_time = 5 - // help_all = false - - // Boilerplate options - version = false - email = null - email_on_fail = null - plaintext_email = false - - - // Max Job Request Parameters - max_cpus = 40 - max_memory = "132.GB" - max_time = "240.h" - // max_retry = 3 - // min_time = 60 - // max_downloads = 3 - - // Nextflow Configuration Parameters - // force = false - // cleanup_workdir = false - - // Nextflow Profile Parameters - // condadir = "${baseDir}/conda/envs" - // use_mamba = false - // registry = "quay" - // singularity_cache = "$HOME/.phylonext/singularity" - // singularity_pull_docker_container = false - // force_rebuild = false - - // nf-core - monochrome_logs = false - enable_conda = false - show_hidden_params = false - schema_ignore_params = '' - - // Parameters to ignore - // build_all = false - // include_tools = null - // verbose = null - // silent = null - - // nf-schema configuration - showHidden = false - -} - - diff --git a/src/pipecraft-core/service_scripts/NextITS/conf/params_illumina_2color.config b/src/pipecraft-core/service_scripts/NextITS/conf/params_illumina_2color.config deleted file mode 100644 index e2b6e47f..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/conf/params_illumina_2color.config +++ /dev/null @@ -1,11 +0,0 @@ - -// Conditinal parameters for Illumina PE reads -// if `params.qc_twocolor = true` - -// reduced resolution Phred-scores (two-color Illumina chemistry) -params { - qc_phredmin = 24 - qc_phredperc = 30 - qc_polyglen = 8 - -} diff --git a/src/pipecraft-core/service_scripts/NextITS/conf/params_illumina_4color.config b/src/pipecraft-core/service_scripts/NextITS/conf/params_illumina_4color.config deleted file mode 100644 index e9a70d56..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/conf/params_illumina_4color.config +++ /dev/null @@ -1,11 +0,0 @@ - -// Conditinal parameters for Illumina PE reads -// if `params.qc_twocolor = false` - -// reduced resolution Phred-scores (two-color Illumina chemistry) -params { - qc_phredmin = null - qc_phredperc = null - qc_polyglen = null - -} diff --git a/src/pipecraft-core/service_scripts/NextITS/conf/singularity.config b/src/pipecraft-core/service_scripts/NextITS/conf/singularity.config deleted file mode 100644 index 0186233e..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/conf/singularity.config +++ /dev/null @@ -1,16 +0,0 @@ -// Container specifications for the Singularity profile - -process { - - // Container from Singularity library - container = 'library://vmiks/nextits/nextits:1-2-0' - - // Container from Quay.io registry [not working yet] - // container = 'quay.io/vmikk/nextits:1.2.0' - - // Container from Docker Hub (will be converted to Singularity image) - // container = 'vmikk/nextits:1.2.0' - - // Local Singularity image file - // container = '/path/to/local/file/nextits-1-2-0.sif' -} diff --git a/src/pipecraft-core/service_scripts/NextITS/conf/test_step1.config b/src/pipecraft-core/service_scripts/NextITS/conf/test_step1.config deleted file mode 100644 index b89cf628..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/conf/test_step1.config +++ /dev/null @@ -1,40 +0,0 @@ -/* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Nextflow config file for running minimal tests for the Step-1 of the NextITS pipeline -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Defines input files and everything required to run a fast and simple pipeline test. - - Use as follows: - nextflow run vmikk/nextits -r main -profile test, --outdir - ----------------------------------------------------------------------------------------- -*/ - - -// Path to test data -testdata_base_path = "${projectDir}/test_data" - - -process { - resourceLimits = [ - cpus: 4, - memory: '8.GB', - time: '10.m' - ] -} - -params { - - input = "${testdata_base_path}/Test_Step1.fastq.gz" - barcodes = "${testdata_base_path}/Test_Step1_barcodes.fasta" - primer_forward = "TACACACCGCCCGTCG" // ITS9mun - primer_reverse = "CCTSCSCTTANTDATATGC" // ITS4ngsUni - chimera_db = "${testdata_base_path}/Test_ChimeraDB.udb" - step = "Step1" - its_region = "full" - ITSx_tax = "fungi" - outdir = "NextITS_Test_Step1" - tracedir = "${params.outdir}/pipeline_info" - - storagemode = "copy" -} diff --git a/src/pipecraft-core/service_scripts/NextITS/conf/test_step2.config b/src/pipecraft-core/service_scripts/NextITS/conf/test_step2.config deleted file mode 100644 index ce7fe2fb..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/conf/test_step2.config +++ /dev/null @@ -1,43 +0,0 @@ -/* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Nextflow config file for running minimal tests for the Step-2 of the NextITS pipeline -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Defines input files and everything required to run a fast and simple pipeline test. - - Use as follows: - nextflow run vmikk/nextits -r main -profile test2, --outdir - ----------------------------------------------------------------------------------------- -*/ - - -// Path to test data -testdata_base_path = "${projectDir}/test_data" - - -process { - resourceLimits = [ - cpus: 4, - memory: '8.GB', - time: '10.m' - ] -} - -params { - - data_path = "${testdata_base_path}/Test_S2" - step = "Step2" - outdir = "NextITS_Test_Step2" - tracedir = "${params.outdir}/pipeline_info" - merge_replicates = false - max_MEEP = 0.5 - max_ChimeraScore = 0.6 - preclustering = "unoise" - unoise_alpha = 6.0 - unoise_minsize = 1 - clustering = "vsearch" - otu_id = 0.98 - lulu = false - - storagemode = "copy" -} diff --git a/src/pipecraft-core/service_scripts/NextITS/containerfiles/NextITS.def b/src/pipecraft-core/service_scripts/NextITS/containerfiles/NextITS.def deleted file mode 100644 index 86c1a35d..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/containerfiles/NextITS.def +++ /dev/null @@ -1,306 +0,0 @@ -# NextITS - Singularity definition file, main container - -## To avoid GitHub API limits, use `SINGULARITYENV_GITHUB_PAT=[token]` with Singularity -## to specify the GITHUB_PAT token (the token will be available only during the `%post` section of the build) - - -## Build stage (Rust and Cargo), for runiq -Bootstrap: docker -From: rust:1.92.0-slim -Stage: build - -%post - # rustup --version # 1.28.2 - # rustc --version # 1.92.0 - # cargo --version # 1.92.0 - cargo install runiq sd - # runiq --version # 2.1.0 - # sd --version # 1.0.0 - -## Main stage -Bootstrap: docker -From: rocker/r-ver:4.5.2 -Stage: final - -%labels - Maintainer vladimir.mikryukov@ut.ee - R_Version 4.5.2 - NextITS_container_version 1.2.0 - -%runscript - exec "$@" - -%apprun R - exec R "${@}" - -%apprun Rscript - exec Rscript "${@}" - -%environment - export PATH="/opt/software/conda/bin:$PATH" - export CONDA_PREFIX="/opt/software/conda" - - # Source conda initialization if available (for interactive sessions) - if [ -f "/opt/software/conda/init.bash" ]; then - source "/opt/software/conda/init.bash" - fi - -## Install the required dependencies -%post - export R_VERSION=4.5.2 - - apt-get update -qq \ - && apt-get -y --no-install-recommends install \ - tar zip unzip pigz gzip zstd xz-utils bzip2 coreutils \ - curl wget git less gawk nano rename bc \ - ca-certificates locales \ - libtre-dev libtre5 zlib1g zlib1g-dev liblzma-dev libbz2-dev libcurl4-openssl-dev libglpk-dev libglpk40 \ - build-essential - - ## Configure default locale - # echo "C.UTF-8" >> /etc/locale.gen - # locale-gen C.UTF-8 - /usr/sbin/update-locale LANG=C.UTF-8 - export LC_ALL=C.UTF-8 - export LANG=C.UTF-8 - - ## Install additional R packages - install2.r --error --skipinstalled --ncpus -1 \ - remotes \ - optparse \ - R.utils \ - data.table \ - arrow \ - duckdb \ - BiocManager \ - plyr \ - dplyr \ - ggplot2 \ - doFuture \ - openxlsx \ - yaml - - R -e 'BiocManager::install("Biostrings", ask = FALSE)' \ - && R -e 'BiocManager::install("ShortRead", ask = FALSE)' \ - && R -e 'BiocManager::install("DECIPHER", ask = FALSE)' \ - && R -e 'BiocManager::install("dada2", ask = FALSE)' \ - && R -e 'BiocManager::install("phyloseq", ask = FALSE)' \ - && rm -rf /tmp/downloaded_packages - - install2.r --error --skipinstalled \ - geodist \ - phytools \ - ggdendro \ - gridExtra \ - && R -e 'ok <- tryCatch({ remotes::install_github("mikemc/speedyseq"); TRUE }, error=function(e){ message(e); FALSE }); \ - if (!ok || !requireNamespace("speedyseq", quietly=TRUE)) quit(status=1)' \ - && R -e 'ok <- tryCatch({ remotes::install_github("vmikk/metagMisc"); TRUE }, error=function(e){ message(e); FALSE }); \ - if (!ok || !requireNamespace("metagMisc", quietly=TRUE)) quit(status=1)' \ - && R -e 'ok <- tryCatch({ remotes::install_cran("qs", type = "source", configure.args = "--with-simd=AVX2"); TRUE }, error=function(e){ message(e); FALSE }); \ - if (!ok || !requireNamespace("qs", quietly=TRUE)) quit(status=1)' \ - && rm -rf /tmp/downloaded_packages - - ## Install conda - readonly conda_prefix="/opt/software/conda" - - mkdir -p /opt/software - cd /opt/software - curl -L -O "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh" - bash Miniforge3-Linux-x86_64.sh -u -b -p ${conda_prefix} - rm Miniforge3-Linux-x86_64.sh - ${conda_prefix}/bin/conda config --add channels bioconda - ${conda_prefix}/bin/mamba update -y --all - - ## Conda initialization script by Antoine Prouvost - # https://github.com/ds4dm/singularity-conda/blob/main/conda.def - # Singularity does all the environment sourcing as shell (only latter calls bash), which conda does not support. - # We put the content in a file, manually call bash, and source it. - { - echo 'eval "$(' "'${conda_prefix}/bin/conda' 'shell.bash' 'hook' 2> /dev/null" ')"' - echo 'if [ $? -eq 0 ]; then' - echo ' eval "$__conda_setup"' - echo 'else' - echo ' if [ -f ' "'${conda_prefix}/etc/profile.d/conda.sh'" ']; then' - echo ' .' "'${conda_prefix}/opt/mamba/etc/profile.d/conda.sh'" - echo ' else' - echo ' export PATH="/opt/software/conda/bin:$PATH"' - echo ' fi' - echo 'fi' - echo 'unset __conda_setup' - } >> ${conda_prefix}/init.bash - - ## Create conda environment and install software - ${conda_prefix}/bin/mamba install --quiet --yes \ - "lima>=2.13.0" \ - "pbtk>=3.5.0" \ - "vsearch>=2.30.3" \ - "swarm>=3.1.6" \ - "seqkit>=2.12.0" \ - "seqfu>=1.23.0" \ - "fastp>=1.0.1" \ - "blast>=2.17.0" \ - "bioawk" \ - "miller>=6.16.0" \ - "xsv>=0.13.0" \ - "bedtools>=2.31.1" \ - "parallel>=20251122" \ - "csvtk>=0.36.0" \ - "cutadapt>=5.2" \ - "itsx>=1.1.3" \ - "bbmap>=39.52" \ - "ripgrep>=15.1.0" \ - "fd-find>=10.3.0" \ - "mmseqs2" - - ## seqhasher - # https://github.com/vmikk/seqhasher - wget https://github.com/vmikk/seqhasher/releases/download/1.1.2/seqhasher - chmod +x seqhasher - mv seqhasher ${conda_prefix}/bin/ - - ## phredsort - # https://github.com/vmikk/phredsort - wget https://github.com/vmikk/phredsort/releases/download/1.4.0/phredsort - chmod +x phredsort - mv phredsort ${conda_prefix}/bin/ - - ## ucs - # https://github.com/vmikk/ucs - wget https://github.com/vmikk/ucs/releases/download/0.8.0/ucs - chmod +x ucs - mv ucs ${conda_prefix}/bin/ - - ## fqgrep v.0.4.4 - # https://github.com/indraniel/fqgrep - git clone --depth 1 https://github.com/indraniel/fqgrep - cd fqgrep - make - mv fqgrep ${conda_prefix}/bin/ - cd .. - rm -r fqgrep - - ## rush - # https://github.com/shenwei356/rush - wget https://github.com/shenwei356/rush/releases/download/v0.8.0/rush_linux_amd64.tar.gz - tar -xzf rush_linux_amd64.tar.gz - mv rush ${conda_prefix}/bin/ - rm rush_linux_amd64.tar.gz - - ## brename - # https://github.com/shenwei356/brename - wget https://github.com/shenwei356/brename/releases/download/v2.14.0/brename_linux_amd64.tar.gz - tar -xzf brename_linux_amd64.tar.gz - mv brename ${conda_prefix}/bin/ - rm brename_linux_amd64.tar.gz - - ## MUMU - # https://github.com/frederic-mahe/mumu - git clone --depth 1 https://github.com/frederic-mahe/mumu.git \ - && cd ./mumu/ \ - && make && make check && make install - mv mumu ${conda_prefix}/bin/ - cd .. - rm -r mumu - - ## Get the updated ITSx databases ["Version 2", 5 April 2024, curated by Henrik Nilsson] - ## NB! Currently, there is no X.hmm profile (Apusozoa) - git clone --depth 1 https://github.com/USDA-ARS-GBRU/ITS_HMMs/ - ## Compress and index HMM flatfiles - find ITS_HMMs/ITSx_db/HMMs/ -name "*.hmm" | grep -v "N.hmm" \ - | ${conda_prefix}/bin/parallel -j1 "${conda_prefix}/bin/hmmpress {}" - ## Replace old profiles - rm ${conda_prefix}/bin/ITSx_db/HMMs/* - mv ITS_HMMs/ITSx_db/HMMs/* ${conda_prefix}/bin/ITSx_db/HMMs/ - rm -r ITS_HMMs - ## Activate Parabasalia model in ITSx - # grep "\#push(@profileSet,\"Y\")" ${conda_prefix}/bin/ITSx - sed -i '/#push(@profileSet,"Y")/s/#//' ${conda_prefix}/bin/ITSx - - ## DuckDB - # https://duckdb.org/docs/installation/?version=stable - curl -L https://github.com/duckdb/duckdb/releases/download/v1.4.3/duckdb_cli-linux-amd64.zip -o duckdb_cli-linux-amd64.zip \ - && unzip duckdb_cli-linux-amd64.zip -d ${conda_prefix}/bin/ \ - && rm duckdb_cli-linux-amd64.zip - - ## Clean up - apt-get autoremove -y \ - && apt-get autoclean -y \ - && rm -rf /var/lib/apt/lists/* \ - && ${conda_prefix}/bin/mamba clean --all --quiet --yes - - ## Activate environment - # source ${conda_prefix}/bin/activate ${conda_prefix} - - ## Ensure software is accessible to any user (NB! avoid slow recursive operations) - chmod 755 /opt/software - chmod a+rX /opt/software/conda/bin - chmod a+rX /opt/software/conda/lib - - ## Use bash to properly activate the conda environment - echo 'source' "'${conda_prefix}/init.bash'" | bash - - cd /opt/software - - -## Add binary from the stage one - runiq, sd -%files from build - /usr/local/cargo/bin/runiq /opt/software/conda/bin/ - /usr/local/cargo/bin/sd /opt/software/conda/bin/ - - -%test - echo "=== Testing R installation and packages ===" - R --quiet -e "stopifnot(getRversion() == '${R_VERSION}')" - - echo "Testing R package installations..." - R --quiet -e " - required_packages <- c('optparse', 'R.utils', 'data.table', 'arrow', 'duckdb', - 'plyr', 'dplyr', 'ggplot2', 'doFuture', 'openxlsx', 'yaml', - 'Biostrings', 'ShortRead', 'DECIPHER', 'dada2', 'phyloseq', - 'metagMisc', 'qs') - - for(pkg in required_packages) { - cat('Testing package:', pkg, '... ') - tryCatch({ - suppressPackageStartupMessages( - library(pkg, character.only = TRUE, quietly = TRUE, warn.conflicts = FALSE) - ) - cat('OK\n') - }, error = function(e) { - cat('FAILED\n') - stop('Package ', pkg, ' failed to load: ', e$message) - }) - } - cat('All R packages loaded successfully!\n') - " - - echo "=== Testing conda/mamba installed tools ===" - ## Source conda environment - source /opt/software/conda/init.bash - - ## Test conda/mamba tools - tools_conda="lima bam2fastq vsearch swarm seqkit seqfu fastp blastn bioawk mlr xsv bedtools parallel csvtk ITSx cutadapt bbduk.sh rg fd mmseqs" - for tool in $tools_conda; do - echo -n "Testing $tool... " - if command -v $tool >/dev/null 2>&1; then - echo "OK" - else - echo "FAILED - $tool not found in PATH" - exit 1 - fi - done - - echo "=== Testing manually installed tools ===" - ## Test manually installed tools - tools_manual="seqhasher phredsort ucs fqgrep rush brename mumu duckdb runiq sd" - for tool in $tools_manual; do - echo -n "Testing $tool... " - if command -v $tool >/dev/null 2>&1; then - echo "OK" - else - echo "FAILED - $tool not found in PATH" - exit 1 - fi - done - - echo "=== All tests passed! Container looks ready for use ===" diff --git a/src/pipecraft-core/service_scripts/NextITS/containerfiles/NextITS.dockerfile b/src/pipecraft-core/service_scripts/NextITS/containerfiles/NextITS.dockerfile deleted file mode 100644 index 99e1e4c2..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/containerfiles/NextITS.dockerfile +++ /dev/null @@ -1,270 +0,0 @@ -# NextITS - Dockerfile, main container -# Multi-stage build is used to compile Rust-based software -# Nextflow is included in the image - -## To build the image, run: -# docker build --tag nextits --file NextITS.dockerfile . -# -## To run tests during build: -# docker build --target test --tag nextits-test --file NextITS.dockerfile . - -## Build stage 1 (Rust and Cargo) -FROM rust:1.92.0-slim AS rust -RUN cargo install runiq sd - -## Build stage 2 - Main -FROM rocker/r-ver:4.5.2 AS main - -ENV LANG=C.UTF-8 -ENV LC_ALL=C.UTF-8 -ENV SHELL=/bin/bash -LABEL org.opencontainers.image.authors="vladimir.mikryukov@ut.ee" -LABEL org.opencontainers.image.version="1.2.0" - -RUN apt-get update -qq \ - && apt-get -y --no-install-recommends install \ - tar zip unzip pigz gzip zstd xz-utils bzip2 coreutils \ - curl wget git less gawk nano rename bc \ - ca-certificates locales \ - libtre-dev libtre5 zlib1g zlib1g-dev liblzma-dev libbz2-dev libcurl4-openssl-dev libglpk-dev libglpk40 \ - build-essential \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -## Install additional R packages -RUN install2.r --error --skipinstalled --ncpus -1 \ - remotes \ - optparse \ - R.utils \ - data.table \ - arrow \ - duckdb \ - BiocManager \ - plyr \ - dplyr \ - ggplot2 \ - doFuture \ - openxlsx \ - yaml - -RUN R -e 'BiocManager::install("Biostrings", ask = FALSE)' \ - && R -e 'BiocManager::install("ShortRead", ask = FALSE)' \ - && R -e 'BiocManager::install("DECIPHER", ask = FALSE)' \ - && R -e 'BiocManager::install("dada2", ask = FALSE)' \ - && R -e 'BiocManager::install("phyloseq", ask = FALSE)' \ - && rm -rf /tmp/downloaded_packages - -RUN install2.r --error --skipinstalled \ - geodist \ - phytools \ - ggdendro \ - gridExtra \ - && R -e 'ok <- tryCatch({ remotes::install_github("mikemc/speedyseq"); TRUE }, error=function(e){ message(e); FALSE }); \ - if (!ok || !requireNamespace("speedyseq", quietly=TRUE)) quit(status=1)' \ - && R -e 'ok <- tryCatch({ remotes::install_github("vmikk/metagMisc"); TRUE }, error=function(e){ message(e); FALSE }); \ - if (!ok || !requireNamespace("metagMisc", quietly=TRUE)) quit(status=1)' \ - && R -e 'ok <- tryCatch({ remotes::install_cran("qs", type = "source", configure.args = "--with-simd=AVX2"); TRUE }, error=function(e){ message(e); FALSE }); \ - if (!ok || !requireNamespace("qs", quietly=TRUE)) quit(status=1)' \ - && rm -rf /tmp/downloaded_packages - -## Install conda -RUN mkdir -p /opt/software \ - && cd /opt/software \ - && curl -L -O "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh" \ - && bash Miniforge3-Linux-x86_64.sh -u -b -p /opt/software/conda \ - && rm Miniforge3-Linux-x86_64.sh \ - && /opt/software/conda/bin/conda config --add channels bioconda \ - && /opt/software/conda/bin/mamba update -y --all \ - && /opt/software/conda/bin/mamba clean --all --yes - -## Create conda initialization script (for Singularity compatibility) -RUN cd /opt/software \ - && { \ - echo 'eval "$(' '"/opt/software/conda/bin/conda" "shell.bash" "hook" 2> /dev/null' ')"'; \ - echo 'if [ $? -eq 0 ]; then'; \ - echo ' eval "$__conda_setup"'; \ - echo 'else'; \ - echo ' if [ -f "/opt/software/conda/etc/profile.d/conda.sh" ]; then'; \ - echo ' . "/opt/software/conda/etc/profile.d/conda.sh"'; \ - echo ' else'; \ - echo ' export PATH="/opt/software/conda/bin:$PATH"'; \ - echo ' fi'; \ - echo 'fi'; \ - echo 'unset __conda_setup'; \ - } > /opt/software/conda/init.bash - -## Create conda environment and install software -RUN /opt/software/conda/bin/mamba install -y \ - "lima>=2.13.0" \ - "pbtk>=3.5.0" \ - "vsearch>=2.30.3" \ - "swarm>=3.1.6" \ - "seqkit>=2.12.0" \ - "seqfu>=1.23.0" \ - "fastp>=1.0.1" \ - "blast>=2.17.0" \ - "bioawk" \ - "miller>=6.16.0" \ - "xsv>=0.13.0" \ - "bedtools>=2.31.1" \ - "parallel>=20251122" \ - "csvtk>=0.36.0" \ - "cutadapt>=5.2" \ - "itsx>=1.1.3" \ - "bbmap>=39.52" \ - "ripgrep>=15.1.0" \ - "fd-find>=10.3.0" \ - "mmseqs2" \ - && /opt/software/conda/bin/conda clean --all --yes - - -## Install cutadapt (with dependencies) from pip - it fails with conda (Python 3.13 confilict) -# RUN /opt/software/conda/bin/pip install --no-cache-dir \ -# "dnaio>=1.2.3" "xopen>=2.0.2" "cutadapt>=5.1" - -## Add new tools (seqhasher, phredsort, ucs) -RUN cd /opt/software \ - && wget https://github.com/vmikk/seqhasher/releases/download/1.1.2/seqhasher \ - && chmod +x seqhasher \ - && mv seqhasher /opt/software/conda/bin/ \ - && wget https://github.com/vmikk/phredsort/releases/download/1.4.0/phredsort \ - && chmod +x phredsort \ - && mv phredsort /opt/software/conda/bin/ \ - && wget https://github.com/vmikk/ucs/releases/download/0.8.0/ucs \ - && chmod +x ucs \ - && mv ucs /opt/software/conda/bin/ - -## fqgrep -RUN git clone --depth 1 https://github.com/indraniel/fqgrep \ - && cd fqgrep \ - && make \ - && mv fqgrep /opt/software/conda/bin/ \ - && cd .. \ - && rm -r fqgrep - -## rush -RUN wget https://github.com/shenwei356/rush/releases/download/v0.8.0/rush_linux_amd64.tar.gz \ - && tar -xzf rush_linux_amd64.tar.gz \ - && mv rush /opt/software/conda/bin/ \ - && rm rush_linux_amd64.tar.gz - -## brename -RUN wget https://github.com/shenwei356/brename/releases/download/v2.14.0/brename_linux_amd64.tar.gz \ - && tar -xzf brename_linux_amd64.tar.gz \ - && mv brename /opt/software/conda/bin/ \ - && rm brename_linux_amd64.tar.gz - -## MUMU -RUN git clone --depth 1 https://github.com/frederic-mahe/mumu.git \ - && cd ./mumu/ \ - && make && make check && make install \ - && mv mumu /opt/software/conda/bin/ \ - && cd .. \ - && rm -r mumu - -## Rust tools (from the Cargo-based stage) -COPY --from=rust /usr/local/cargo/bin/runiq /opt/software/conda/bin/runiq -COPY --from=rust /usr/local/cargo/bin/sd /opt/software/conda/bin/sd - -## Update ITSx databases -RUN cd /opt/software \ - && git clone --depth 1 https://github.com/USDA-ARS-GBRU/ITS_HMMs/ \ - && find ITS_HMMs/ITSx_db/HMMs/ -name "*.hmm" | grep -v "N.hmm" \ - | /opt/software/conda/bin/parallel -j1 "/opt/software/conda/bin/hmmpress {}" \ - && rm /opt/software/conda/bin/ITSx_db/HMMs/* \ - && mv ITS_HMMs/ITSx_db/HMMs/* /opt/software/conda/bin/ITSx_db/HMMs/ \ - && rm -r ITS_HMMs \ - && sed -i '/#push(@profileSet,"Y")/s/#//' /opt/software/conda/bin/ITSx - -## Install DuckDB -RUN cd /opt/software \ - && curl -L https://github.com/duckdb/duckdb/releases/download/v1.4.3/duckdb_cli-linux-amd64.zip -o duckdb_cli-linux-amd64.zip \ - && unzip duckdb_cli-linux-amd64.zip -d /opt/software/conda/bin/ \ - && rm duckdb_cli-linux-amd64.zip - -## Set up environment for both Docker and Singularity compatibility -ENV PATH="/opt/software/conda/bin:${PATH}" - -## Create non-privileged user -RUN groupadd -g 1000 nextits \ - && useradd -u 1000 -g 1000 -m -s /bin/bash nextits \ - && mkdir -p /home/nextits \ - && chown -R nextits:nextits /home/nextits \ - && mkdir -p /tmp/nextits \ - && chmod 1777 /tmp/nextits - -## Set software directory permissions -## (NB! avoid recursive operations on large conda env) -RUN chmod 755 /opt/software \ - && chmod 755 /opt/software/conda \ - && chmod a+rX /opt/software/conda/bin \ - && chmod a+rX /opt/software/conda/lib - -## Create entrypoint script that initializes conda properly -RUN echo '#!/bin/bash' > /opt/software/entrypoint.sh \ - && echo 'set -e' >> /opt/software/entrypoint.sh \ - && echo '# Try to source conda initialization if available' >> /opt/software/entrypoint.sh \ - && echo 'if [ -f "/opt/software/conda/init.bash" ]; then' >> /opt/software/entrypoint.sh \ - && echo ' source /opt/software/conda/init.bash' >> /opt/software/entrypoint.sh \ - && echo 'fi' >> /opt/software/entrypoint.sh \ - && echo 'exec "$@"' >> /opt/software/entrypoint.sh \ - && chmod +x /opt/software/entrypoint.sh - -## Switch to non-privileged user -USER nextits -## Change working directory (for compatiblity with Singularity) -WORKDIR /tmp/nextits -ENTRYPOINT ["/opt/software/entrypoint.sh"] - -## Test stage - run with: docker build --target test -FROM main AS test - -# Set environment variable for R version testing -ENV R_VERSION=4.5.2 - -RUN echo "=== Testing R installation and packages ===" \ - && R --quiet -e "stopifnot(getRversion() == '${R_VERSION}')" \ - && echo "Testing R package installations..." \ - && printf '%s\n' \ - 'required_packages <- c("optparse", "data.table", "arrow", "duckdb",' \ - ' "plyr", "dplyr", "ggplot2", "openxlsx", "yaml",' \ - ' "Biostrings", "DECIPHER", "dada2", "phyloseq",' \ - ' "metagMisc", "qs")' \ - '' \ - 'for(pkg in required_packages) {' \ - ' cat("Testing package:", pkg, "... ")' \ - ' tryCatch({' \ - ' suppressPackageStartupMessages(' \ - ' library(pkg, character.only = TRUE, quietly = TRUE, warn.conflicts = FALSE)' \ - ' )' \ - ' cat("OK\n")' \ - ' }, error = function(e) {' \ - ' cat("FAILED\n")' \ - ' stop("Package ", pkg, " failed to load: ", e$message)' \ - ' })' \ - '}' \ - 'cat("All R packages loaded successfully!\n")' \ - > test_packages.R \ - && Rscript test_packages.R \ - && rm test_packages.R \ - && echo "=== Testing conda/mamba installed tools ===" \ - && tools_conda="lima bam2fastq vsearch swarm seqkit seqfu fastp blastn bioawk mlr xsv bedtools parallel csvtk ITSx cutadapt bbduk.sh rg fd mmseqs" \ - && for tool in $tools_conda; do \ - echo -n "Testing $tool... " \ - && if command -v $tool >/dev/null 2>&1; then \ - echo "OK"; \ - else \ - echo "FAILED - $tool not found in PATH" && exit 1; \ - fi; \ - done \ - && echo "=== Testing manually installed tools ===" \ - && tools_manual="seqhasher phredsort ucs fqgrep rush brename mumu duckdb runiq sd" \ - && for tool in $tools_manual; do \ - echo -n "Testing $tool... " \ - && if command -v $tool >/dev/null 2>&1; then \ - echo "OK"; \ - else \ - echo "FAILED - $tool not found in PATH" && exit 1; \ - fi; \ - done \ - && echo "=== All tests passed! Container looks ready for use ===" diff --git a/src/pipecraft-core/service_scripts/NextITS/containerfiles/README.md b/src/pipecraft-core/service_scripts/NextITS/containerfiles/README.md deleted file mode 100644 index 49f95634..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/containerfiles/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# NextITS container images - -Reproducible computational environments are essential for scientific workflows. -NextITS provides pre-built container images to ensure consistent software versions and dependencies across different computing platforms, eliminating "it works on my machine" issues and enabling reproducible bioinformatics analyses. - -In general, NextITS will pull the required images automatically (e.g., when providing the `-profile singularity` or `-profile docker` flag to the Nextflow command). - -However, if you want to pull or build the container images manually, you can do so using the following instructions. - -## Pull pre-built images - -### Docker Hub - -```bash -# Pull specific version -docker pull vmikk/nextits:1.1.0 -``` - -### Singularity library - -```bash -# Pull specific version -singularity pull library://vmiks/nextits/nextits:1-1-0 -``` - diff --git a/src/pipecraft-core/service_scripts/NextITS/images/NextITS_logo.png b/src/pipecraft-core/service_scripts/NextITS/images/NextITS_logo.png deleted file mode 100644 index cdbdc26b7c8359a514a87adebb22ac5a102709f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8790 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 G>^}fV)<1m! diff --git a/src/pipecraft-core/service_scripts/NextITS/images/NextITS_logo.svg b/src/pipecraft-core/service_scripts/NextITS/images/NextITS_logo.svg deleted file mode 100755 index 1b6b6dc2..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/images/NextITS_logo.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/src/pipecraft-core/service_scripts/NextITS/main.nf b/src/pipecraft-core/service_scripts/NextITS/main.nf deleted file mode 100644 index 2da9cc32..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/main.nf +++ /dev/null @@ -1,283 +0,0 @@ -#!/usr/bin/env nextflow -/* -============================================================================ - NextITS: Pipeline to process eukaryotic ITS amplicons -============================================================================ - License: Apache-2.0 - Github : https://github.com/vmikk/NextITS - Website: https://Next-ITS.github.io/ ----------------------------------------------------------------------------- -*/ - -// NB!!: -// - provide absolute paths to the input data (e.g. --input and --barcodes) -// - File names should not contain period (.) characters (except for extensions) - -// Databases: -// - UDB for chimera identification - - -// Enable DSL2 syntax -nextflow.enable.dsl = 2 - -// Print the version and exit -if (params.version) { - ver = "NextITS " + workflow.manifest.version - if (workflow.commitId) { ver += " revision " + workflow.commitId.substring(0, 7) } - println "${ver}\n" - exit(0) -} - -// Note: nf-schema plugin handles --help automatically via configuration in nextflow.config - -// Show a custom help message and exit -if (params.helpMsg){ - include { helpMsg } from './modules/help_message.nf' - helpMsg() - exit(0) -} - - -// Enable topic channels -// nextflow.preview.topic = true // Nextflow < 25.04.0 - - - -// nf-schema functions for parameter validation -include { validateParameters } from 'plugin/nf-schema' - -// Include custom parameter summary function -include { paramSummary } from './modules/parameter_summary' - -// Include color utilities -include { getColors; colorize; colorizeMultiple; errorMsg; warningMsg; infoMsg; successMsg } from './modules/colors' - -// Include workflows -// NB! `include` statements are static, meaning they are resolved at compile time rather than at runtime! -include { S1 } from './workflows/STEP1.nf' -include { S2 } from './workflows/STEP2.nf' -include { seqstats } from './workflows/STEP1.nf' - - -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ VALIDATE INPUTS - - - -// Print NextITS logo -def logoColors = getColors(params.monochrome_logs) - -def workflow_version = workflow.manifest.version ?: "unknown" -if (workflow.commitId) { workflow_version += " (${workflow.commitId.substring(0, 7)})" } - -def logo = """ -${logoColors.dim}----------------------------------------------------${logoColors.reset} - ${colorizeMultiple("Next", ['green', 'bold'], params.monochrome_logs)}${colorizeMultiple("ITS", ['purple', 'bold'], params.monochrome_logs)} ${logoColors.cyan}${workflow_version}${logoColors.reset} -${logoColors.green} SSU ${logoColors.purple}ITS1 ${logoColors.green}5.8S ${logoColors.purple}ITS2 ${logoColors.green}LSU ${logoColors.reset} -${logoColors.green} ▒▒▒▒▒▒▒▒▒${logoColors.purple}░░░░░░░░░${logoColors.green}▒▒▒▒▒${logoColors.purple}░░░░░░░░░░${logoColors.green}▒▒▒▒▒▒▒▒▒▒▒▒${logoColors.reset} -${logoColors.dim}----------------------------------------------------${logoColors.reset} -""" - -log.info logo - -// Print all parameters using nf-schema plugin -// include { paramsSummaryLog } from 'plugin/nf-schema' -// log.info paramsSummaryLog(workflow) // will print params from Step-1 and Step-2 simultaneously - - -// Additional runtime parameter validation -// These checks are performed after schema validation and handle -// conditional logic and file existence checks that cannot be expressed in JSON Schema - -// Additional parameter validation for Step-1 -if( params.step == "Step1" || params.step == "seqstats" ) { - - if (params.input == false && params.seqplatform == "PacBio") { - println( errorMsg("Please provide the input file with sequences in FASTQ.gz or BAM format with `--input` parameter.", params.monochrome_logs)) - exit(1) - } - if (params.input_R1 == false && params.input_R2 == false && params.seqplatform == "Illumina") { - println( errorMsg("Please provide input files with sequences in FASTQ.gz format with `--input_R1` and `--input_R2` parameters.", params.monochrome_logs)) - exit(1) - } - if (params.barcodes == false && params.demultiplexed == false) { - println( errorMsg("Please provide the file with sample barcodes in FASTA format with `--barcodes` parameter.", params.monochrome_logs)) - exit(1) - } -} - -if( params.step == "Step1" ) { - - // Reference-based chimera removal - if (params.chimera_methods && params.chimera_methods.toLowerCase().split(',').contains('ref')) { - if (!params.chimera_db || !file(params.chimera_db).exists()) { - println( errorMsg("For reference-based chimera removal, please provide the database in UDB format with `--chimera_db` parameter.", params.monochrome_logs)) - println( colorize(" See https://Next-ITS.github.io/installation/#databases for more information.", 'red', params.monochrome_logs)) - println( colorize("Alternatively, you can disable reference-based chimera removal with `--chimera_methods` parameter (set it to `none` or `denovo`).", 'red', params.monochrome_logs)) - exit(1) - } - if (!(params.chimera_db.toLowerCase().endsWith('.udb'))) { - println( errorMsg("The reference database file specified with `--chimera_db` parameter must be in UDB format.", params.monochrome_logs)) - println( colorize(" See https://Next-ITS.github.io/installation/#databases for more information.", 'red', params.monochrome_logs)) - exit 1 - } - } - - if (params.hp == true && params.seqplatform == "Illumina" && params.illumina_keep_notmerged == true) { - println( errorMsg("Homopolymer compression is not implemented for Illumina non-merged reads (add `--hp false` to your command).", params.monochrome_logs)) - exit(1) - } - if (params.seqplatform == "Illumina" && params.demultiplexed == true) { - println( errorMsg("Handling demultiplexed data for Illumina is not implemented yet.", params.monochrome_logs)) - exit(1) - } - - if (params.seqplatform == "Illumina" && params.illumina_keep_notmerged == true && params.its_region != "none") { - println( warningMsg("Unmerged Illumina reads are not compatible with ITSx. Amplicons will be primer-trimmed.", params.monochrome_logs)) - } - - // ITSx profiles validation - if (params.its_region != "none") { - - /* - Currently, the following regex pattern is used to pre-validate the `ITSx_tax` parameter (in schema): - "^(?:all| - (?:alveolata|bryophyta|bacillariophyta|amoebozoa|euglenozoa|fungi|chlorophyta|rhodophyta|phaeophyceae|marchantiophyta|metazoa|oomycota|haptophyceae|raphidophyceae|rhizaria|synurophyceae|tracheophyta|eustigmatophyceae|apusozoa|parabasalia) - (?:,\\s*(?:alveolata|bryophyta|bacillariophyta|amoebozoa|euglenozoa|fungi|chlorophyta|rhodophyta|phaeophyceae|marchantiophyta|metazoa|oomycota|haptophyceae|raphidophyceae|rhizaria|synurophyceae|tracheophyta|eustigmatophyceae|apusozoa|parabasalia))*)$" - - this forbids: - - mixing `all` with other values - - empty elements and trailing commas - - invalid values - */ - - def itsx_profiles = params.ITSx_tax - - // `ITSx_tax` must be a non-empty string (if specifying `--ITSx_tax ""`, Nextflow may coerce empty/flag to boolean) - if (itsx_profiles == null || itsx_profiles instanceof Boolean) { - println( errorMsg("Parameter --ITSx_tax must have a value (e.g. 'all' or 'fungi,rhizaria').", params.monochrome_logs) ) - exit(1) - } - if (itsx_profiles.toString().trim().isEmpty()) { - println( errorMsg("Parameter --ITSx_tax cannot be empty. Use 'all' or a comma-separated list of taxa.", params.monochrome_logs) ) - exit(1) - } - - // Allowed profiles - def ITSX_ALLOWED = [ - 'alveolata','bryophyta','bacillariophyta','amoebozoa','euglenozoa','fungi', - 'chlorophyta','rhodophyta','phaeophyceae','marchantiophyta','metazoa','oomycota', - 'haptophyceae','raphidophyceae','rhizaria','synurophyceae','tracheophyta', - 'eustigmatophyceae','apusozoa','parabasalia' - ] as Set - - // Parse the specified profile string - def itsx_items = itsx_profiles.toString().split(',', -1) as List - - // Empty-item validation (empty or whitespace-only tokens, incl. ",," and trailing commas) - def emptyIdx = [] - itsx_items.eachWithIndex { s, i -> - if (s == null || s.trim().isEmpty()) emptyIdx << i - } - if (emptyIdx) { - println( errorMsg("Parameter --ITSx_tax: empty entries are not allowed (check commas at positions: ${emptyIdx.join(', ')}).", params.monochrome_logs) ) - exit(1) - } - - // Disallow internal whitespaces - def whitespaces = itsx_items.findAll { s -> - def tr = s.toString().trim() - !(tr ==~ /\S+/) // after trimming, token must be all non-whitespace - } - if (whitespaces) { - println( errorMsg("Parameter --ITSx_tax: whitespace is not allowed in profile names.", params.monochrome_logs) ) - exit(1) - } - - // Detect duplicates - itsx_items = itsx_items.collect { it.trim() } - def dups = itsx_items.countBy { it }.findAll { k, v -> v > 1 }.keySet().toList() - if (dups) { - println( errorMsg("Parameter --ITSx_tax: duplicated profile names are not allowed: ${dups.join(', ')}", params.monochrome_logs) ) - exit(1) - } - - // Disallow mixing 'all' with specific profile names - if (itsx_items.size() > 1 && itsx_items.contains('all')) { - println( errorMsg("Parameter --ITSx_tax: do not combine 'all' with taxon-specific profile names.", params.monochrome_logs)) - exit(1) - } - - // Validate values against the allow-list (skip when it's exactly ['all']) - if (!(itsx_items.size() == 1 && itsx_items[0] == 'all')) { - def invalid_profiles = (itsx_items as Set) - ITSX_ALLOWED - if (invalid_profiles) { - println( errorMsg("Parameter --ITSx_tax: invalid profile names - ${invalid_profiles.join(', ')}", params.monochrome_logs) ) - println( colorize(" Supported profiles: `all` OR a comma-separated list of the following: ${ITSX_ALLOWED.join(', ')}", 'red', params.monochrome_logs)) - exit(1) - } - } - - // Currently, there is no X.hmm profile (Apusozoa) - if (itsx_items.contains('apusozoa')) { - println( errorMsg("Parameter --ITSx_tax: Apusozoa profile is not yet supported in ITSx.", params.monochrome_logs)) - exit(1) - } - - } // end of ITSx profiles validation - - -} // end of Step-1 parameter validation - - -// Additional parameter validation for Step-2 -if( params.step == "Step2" ) { - - if (params.preclustering == "none" && params.clustering == "none" && params.lulu == true){ - println errorMsg("LULU can not be applied when pre-clustering and clustering are set to 'none'", params.monochrome_logs) - exit(1) - } - - if (params.preclustering == "dada2" && params.dada2_pooling == "byrun" && - (params.chunking_n > 1 || params.chunking_n != null)){ - println errorMsg("By-sequencing-run pooling in DADA2 is not compatible with chunking.", params.monochrome_logs) - println( colorize("Set `--chunking_n` to 1 to disable chunking OR use `--dada2_pooling global`.", 'red', params.monochrome_logs)) - exit(1) - } - - -} // end of Step-2 parameter validation - - -// Run the workflow -workflow { - - // Print step-specific parameter summary - paramSummary(workflow, params) - validateParameters() - - if (params.step == "Step1") { - S1() - } - - if (params.step == "Step2") { - S2() - } - - if (params.step == "seqstats") { - seqstats() - } - -} - - -// On completion -workflow.onComplete { - println "Pipeline completed at : $workflow.complete" - println "Duration : ${workflow.duration}" - println "Execution status : ${workflow.success ? 'All done!' : 'Failed' }" -} - -// On error -workflow.onError { - println "Pipeline execution stopped with the following message: ${workflow.errorMessage}" -} diff --git a/src/pipecraft-core/service_scripts/NextITS/modules/Illumina_pe.nf b/src/pipecraft-core/service_scripts/NextITS/modules/Illumina_pe.nf deleted file mode 100644 index 34f33606..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/modules/Illumina_pe.nf +++ /dev/null @@ -1,484 +0,0 @@ - - - -// Quality filtering for pair-end reads -process qc_pe { - - label "main_container" - - // cpus 10 - - input: - path input_R1 - path input_R2 - - output: - path "QC_R1.fq.gz", emit: filtered_R1 - path "QC_R2.fq.gz", emit: filtered_R2 - - script: - filter_avgphred = params.qc_avgphred ? "--average_qual ${params.qc_avgphred}" : "--average_qual 0" - filter_phredmin = params.qc_phredmin ? "--qualified_quality_phred ${params.qc_phredmin}" : "" - filter_phredperc = params.qc_phredperc ? "--unqualified_percent_limit ${params.qc_phredperc}" : "" - filter_polyglen = params.qc_polyglen ? "--trim_poly_g --poly_g_min_len ${params.qc_polyglen}" : "" - """ - echo -e "QC\\n" - echo -e "Input R1: " ${input_R1} - echo -e "Input R2: " ${input_R2} - - ## If `filter_phredmin` && `filter_phredperc` are specified, - # Filtering based on percentage of unqualified bases - # how many percents of bases are allowed to be unqualified (Q < 24) - - fastp \ - --in1 ${input_R1} \ - --in2 ${input_R2} \ - --disable_adapter_trimming \ - --n_base_limit ${params.qc_maxn} \ - ${filter_avgphred} \ - ${filter_phredmin} \ - ${filter_phredperc} \ - ${filter_polyglen} \ - --length_required 100 \ - --thread ${task.cpus} \ - --html qc.html \ - --json qc.json \ - --out1 QC_R1.fq.gz \ - --out2 QC_R2.fq.gz - - echo -e "\\nQC finished" - """ -} - - - - - -// Demultiplexing with cutadapt - for Illumina PE reads (only not merged) -// NB. it's possible to use anchored adapters (e.g., -g ^file:barcodes.fa), -// but there could be a preceding nucleotides before the barcode, -// therefore, modified barcodes would be used here (e.g., XN{30}) -process demux_illumina_notmerged { - - label "main_container" - - publishDir "${out_1_demux}", mode: 'symlink' - // cpus 20 - - input: - path input_R1 - path input_R2 - path barcodes // barcodes_modified.fa (e.g., XN{30}) - - output: - path "*.fq.gz", emit: samples_demux - - script: - """ - echo -e "\nDemultiplexing not-merged reads" - - echo -e "Input R1: " ${input_R1} - echo -e "Input R2: " ${input_R2} - echo -e "Barcodes: " ${barcodes} - - ## First round - echo -e "\nRound 1:" - - cutadapt -g file:${barcodes} \ - -o round1-{name}.R1.fastq.gz \ - -p round1-{name}.R2.fastq.gz \ - --errors ${params.barcode_errors} \ - --overlap ${params.barcode_overlap} \ - --no-indels \ - --cores ${task.cpus} \ - ${input_R1} ${input_R2} \ - > cutadapt_round_1.log - - echo -e ".. round 1 finished" - - ## Second round - echo -e "\nRound 2:" - - cutadapt -g file:${barcodes} \ - -o round2-{name}.R2.fastq.gz \ - -p round2-{name}.R1.fastq.gz \ - --errors ${params.barcode_errors} \ - --overlap ${params.barcode_overlap} \ - --no-indels \ - --cores ${task.cpus} \ - round1-unknown.R2.fastq.gz round1-unknown.R1.fastq.gz \ - > cutadapt_round_2.log - - echo -e ".. round 2 finished" - - ## Remove empty files (no sequences) - echo -e "\nRemoving empty files" - find . -type f -name "round*.fastq.gz" -size -29c -print -delete - echo -e "..Done" - - ## Remove unknowns - echo -e "Removing unknowns" - rm round1-unknown.R{1,2}.fastq.gz - rm round2-unknown.R{1,2}.fastq.gz - - ## Combine sequences from round 1 and round 2 for each sample - echo -e "\nCombining sequences from round 1 and round 2 for each sample" - - mkdir -p Combined - - find . -name "round*.R1.fastq.gz" | sort | parallel -j1 \ - "cat {} >> Combined/{= s/round1-//; s/round2-// =}" - - find . -name "round*.R2.fastq.gz" | sort | parallel -j1 \ - "cat {} >> Combined/{= s/round1-//; s/round2-// =}" - - echo -e "..Done" - - ## Clean up - echo -e "..Removing temporary files" - find . -type f -name "round*.fastq.gz" -print -delete - - - - echo -e "\nDemultiplexing finished" - """ -} - - -// Trim primers of nonmerged PE reads -// + Estimate sequence qualities -process trim_primers_pe { - - label "main_container" - - publishDir "${out_3_trimPE}", mode: 'symlink' - // cpus 2 - - // Add sample ID to the log file - tag "${input.getSimpleName()}" - - input: - path input // tuple of size 2 - - output: - path "${input.getSimpleName()}_R1.fa.gz", emit: primertrimmed_fa_R1, optional: true - path "${input.getSimpleName()}_R2.fa.gz", emit: primertrimmed_fa_R2, optional: true - path "${input.getSimpleName()}_hash_table_R1.txt.gz", emit: hashes_R1, optional: true - path "${input.getSimpleName()}_hash_table_R2.txt.gz", emit: hashes_R2, optional: true - path "${input.getSimpleName()}_R1.fq.gz", emit: primertrimmed_fq_R1, optional: true - path "${input.getSimpleName()}_R2.fq.gz", emit: primertrimmed_fq_R2, optional: true - path "${input.getSimpleName()}_uc_R1.uc.gz", emit: ucR1, optional: true - path "${input.getSimpleName()}_uc_R2.uc.gz", emit: ucR2, optional: true - - script: - sampID="${input.getSimpleName()}" - - """ - echo -e "Input sample: " ${sampID} - echo -e "Forward primer: " ${params.primer_forward} - echo -e "Reverse primer: " ${params.primer_reverse} - - ## Reverse-complement primers - FR=\$(rc.sh ${params.primer_forward}) - RR=\$(rc.sh ${params.primer_reverse}) - - echo -e "Forward primer RC: " "\$FR" - echo -e "Reverse primer RC: " "\$RR" - - ## Discard sequences without both primers - echo -e "\nChecking primers" - - echo -e "..Forward strain" - - cutadapt \ - -a ${params.primer_forward}";required;min_overlap=${params.primer_foverlap}"..."\$RR"";required;min_overlap=${params.primer_roverlap}" \ - --errors ${params.primer_mismatches} \ - --cores ${task.cpus} \ - --action=none \ - --discard-untrimmed \ - -o for_R1.fastq.gz -p for_R2.fastq.gz \ - ${input[0]} ${input[1]} \ - > cutadapt_1.log - - - echo -e "..Reverse strain" - - cutadapt \ - -a ${params.primer_reverse}";required;min_overlap=${params.primer_roverlap}"..."\$FR"";required;min_overlap=${params.primer_foverlap}" \ - --errors ${params.primer_mismatches} \ - --cores ${task.cpus} \ - --action=none \ - --discard-untrimmed \ - -p rev_R1.fastq.gz -o rev_R2.fastq.gz \ - ${input[0]} ${input[1]} \ - > cutadapt_2.log - - # cutadapt \ - # -a FWDPRIMER...RCREVPRIMER \ - # -A REVPRIMER...RCFWDPRIMER \ - # --discard-untrimmed \ - # -o out.1.fastq.gz -p out.2.fastq.gz \ - # in.1.fastq.gz in.2.fastq.gz - - - echo -e "\nReorienting" - - if [ -s for_R1.fastq.gz ]; then - zcat for_R1.fastq.gz | seqkit replace -p "\\s.+" | gzip -7 > OK_R1.fastq.gz - zcat for_R2.fastq.gz | seqkit replace -p "\\s.+" | gzip -7 > OK_R2.fastq.gz - fi - - if [ -s rev_R1.fastq.gz ]; then - echo -e "..Adding sequences to the main pool" - zcat rev_R1.fastq.gz | seqkit replace -p "\\s.+" | gzip -7 >> OK_R1.fastq.gz - zcat rev_R2.fastq.gz | seqkit replace -p "\\s.+" | gzip -7 >> OK_R2.fastq.gz - - else - echo -e "..Probably all sequences are in forward orientation" - fi - - - echo -e "\nTrimming primers" - if [ -s OK_R1.fastq.gz]; then - - cutadapt \ - -a ${params.primer_forward}";required;min_overlap=${params.primer_foverlap}"..."\$RR"";required;min_overlap=${params.primer_roverlap}" \ - --errors ${params.primer_mismatches} \ - --cores ${task.cpus} \ - --action=trim \ - --discard-untrimmed \ - --minimum-length ${params.trim_minlen} \ - --output ${sampID}_R1.fq.gz --paired-output ${sampID}_R2.fq.gz \ - OK_R1.fastq.gz OK_R2.fastq.gz - - fi - - - ## Quality estimation and dereplication - - if [ -s ${sampID}_R1.fq.gz ]; then - - ## Estimate sequence quality (for the extracted region) - ## Sequence ID - Hash - Length - Average Phred score - echo -e "\nCreating sequence hash table with average sequence quality" - - seqkit fx2tab --length --avg-qual ${sampID}_R1.fq.gz \ - | hash_sequences.sh \ - | awk '{print \$1 "\t" \$6 "\t" \$4 "\t" \$5}' \ - > tmp_hash_table_R1.txt - - seqkit fx2tab --length --avg-qual ${sampID}_R2.fq.gz \ - | hash_sequences.sh \ - | awk '{print \$1 "\t" \$6 "\t" \$4 "\t" \$5}' \ - > tmp_hash_table_R2.txt - - echo -e "..Done" - - - ## Estimating MaxEE - echo -e "\nEstimating maximum number of expected errors per sequence" - - vsearch \ - --fastx_filter ${sampID}_R1.fq.gz \ - --fastq_qmax 93 \ - --eeout \ - --fastaout - \ - | seqkit seq --name \ - | sed 's/;ee=/\t/g' \ - > tmp_ee_R1.txt - - vsearch \ - --fastx_filter ${sampID}_R2.fq.gz \ - --fastq_qmax 93 \ - --eeout \ - --fastaout - \ - | seqkit seq --name \ - | sed 's/;ee=/\t/g' \ - > tmp_ee_R2.txt - - echo -e "..Done" - - - echo -e "\nMerging quality estimates" - - max_ee.R \ - tmp_hash_table_R1.txt \ - tmp_ee_R1.txt \ - ${sampID}_hash_table_R1.txt - - max_ee.R \ - tmp_hash_table_R2.txt \ - tmp_ee_R2.txt \ - ${sampID}_hash_table_R2.txt - - echo -e "..Done" - - - ## Independent dereplication of pair-end reads - echo -e "\nDereplicating R1 and R2 (independently)" - - seqkit fq2fa -w 0 ${sampID}_R1.fq.gz \ - | vsearch \ - --derep_fulllength - \ - --output - \ - --strand both \ - --fasta_width 0 \ - --threads 1 \ - --relabel_sha1 \ - --sizein --sizeout \ - --uc ${sampID}_uc_R1.uc \ - --quiet \ - | gzip -7 \ - > ${sampID}_R1.fa.gz - - seqkit fq2fa -w 0 ${sampID}_R2.fq.gz \ - | vsearch \ - --derep_fulllength - \ - --output - \ - --strand both \ - --fasta_width 0 \ - --threads 1 \ - --relabel_sha1 \ - --sizein --sizeout \ - --uc ${sampID}_uc_R2.uc \ - --quiet \ - | gzip -7 \ - > ${sampID}_R2.fa.gz - - - echo -e "..Done" - - ## Compress results - echo -e "Compressing result" - gzip -7 ${sampID}_hash_table_R1.txt - gzip -7 ${sampID}_hash_table_R2.txt - gzip -7 ${sampID}_uc_R1.uc - gzip -7 ${sampID}_uc_R2.uc - - - else - echo -e "\nNo sequences found after primer removal" - fi - - ## Clean up - if [ -f for_R1.fastq.gz ]; then rm for_R1.fastq.gz; fi - if [ -f for_R2.fastq.gz ]; then rm for_R2.fastq.gz; fi - if [ -f rev_R1.fastq.gz ]; then rm rev_R1.fastq.gz; fi - if [ -f rev_R2.fastq.gz ]; then rm rev_R2.fastq.gz; fi - if [ -f OK_R1.fastq.gz ]; then rm OK_R1.fastq.gz; fi - if [ -f OK_R2.fastq.gz ]; then rm OK_R2.fastq.gz; fi - - echo -e "..Done" - - """ -} - - - - -// Combine paired reads into single sequences -// by reverse-complementing the reverse read and inserting poly-N padding -// + Estimate sequence qualities (without N pads!) -process join_pe { - - label "main_container" - - // publishDir "${out_1_joinPE}", mode: "${params.storagemode}" - // cpus 2 - - // Add sample ID to the log file - tag "${input}" - - input: - val input // Sample name "(e.g., Barcode07_1__IS859)" - path all_samples - - output: - path "${input}_JoinedPE.fq.gz", emit: jj_FQ, optional: true - path "${input}_JoinedPE_hash_table.txt.gz", emit: jj_hashes, optional: true - - script: - sampID="${input}" - - """ - echo -e "Joining non-merged Illumina reads" - echo -e "Input sample: " ${sampID} - - echo -e "\\nJoining with N-pads" - vsearch \ - --fastq_join ${input}.R1.fastq.gz \ - --reverse ${input}.R2.fastq.gz \ - --join_padgap ${params.illumina_joinpadgap} \ - --join_padgapq ${params.illumina_joinpadqual} \ - --fastqout - \ - | seqkit replace -p "\\s.+" \ - | gzip -${params.gzip_compression} \ - > ${sampID}_JoinedPE.fq.gz - - ## Check if there are some sequences in the file - if [ -n "\$(find . -name ${sampID}_JoinedPE.fq.gz -prune -size +29c)" ]; then - - echo -e "\\nJoining without N-pads (for quality estimation)" - vsearch \ - --fastq_join ${input}.R1.fastq.gz \ - --reverse ${input}.R2.fastq.gz \ - --join_padgap "" \ - --join_padgapq "" \ - --fastqout - \ - | seqkit replace -p "\\s.+" \ - | gzip -${params.gzip_compression} \ - > tmp_for_qual.fq.gz - - - ## Estimate sequence quality (without N pads!) - ## Sequence ID - Hash - Length - Average Phred score - echo -e "\\nCreating sequence hash table with average sequence quality" - - seqkit fx2tab --length --avg-qual tmp_for_qual.fq.gz \ - | hash_sequences.sh \ - | awk '{print \$1 "\t" \$6 "\t" \$4 "\t" \$5}' \ - > tmp_hash_table.txt - - echo -e "..Done" - - ## Estimating MaxEE - echo -e "\\nEstimating maximum number of expected errors per sequence" - - vsearch \ - --fastx_filter tmp_for_qual.fq.gz \ - --fastq_qmax 93 \ - --eeout \ - --fastaout - \ - | seqkit seq --name \ - | sed 's/;ee=/\t/g' \ - > tmp_ee.txt - - echo -e "..Done" - - echo -e "\\nMerging quality estimates" - - max_ee.R \ - tmp_hash_table.txt \ - tmp_ee.txt \ - ${sampID}_JoinedPE_hash_table.txt - - echo -e "..Done" - - ## Compress results - gzip -${params.gzip_compression} ${sampID}_JoinedPE_hash_table.txt - - ## Clean up - rm tmp_for_qual.fq.gz - rm tmp_hash_table.txt - rm tmp_ee.txt - - else - echo -e "\\nIt looks like there are no joined reads" - fi - - ## Remove redundant symlinks - find -L . -name "*.fastq.gz" | grep -v ${input} | parallel -j1 "rm {}" - - """ -} - diff --git a/src/pipecraft-core/service_scripts/NextITS/modules/colors.nf b/src/pipecraft-core/service_scripts/NextITS/modules/colors.nf deleted file mode 100644 index ca6ccb42..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/modules/colors.nf +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Color utilities for NextITS pipeline - * Provides ANSI color codes and helper functions for terminal output - */ - -/** - * Get color codes map based on monochrome setting - * @param monochrome Whether to disable colors (from params.monochrome_logs) - * @return Map of color names to ANSI codes - */ -def getColors(boolean monochrome = false) { - return monochrome ? [:] : [ - // Basic colors - black: "\033[0;30m", - red: "\033[0;31m", - green: "\033[0;32m", - yellow: "\033[0;33m", - blue: "\033[0;34m", - purple: "\033[0;35m", - cyan: "\033[0;36m", - white: "\033[0;37m", - - // Bright colors - bright_black: "\033[0;90m", - bright_red: "\033[0;91m", - bright_green: "\033[0;92m", - bright_yellow: "\033[0;93m", - bright_blue: "\033[0;94m", - bright_purple: "\033[0;95m", - bright_cyan: "\033[0;96m", - bright_white: "\033[0;97m", - - // Text formatting - bold: "\033[1m", - dim: "\033[2m", - italic: "\033[3m", - underline: "\033[4m", - blink: "\033[5m", - reverse: "\033[7m", - - // Reset - reset: "\033[0m" - ] -} - -/** - * Apply color formatting to text - * @param text The text to colorize - * @param color The color name (e.g., 'red', 'green', 'bold') - * @param monochrome Whether to disable colors - * @return Formatted text string - */ -def colorize(String text, String color, boolean monochrome = false) { - def colors = getColors(monochrome) - if (!colors[color]) { - return text - } - return "${colors[color]}${text}${colors.reset}" -} - -/** - * Apply multiple color/format combinations to text - * @param text The text to colorize - * @param formats List of format names (e.g., ['red', 'bold']) - * @param monochrome Whether to disable colors - * @return Formatted text string - */ -def colorizeMultiple(String text, List formats, boolean monochrome = false) { - def colors = getColors(monochrome) - if (monochrome || !formats) { - return text - } - - def prefix = formats.findAll { colors[it] }.collect { colors[it] }.join('') - return "${prefix}${text}${colors.reset}" -} - -/** - * Create an error message with red coloring - * @param message The error message text - * @param monochrome Whether to disable colors - * @return Formatted error message - */ -def errorMsg(String message, boolean monochrome = false) { - return colorizeMultiple("ERROR: ${message}", ['red', 'bold'], monochrome) -} - -/** - * Create a warning message with yellow coloring - * @param message The warning message text - * @param monochrome Whether to disable colors - * @return Formatted warning message - */ -def warningMsg(String message, boolean monochrome = false) { - return colorizeMultiple("WARNING: ${message}", ['yellow', 'bold'], monochrome) -} - -/** - * Create an info message with cyan coloring - * @param message The info message text - * @param monochrome Whether to disable colors - * @return Formatted info message - */ -def infoMsg(String message, boolean monochrome = false) { - return colorize(message, 'cyan', monochrome) -} - -/** - * Create a success message with green coloring - * @param message The success message text - * @param monochrome Whether to disable colors - * @return Formatted success message - */ -def successMsg(String message, boolean monochrome = false) { - return colorizeMultiple(message, ['green', 'bold'], monochrome) -} diff --git a/src/pipecraft-core/service_scripts/NextITS/modules/dump_parameters.nf b/src/pipecraft-core/service_scripts/NextITS/modules/dump_parameters.nf deleted file mode 100644 index a27426d9..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/modules/dump_parameters.nf +++ /dev/null @@ -1,48 +0,0 @@ - -// Custom function to dump pipeline parameters to a TSV file - - -/* - * Flatten a nested map into dot-notated key/value pairs - * Example: [foo:[bar:1]] -> [[ "foo.bar", 1 ]] - */ -def flattenMap(Map m, String prefix = '') { - def out = [] - m.each { k, v -> - def key = prefix ? "${prefix}.${k}" : (k as String) - if( v instanceof Map ) - out.addAll( flattenMap((Map)v, key) ) - else - out << [ key, v ] - } - return out -} - -/* - * Make a value TSV-safe and readable - * - Converts collections to comma-separated lists - * - Normalizes tabs/newlines so the file stays valid TSV - */ -def tsvValue(Object v) { - if( v == null ) return 'null' - if( v instanceof Map ) return v.collect { kk, vv -> "${kk}:${tsvValue(vv)}" }.join(',') - if( v instanceof Collection ) return v.collect { tsvValue(it) }.join(',') - def s = v.toString() - return s.replace('\t',' ') - .replace('\r','\\r') - .replace('\n','\\n') -} - -/* - * Main function (exported in other files) - * Return a channel emitting one string per line: "param\tvalue" - */ -def dumpParamsTsv() { - assert params instanceof Map : 'params must be map-like' - def pairs = flattenMap((Map)params).sort { a, b -> a[0] <=> b[0] } - // If you want a header, uncomment the next line - // def lines = ['param\tvalue'] + pairs.collect { k,v -> "${k}\t${tsvValue(v)}" } - def lines = pairs.collect { k,v -> "${k}\t${tsvValue(v)}" } - // Emit each line on the channel (one item per line) - return channel.fromList(lines) // value/queue channel factory methods are standard :contentReference[oaicite:0]{index=0} -} diff --git a/src/pipecraft-core/service_scripts/NextITS/modules/help_message.nf b/src/pipecraft-core/service_scripts/NextITS/modules/help_message.nf deleted file mode 100644 index 663d82dc..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/modules/help_message.nf +++ /dev/null @@ -1,96 +0,0 @@ - - -// Pipeline help message -def helpMsg() { - log.info""" - ===================================================================== - NextITS v.${workflow.manifest.version} - ===================================================================== - - Pipeline Usage: - To run the pipeline, enter the following in the command line: - nextflow run vmikk/nextits -r ${workflow.manifest.version} --input ... --outdir ... - - Options: - REQUIRED: - --input File with single-end input sequences, PacBio (FASTQ or BAM) or a directory with pre-demultiplexed files - --input_R1 Files with paired-end input sequences, Illumina (FASTQ) - --input_R2 - --barcodes Barcodes for demultiplexing (FASTA) - --outdir The output directory where the results will be saved - - OPTIONAL: - --demultiplexed Boolean, input is multiplexed (true, single FASTQ file) or pre-demultiplexed (multiple FASTQ files) - --seqplatform Sequencing platform type - "PacBio" (default) or "Illumina" - --its_region ITS part selector - "full" (defalut), "ITS1", "ITS2", "none" (trims primers only), or "ITS1_5.8S_ITS2" - --primer_forward Forward primer sequence (default, ITS9mun) - --primer_reverse Reverse primer sequence (default, ITS4ngsUni) - --primer_mismatches - --primer_foverlap Min primer overlap (default, F primer length - 2) - --primer_roverlap Min primer overlap (default, R primer length - 2) - --qc_maxn Discard sequences with more than the specified number of N’s - --trim_minlen Min sequence length after primer trimming (default, 10) - --ITSx_tax ITSx taxonomy profile (default, "all") - --ITSx_evalue ITSx E-value cutoff threshold (default, 1e-1) - --ITSx_partial Keep partial ITS sequences (defalt, off), otherwise specify min length cutoff - --hp Homopolymer compression (default, true) - --hp_similarity Allowed sequence similarity for homopolymer compression (default, 0.999) - --hp_iddef Sequence similarity definition for homopolymer compression (default, 2) - - # Chimera identification - --chimera_db Database for reference-based chimera removal - --chimera_rescueoccurrence Min occurrence of chimeric sequences required to rescue them (default, 2) - --chimeranov_abskew De novo chimera identification `abskew` parameter (default, 2.0) - --chimeranov_dn De novo chimera identification `dn` parameter (default, 1.4) - --chimeranov_mindiffs De novo chimera identification `mindiffs` parameter (default, 3) - --chimeranov_mindiv De novo chimera identification `mindiv` parameter (default, 0.8) - --chimeranov_minh De novo chimera identification `minh` parameter (default, 0.28) - --chimeranov_xn De novo chimera identification `xn` parameter (default, 8.0) - - # Tag-jump removal - --tj_f Tag-jump filtering, UNCROSS parameter `f` (default, 0.01) - --tj_p Tag-jump filtering parameter `p` (default, 1) - --otu_id Sequence similarity for OTU clustering (default, 0.98) - --otu_iddef Sequence similarity definition for tag-jump removal step (default, 2) - - # PacBio-specific parameters - --lima_barcodetype Tag type ("single", "dual", "dual_symmetric", "dual_asymmetric") - --lima_minscore Minimum barcode score for demultiplexing (default, 93) - --lima_minendscore Minimum second barcode score (only for asymmetric and dual barcoding scheme; default, 50) - --lima_minrefspan Minimum read span relative to the barcode length (0-1; default, 0.75) - --lima_minscoringregions Number of barcodes scored required for demultiplexing using dual barcodes (default, 2 = requires both barcodes) - --lima_windowsize Window size for barcode lookup (default, 70 bp) - --lima_minlen Minimum sequence length after clipping barcodes (default, 40) - --qc_maxee Maximum number of expected errors (default, false) - --qc_maxeerate Maximum number of expected errors per base (default, 0.01) - --qc_maxhomopolymerlen Threshold for a homopolymer region length in a sequence (default, 25) - - # Illumina-specific parameters - --qc_avgphred Average Phred score for QC (default, false) - --qc_twocolor Enable two-color chemistry mode, e.g. for Illumina NovaSeq (default, false) - --qc_phredmin Two-color mode: min Phred score of qualified bases (default, 24) - --qc_phredperc Two-color mode: Percentage of bases allowed to be unqualified (default, 30) - --qc_polyglen Two-color mode: minimum length of polyG tail (default, 8) - --barcode_window Window size for barcode lookup (default, 30 bp) - --barcode_errors Maximum allowed number of errors in barcodes (default, 1) - --barcode_overlap Min overlap between read and barcode (default, 11) - --pe_minoverlap Min length to detect overlapped region of PE reads (default, 20) - --pe_difflimit Max number of mismatched bases in PE overlap (default, 5) - --pe_diffperclimit Max percentage of mismatched bases in PE overlap (default, 20) - --pe_minlen Min length of merged sequences (default, 30) - --illumina_keep_notmerged Keep not merged Illumina reads (default, true) - --illumina_joinpadgap Join not merged reads into one sequence using padding sequence string (default, NNNNNNNNNN) - --illumina_joinpadqual Join not merged reads into one sequence using padding quality string (default, IIIIIIIIII) - - # Miscellaneous parameters - --gzip_compression Compression level for GZIP (default, 7; 1 = fastest (worst compression), 9 = slowest (best)) - - NEXTFLOW-SPECIFIC: - -profile Configuration profile - -resume Execute the pipeline using the cached results (e.g., in case of ) - -work-dir Path to the directory where intermediate result files are stored - -qs Queue size (max number of processes that can be executed in parallel); e.g., 8 - -r Pipeline version to run (GitHub branch, tag, or SHA number) - """.stripIndent() -} - diff --git a/src/pipecraft-core/service_scripts/NextITS/modules/parameter_summary.nf b/src/pipecraft-core/service_scripts/NextITS/modules/parameter_summary.nf deleted file mode 100644 index d07b2d2f..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/modules/parameter_summary.nf +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Custom parameter summary function for NextITS pipeline - * Shows only step-specific parameters and relevant configuration - */ - -def parameterSummary(workflow, params) { - - // Color definitions - def colors = params.monochrome_logs ? [:] : [ - green: "\033[0;32m", blue: "\033[0;34m", yellow: "\033[0;33m", - cyan: "\033[0;36m", purple: "\033[0;35m", dim: "\033[2m", - bold: "\033[1m", reset: "\033[0m" - ] - - def summary = "" - summary += "${colors.bold}Parameters:${colors.reset}\n" - summary += " ${colors.green}step${colors.reset} : ${colors.cyan}${params.step}${colors.reset}\n" - - // Step-specific parameters - if (params.step == "Step1" || params.step == "seqstats") { - - if (params.seqplatform == "PacBio") { - if (params.input) { - summary += " ${colors.green}input${colors.reset} : ${colors.cyan}${params.input}${colors.reset}\n" - } - } else if (params.seqplatform == "Illumina") { - if (params.input_R1) { - summary += " ${colors.green}input_R1${colors.reset} : ${colors.cyan}${params.input_R1}${colors.reset}\n" - } - if (params.input_R2) { - summary += " ${colors.green}input_R2${colors.reset} : ${colors.cyan}${params.input_R2}${colors.reset}\n" - } - } - - if (params.barcodes && !params.demultiplexed) { - summary += " ${colors.green}barcodes${colors.reset} : ${colors.cyan}${params.barcodes}${colors.reset}\n" - } - - if (params.demultiplexed) { - summary += " ${colors.green}demultiplexed${colors.reset} : ${colors.cyan}${params.demultiplexed}${colors.reset}\n" - } - - summary += " ${colors.green}chimera_db${colors.reset} : ${colors.cyan}${params.chimera_db}${colors.reset}\n" - if(params.step == "Step1") { - summary += " ${colors.green}its_region${colors.reset} : ${colors.cyan}${params.its_region}${colors.reset}\n" - } - - } - else if (params.step == "Step2") { - summary += "\n${colors.bold}Step 2 inputs:${colors.reset}\n" - summary += " ${colors.green}data_path${colors.reset} : ${colors.cyan}${params.data_path}${colors.reset}\n" - summary += " ${colors.green}clustering${colors.reset} : ${colors.cyan}${params.clustering}${colors.reset}\n" - summary += " ${colors.green}preclustering${colors.reset} : ${colors.cyan}${params.preclustering}${colors.reset}\n" - } - - - // Common parameters - summary += "\n${colors.bold}Output:${colors.reset}\n" - summary += " ${colors.green}outdir${colors.reset} : ${colors.cyan}${params.outdir}${colors.reset}\n" - summary += " ${colors.green}workDir${colors.reset} : ${colors.cyan}${workflow.workDir}${colors.reset}\n" - - // Nextflow configuration - summary += "\n${colors.bold}Config:${colors.reset}\n" - summary += " ${colors.green}NextITS version${colors.reset} : ${colors.cyan}${workflow.manifest.version}${colors.reset}\n" - if(workflow.commitId) { - summary += " ${colors.green}NextITS revision${colors.reset} : ${colors.cyan}${workflow.commitId.substring(0, 7)}${colors.reset}\n" - } - summary += " ${colors.green}Profile${colors.reset} : ${colors.cyan}${workflow.profile}${colors.reset}\n" - summary += " ${colors.green}Container engine${colors.reset} : ${colors.cyan}${workflow.containerEngine ?: 'none'}${colors.reset}\n" - - if (workflow.container) { - summary += " ${colors.green}Container${colors.reset} : ${colors.cyan}${workflow.container}${colors.reset}\n" - } - - return summary -} - -// Export the function so it can be used in other files -workflow paramSummary { - take: - wf - prms - - main: - def summary = parameterSummary(wf, prms) - log.info summary -} diff --git a/src/pipecraft-core/service_scripts/NextITS/modules/version_parser.nf b/src/pipecraft-core/service_scripts/NextITS/modules/version_parser.nf deleted file mode 100644 index b93492ff..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/modules/version_parser.nf +++ /dev/null @@ -1,28 +0,0 @@ - -// Custom function to parse software versions and return a YAML string - -def software_versions_to_yaml(versions) { - - def workflow_info = Channel.of( - "NextITS:\n" + - " version: ${workflow.manifest.version}\n" + - (workflow.commitId ? " revision: ${workflow.commitId.substring(0,7)}\n" : "") + - "\nNextflow:\n" + - " version: ${nextflow.version}\n" - ) - - return workflow_info.mix( - versions - .unique() - .map { name, tool, version -> - [ name.tokenize(':')[-1], [ tool, version ] ] - } - .groupTuple() - .map { processName, toolInfo -> - def toolVersions = toolInfo.collect { tool, version -> " ${tool}: ${version}" }.join('\n') - "${processName}:\n${toolVersions}\n" - } - .map { it.trim() } - ) -} - diff --git a/src/pipecraft-core/service_scripts/NextITS/nextflow.config b/src/pipecraft-core/service_scripts/NextITS/nextflow.config deleted file mode 100644 index a723b6d3..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/nextflow.config +++ /dev/null @@ -1,459 +0,0 @@ -/* -============================================================================== - NextITS: Pipeline to process fungal ITS amplicons -============================================================================== - Default config options for all compute environments ------------------------------------------------------------------------------- -*/ - - -// Enable nf-schema for parameter validation -plugins { - id 'nf-schema@2.4.1' -} - -// Global default params, used in configs -includeConfig 'conf/params.config' - -// Configure validation behavior -validation { - parametersSchema = "${projectDir}/nextflow_schema.json" - monochromeLogs = false - failUnrecognisedParams = false - lenientMode = true - - // Help options - help { - enabled = true - command = "nextflow run vmikk/NextITS" - shortParameter = "help" - fullParameter = "helpFull" - showHiddenParameter = "showHidden" - showHidden = false - beforeText = "NextITS: Pipeline to process rRNA amplicons sequenced with PacBio" - afterText = """ -If you use NextITS for your analysis please cite: - -* NextITS pipeline - Mikryukov V, Anslan S, Tedersoo L. - NextITS: a pipeline for metabarcoding fungi and other eukaryotes with full-length ITS sequenced with PacBio. - https://github.com/vmikk/NextITS - -* Software dependencies - https://github.com/vmikk/NextITS/blob/main/CITATIONS.md - """ - } - - // Summary options - summary { - beforeText = "NextITS pipeline parameters:" - // afterText = "" - } -} - -// Conditional params -if (params.qc_twocolor == true) { - includeConfig 'conf/params_illumina_2color.config' -} else { - includeConfig 'conf/params_illumina_4color.config' -} - -// Ignore process selector warnings, -// could be disabled using the `devel` profile -nextflow.enable.configProcessNamesValidation = false - - -// Process-specific parameters -process { - - ////////// Step-1 processes - - // Converting BAM to FASTQ - withName: 'S1:bam2fastq'{ - cpus = 4 - } - - // QC - PacBio single-end reads - // vsearch currently does not suppot multithreading for `--fastq_filter` - // see https://github.com/torognes/vsearch/issues/466 - withName: 'S1:qc_se'{ - cpus = 1 - } - - // QC - Illumina paired-end reads - withName: 'S1:qc_pe'{ - // max threads for fastp = 16 - cpus = 8 - } - - // Primer disambiguation - withName: 'S1:disambiguate'{ - cpus = 1 - } - - // Validate tags for demultiplexing - withName: 'S1:tag_validation'{ - cpus = 1 - } - - // Demultiplexing of PacBio reads (with LIMA) - withName: 'S1:demux'{ - cpus = 8 - } - - // Demultiplexing merged Illumina reads - withName: 'S1:demux_illumina'{ - cpus = 8 - } - - // Demultiplexing non-merged Illumina reads - withName: 'S1:demux_illumina_notmerged'{ - cpus = 8 - } - - // Merging of Illumina PE reads - withName: 'S1:merge_pe'{ - cpus = 8 - } - - // Modify barcodes for cutadapt (Illumina only) - withName: 'S1:prep_barcodes'{ - cpus = 1 - } - - // Demultiplexing of Illumina reads (with cutadapt) - withName: 'S1:demux_illumina'{ - cpus = 8 - } - - // Check primers - withName: 'S1:primer_check'{ - cpus = 1 - } - - // ITSx - withName: 'S1:itsx'{ - cpus = 3 - } - - // Collect all ITS parts extracted by ITSx - withName: 'S1:itsx_collect'{ - cpus = 1 - } - - // Sequence quality tables - withName: 'S1:seq_qual'{ - cpus = 4 - memory = null - } - - // Homopolymer compression - withName: 'S1:homopolymer' { - cpus = 1 - } - - // Reference-based chimera removal - withName: 'S1:CHIMERA_REMOVAL:chimera_ref' { - cpus = 1 - } - - // Chimera rescue - withName: 'S1:CHIMERA_REMOVAL:chimera_rescue' { - cpus = 1 - } - - // De novo chimera search - withName: 'S1:CHIMERA_REMOVAL:chimera_denovo' { - cpus = 1 - } - - // Aggregate de novo chimeras - withName: 'S1:CHIMERA_REMOVAL:chimera_denovo_agg' { - cpus = 1 - } - - // Pool sequences (for tag-jump removal and final sequence table creation) - withName: 'S1:pool_seqs' { - cpus = 3 - } - - // Dereplication or pre-clustering prior to tag-jump removal - withName: 'S1:tj_preclust' { - cpus = 8 - } - - // Tag-jump removal - withName: 'S1:tj' { - cpus = 1 - } - - // Create sequence table - withName: 'S1:prep_seqtab' { - cpus = 4 - } - - // Read count summary - withName: 'S1:read_counts' { - cpus = 4 - } - - // Read count summary - // For a quick workflow for demultiplexing and estimation of the number of reads per sample - withName: 'S1:quick_stats' { - cpus = 4 - } - - - - ////////// Step-2 processes - - // Aggregate sequences, remove de novo chimeras - withName: 'S2:aggregate_sequences' { - cpus = 8 - } - - // Dereplication (currently, only single-threaded; only comression is multithreaded)) - withName: 'S2:dereplication' { - cpus = 8 - } - - // 100% clustering with sequence length variation allowed (UNITE-style) - withName: 'S2:dereplication_unite' { - cpus = 8 - } - - // Pre-clustering - withName: 'S2:linclust' { - cpus = 8 - } - - // Bucketize sequences into clusters - withName: 'S2:bucketize' { - cpus = 6 - } - - // UNOISE - withName: 'S2:CLUSTERING:unoise' { - cpus = 8 - } - - // DADA2 - withName: 'S2:CLUSTERING:dada2' { - cpus = 8 - } - - // VSEARCH clustering - withName: 'S2:CLUSTERING:cluster_vsearch' { - cpus = 8 - } - - // SWARM clustering - withName: 'S2:CLUSTERING:cluster_swarm' { - cpus = 8 - } - - // Bucketizing workflow - merge chunks into a single file - withName: 'S2:merge_buckets' { - cpus = 4 - } - - // Merge UC files - withName: 'S2:merge_uc' { - cpus = 4 - } - - // Summarize sequence abundance by OTU - withName: 'S2:summarize' { - cpus = 4 - } - - // Post-clustering curation with LULU - withName: 'S2:lulu' { - cpus = 8 - } - -} // end of process configs - - - - -profiles { - - // Test config (built-in data) - test { includeConfig 'conf/test_step1.config' } // Step-1 (default test = test for Step-1) - test1 { includeConfig 'conf/test_step1.config' } // Step-1 - test2 { includeConfig 'conf/test_step2.config' } // Step-2 - - // Docker-based profile - docker { - docker.enabled = true - conda.enabled = false - singularity.enabled = false - apptainer.enabled = false - podman.enabled = false - shifter.enabled = false - charliecloud.enabled = false - docker.runOptions = '-u $(id -u):$(id -g)' - - // Container specifications are here - includeConfig 'conf/docker.config' - } - - // Singularity-based profile - singularity { - singularity.enabled = true - singularity.autoMounts = true - // singularity.ociAutoPull = true - conda.enabled = false - docker.enabled = false - podman.enabled = false - shifter.enabled = false - charliecloud.enabled = false - apptainer.enabled = false - - // Container specifications are here - includeConfig 'conf/singularity.config' - } - // Podman-based profile - podman { - podman.enabled = true - docker.enabled = false - conda.enabled = false - singularity.enabled = false - apptainer.enabled = false - podman.enabled = false - shifter.enabled = false - charliecloud.enabled = false - - // Container specifications are the same as for Docker - includeConfig 'conf/docker.config' - } - - - - // Larger resource amount (e.g., CPUs) for execution on HPC - hpc { - includeConfig 'conf/hpc.config' - } - - // Profile for the UT HPC cluster (SLURM) - hpc_utslurm { - includeConfig 'conf/hpc_utslurm.config' - } - - // Enable process selector warnings - // see https://github.com/nextflow-io/nextflow/issues/2700#issuecomment-1383984109 - devel { - nextflow.enable.configProcessNamesValidation = true - } - -} // end of profiles - - - -// Export these variables to prevent local Python/R libraries from conflicting with those in the container -env { - PYTHONNOUSERSITE = 1 - R_PROFILE_USER = "/.Rprofile" - R_ENVIRON_USER = "/.Renviron" - JULIA_DEPOT_PATH = "/usr/local/share/julia" -} - -// Capture exit codes from upstream processes when piping -process.shell = ['/bin/bash', '-euo', 'pipefail'] - - -def trace_timestamp = new java.util.Date().format( 'yyyy-MM-dd_HH-mm-ss') -timeline { - enabled = true - file = "${params.tracedir}/execution_timeline_${trace_timestamp}.html" -} -report { - enabled = true - file = "${params.tracedir}/execution_report_${trace_timestamp}.html" -} -trace { - enabled = true - file = "${params.tracedir}/execution_trace_${trace_timestamp}.txt" -} -dag { - enabled = true - file = "${params.tracedir}/pipeline_dag_${trace_timestamp}.svg" -} - - -manifest { - name = 'NextITS' - author = 'Vladimir Mikryukov' // The author field is deprecated, keep it here for compatibility with Nextflow versions < 24.10.0 - - contributors = [ - [ - name: 'Vladimir Mikryukov', - affiliation: 'University of Tartu', - email: 'vladimir.mikryukov@ut.ee', - github: '@vmikk', - contribution: ["author", "maintainer"], - orcid: '0009-0006-0086-2470' - ], - [ - name: 'Sten Anslan', - affiliation: 'University of Tartu', - email: 'sten.anslan@ut.ee', - github: '@anslan', - contribution: ["contributor"], - orcid: '0000-0002-2299-454X' - ], - [ - name: 'Leho Tedersoo', - affiliation: 'University of Tartu', - email: 'leho.tedersoo@ut.ee', - contribution: ["contributor"], - orcid: '0000-0002-1635-1249' - ], - ] - - homePage = 'https://github.com/vmikk/NextITS' - docsUrl = 'https://Next-ITS.github.io/' - description = 'Pipeline to process long rRNA amplicons sequenced with PacBio' - license = 'Apache-2.0' - mainScript = 'main.nf' - nextflowVersion = '!>=25.04.0' - version = '1.1.0' - doi = '10.5281/zenodo.15074881' -} - - - -// Function to ensure that resource requirements don't go beyond -// a maximum limit -def check_max(obj, type) { - if (type == 'memory') { - try { - if (obj.compareTo(params.max_memory as nextflow.util.MemoryUnit) == 1) - return params.max_memory as nextflow.util.MemoryUnit - else - return obj - } catch (all) { - println " ### ERROR ### Max memory '${params.max_memory}' is not valid! Using default value: $obj" - return obj - } - } else if (type == 'time') { - try { - if (obj.compareTo(params.max_time as nextflow.util.Duration) == 1) - return params.max_time as nextflow.util.Duration - else - return obj - } catch (all) { - println " ### ERROR ### Max time '${params.max_time}' is not valid! Using default value: $obj" - return obj - } - } else if (type == 'cpus') { - try { - return Math.min( obj, params.max_cpus as int ) - } catch (all) { - println " ### ERROR ### Max cpus '${params.max_cpus}' is not valid! Using default value: $obj" - return obj - } - } -} - diff --git a/src/pipecraft-core/service_scripts/NextITS/nextflow_schema.json b/src/pipecraft-core/service_scripts/NextITS/nextflow_schema.json deleted file mode 100644 index c19cf238..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/nextflow_schema.json +++ /dev/null @@ -1,771 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/vmikk/NextITS/main/nextflow_schema.json", - "title": "NextITS pipeline parameters", - "description": "Pipeline to process fungal ITS amplicons sequenced with PacBio", - "type": "object", - "$defs": { - "step_1_specific_parameters": { - "title": "Step-1-specific parameters", - "type": "object", - "properties": { - "input": { - "type": "string", - "format": "path", - "exists": true, - "mimetype": "text/csv", - "description": "Path to input data: either a single FASTQ or BAM file containing multiplexed sequences, or a directory containing pre-demultiplexed FASTQ files.", - "fa_icon": "fas fa-file-csv" - }, - "barcodes": { - "type": "string", - "fa_icon": "fas fa-barcode", - "description": "Path to the file with barcodes (in FASTA format) used for demultiplexing the input data." - }, - "demultiplexed": { - "type": "boolean", - "fa_icon": "fas fa-align-justify", - "description": "Whether input is multiplexed (`false`, single FASTQ file) or pre-demultiplexed (`true`, multiple FASTQ files)." - }, - "seqplatform": { - "type": "string", - "default": "PacBio", - "fa_icon": "fab fa-google-play", - "description": "Sequencing platform used", - "enum": ["PacBio", "Illumina"] - }, - "qc_maxee": { - "type": "number", - "description": "Maximum expected errors allowed in a sequence. Sequences with higher error rates will be discarded.", - "fa_icon": "fas fa-times" - }, - "qc_maxeerate": { - "type": "number", - "default": 0.01, - "description": "Maximum expected error rate per sequence (between 0 and 1). Sequences with higher error rates will be discarded.", - "fa_icon": "fas fa-times" - }, - "qc_maxhomopolymerlen": { - "type": "integer", - "default": 25, - "description": "Threshold for a homopolymer region length in a sequence.", - "fa_icon": "fas fa-redo" - }, - "qc_maxn": { - "type": "integer", - "default": 4, - "description": "Discard sequences with more than the specified number of ambiguous nucleotides (N's).", - "fa_icon": "fas fa-minus-square" - }, - "lima_barcodetype": { - "type": "string", - "default": "dual_symmetric", - "fa_icon": "fab fa-slack-hash", - "description": "Barcode configuration type used for multiplexing samples: 'single' if only one of the primers was tagged, 'dual_symmetric' if identical barcodes were used, 'dual_asymmetric' if different barcodes were used, 'dual' if both primers were tagged with a mixture of identical and different barcodes", - "enum": ["single", "dual", "dual_symmetric", "dual_asymmetric"] - }, - "lima_minscore": { - "type": "integer", - "default": 93, - "minimum": 0, - "maximum": 100, - "fa_icon": "fas fa-balance-scale-left", - "description": "Minimum barcode score (0-100) required for successful demultiplexing. Higher values mean stricter matching." - }, - "lima_minendscore": { - "type": "integer", - "default": 50, - "fa_icon": "fas fa-balance-scale-right", - "description": "Minimum second barcode score (only for asymmetric and dual barcoding scheme)", - "minimum": 0, - "maximum": 100 - }, - "lima_minrefspan": { - "type": "number", - "default": 0.75, - "fa_icon": "fas fa-random", - "description": "Minimum read span relative to the barcode length", - "minimum": 0, - "maximum": 1 - }, - "lima_minscoringregions": { - "type": "integer", - "default": 2, - "fa_icon": "fas fa-angle-down", - "description": "Number of barcodes scored required for demultiplexing using dual barcodes", - "minimum": 1, - "maximum": 2 - }, - "lima_windowsize": { - "type": "integer", - "default": 70, - "fa_icon": "fas fa-arrows-alt-h", - "description": "Window size for barcode lookup, in base pairs", - "minimum": 1 - }, - "lima_minlen": { - "type": "integer", - "default": 40, - "fa_icon": "fas fa-compress-alt", - "description": "Minimum sequence length after clipping barcodes.", - "minimum": 1 - }, - "lima_remove_unknown": { - "type": "boolean", - "fa_icon": "fas fa-compress-alt", - "description": "Remove unknown barcode combinations from the demultiplexed data (if false, unknown combinations will be named as tag IDs)." - }, - "primer_forward": { - "type": "string", - "fa_icon": "fas fa-arrow-right", - "description": "Sequence of the forward primer.", - "default": "TACACACCGCCCGTCG", - "help_text": "**Forward Primer:** `ITS9MUNngs` with sequence `TACACACCGCCCGTCG` \nRefer to [Tedersoo & Lindahl, 2016 DOI:`10.1111/1758-2229.12438`](https://ami-journals.onlinelibrary.wiley.com/doi/10.1111/1758-2229.12438) for more details on these primers. " - }, - "primer_reverse": { - "type": "string", - "fa_icon": "fas fa-arrow-left", - "description": "Sequence of the reverse primer.", - "default": "CCTSCSCTTANTDATATGC", - "help_text": "**Reverse Primer:** `ITS4ngsUni` with sequence `CCTSCSCTTANTDATATGC` \nRefer to [Tedersoo & Lindahl, 2016 DOI:`10.1111/1758-2229.12438`](https://ami-journals.onlinelibrary.wiley.com/doi/10.1111/1758-2229.12438) for more details on these primers. " - }, - "primer_mismatches": { - "type": "integer", - "default": 2, - "fa_icon": "fas fa-chevron-circle-down", - "description": "Allowed number of mismatches for primers." - }, - "primer_foverlap": { - "type": "integer", - "fa_icon": "fas fa-angle-right", - "description": "Minimum overlap for the forward primer.", - "default": null - }, - "primer_roverlap": { - "type": "integer", - "fa_icon": "fas fa-angle-left", - "description": "Minimum overlap for the reverse primer.", - "default": null - }, - "its_region": { - "type": "string", - "default": "full", - "enum": [ - "full", - "ITS1", - "ITS2", - "none", - "ITS1_5.8S_ITS2", - "SSU", - "LSU" - ], - "description": "Target ITS region to extract: 'full' for complete ITS region, 'ITS1' or 'ITS2' for specific spacers, 'ITS1_5.8S_ITS2' for the partial ITS (in case of HMM profiles are not able to detect the full ITS region), 'SSU' or 'LSU' for 18S or 28S ribosomal genes, or 'none' to skip extraction (in this case, only the primers will be trimmed)", - "fa_icon": "fas fa-arrows-alt-h" - }, - "ITSx_tax": { - "type": "string", - "default": "all", - "fa_icon": "fas fa-user-tag", - "description": "Taxonomic profile for ITS extraction", - "pattern": "^(?:all|(?:alveolata|bryophyta|bacillariophyta|amoebozoa|euglenozoa|fungi|chlorophyta|rhodophyta|phaeophyceae|marchantiophyta|metazoa|oomycota|haptophyceae|raphidophyceae|rhizaria|synurophyceae|tracheophyta|eustigmatophyceae|apusozoa|parabasalia)(?:,\\s*(?:alveolata|bryophyta|bacillariophyta|amoebozoa|euglenozoa|fungi|chlorophyta|rhodophyta|phaeophyceae|marchantiophyta|metazoa|oomycota|haptophyceae|raphidophyceae|rhizaria|synurophyceae|tracheophyta|eustigmatophyceae|apusozoa|parabasalia))*)$" - }, - "ITSx_evalue": { - "type": "number", - "default": 0.1, - "fa_icon": "fas fa-check-circle", - "description": "Min E-value threshold for ITSx" - }, - "ITSx_complement": { - "type": "string", - "default": "F", - "fa_icon": "fas fa-check-circle", - "description": "Check single strand (F, default) or both DNA strands (T) for matches to HMM-profiles", - "enum": [ - "F", - "T" - ] - }, - "ITSx_partial": { - "type": "integer", - "default": 0, - "fa_icon": "fas fa-arrows-alt-h", - "description": "Min length cutoff for partial ITS sequences to keep (0 = disabled, default)" - }, - "ITSx_to_parquet": { - "type": "boolean", - "default": true, - "fa_icon": "fas fa-database", - "description": "Convert ITSx output (FASTA files) to Parquet" - }, - "ITSx_chunk_size": { - "type": "integer", - "default": 10000, - "fa_icon": "fas fa-database", - "description": "Chunk size for distributed ITSx processing (number of dereplicated sequences per sample); set to 0 to disable chunking" - }, - "hp": { - "type": "boolean", - "default": true, - "description": "Enable homopolymer error correction in sequences" - }, - "hp_similarity": { - "type": "number", - "default": 0.999, - "minimum": 0.8, - "maximum": 1, - "description": "Sequence similarity threshold for pre-clustering during homopolymer error correction (0.8-1.0)" - }, - "hp_iddef": { - "type": "number", - "default": 2, - "minimum": 0, - "maximum": 4, - "description": "Identity definition for homopolymer compression" - }, - "chimera_methods": { - "type": "string", - "default": "ref,denovo", - "description": "Which chimera removal methods to use", - "enum": ["ref", "denovo", "ref,denovo", "denovo,ref", "none", "null"] - }, - "chimera_db": { - "type": "string", - "fa_icon": "fas fa-database", - "default": "Eukaryome_1.9.3_241222_FullITS_100-800.udb", - "description": "Database for refrence-based chimera removal" - }, - "chimera_rescueoccurrence": { - "type": "integer", - "default": 2, - "description": "Occurrence threshold for rescuing reference-based chimeras" - }, - "chimeranov_abskew": { - "type": "number", - "default": 2, - "description": "abskew parameter for de novo chimera removal" - }, - "chimeranov_dn": { - "type": "number", - "default": 1.4, - "description": "dn parameter for de novo chimera removal" - }, - "chimeranov_mindiffs": { - "type": "number", - "default": 3, - "description": "mindiffs parameter for de novo chimera removal" - }, - "chimeranov_mindiv": { - "type": "number", - "default": 0.8, - "description": "mindiv parameter for de novo chimera removal" - }, - "chimeranov_minh": { - "type": "number", - "default": 0.28, - "description": "minh parameter for de novo chimera removal" - }, - "chimeranov_xn": { - "type": "number", - "default": 8, - "description": "xn parameter for de novo chimera removal" - }, - "tj": { - "type": "boolean", - "default": true, - "description": "Run tag-jump removal" - }, - "tj_f": { - "type": "number", - "default": 0.01, - "minimum": 0, - "maximum": 1, - "description": "UNCROSS parameter f for tag-jump removal" - }, - "tj_p": { - "type": "number", - "default": 1, - "description": "Parameter p for tag-jump removal" - }, - "tj_id": { - "type": "number", - "default": 1, - "minimum": 0, - "maximum": 1, - "description": "Sequence similarity threshold for tag-jump removal (1 = dereplication, < 1 = pre-clustering)" - }, - "tj_iddef": { - "type": "integer", - "default": 2, - "minimum": 0, - "maximum": 4, - "description": "Sequence identity definition for pre-clustering prior tag-jump removal" - } - }, - "fa_icon": "fas fa-bullseye" - }, - "step_2_specific_parameters": { - "title": "Step-2-specific parameters", - "type": "object", - "properties": { - "data_path": { - "type": "string", - "format": "directory-path", - "fa_icon": "fas fa-folder-open", - "description": "Path to the Step-1 results", - "default": "Step1_Results" - }, - "merge_replicates": { - "type": "boolean", - "description": "Pool sample replicates (e.g., re-sequenced samples) in the final OTU table" - }, - "ampliconlen_min": { - "type": "integer", - "description": "Minimum amplicon length for filtering" - }, - "ampliconlen_max": { - "type": "integer", - "description": "Maximum amplicon length for filtering" - }, - "chunking_n": { - "type": "integer", - "description": "Number of chunks to split the dataset into prior clustering" - }, - "chunking_id": { - "type": "number", - "default": 0.6, - "description": "Minimum sequence identity for clustering" - }, - "preclustering": { - "type": "string", - "default": "none", - "enum": ["none", "unoise", "dada2", "swarm_d1", "homopolymer"], - "description": "Sequence denoising or pre-clustering method" - }, - "unoise_alpha": { - "type": "number", - "default": 6, - "description": "UNOISE alpha parameter" - }, - "unoise_minsize": { - "type": "integer", - "default": 1, - "description": "UNOISE minimum size parameter" - }, - "dada2_pooling": { - "type": "string", - "default": "byrun", - "enum": ["byrun", "global"], - "description": "DADA2 pooling strategy" - }, - "dada2_nbases": { - "type": "number", - "default": 1000000, - "description": "DADA2 number of bases" - }, - "dada2_bandsize": { - "type": "integer", - "default": 16, - "description": "DADA2 band size" - }, - "dada2_detectsingletons": { - "type": "boolean", - "default": true, - "description": "DADA2 detect singletons" - }, - "dada2_omegaA": { - "type": "number", - "default": 1e-20, - "description": "DADA2 omega A parameter" - }, - "dada2_omegaC": { - "type": "number", - "default": 1e-40, - "description": "DADA2 omega C parameter" - }, - "dada2_omegaP": { - "type": "number", - "default": 0.0001, - "description": "DADA2 omega P parameter" - }, - "dada2_maxconsist": { - "type": "integer", - "default": 10, - "description": "DADA2 maximum consistency" - }, - "dada2_match": { - "type": "integer", - "default": 4, - "description": "DADA2 match score" - }, - "dada2_mismatch": { - "type": "integer", - "default": -5, - "description": "DADA2 mismatch penalty" - }, - "dada2_gappenalty": { - "type": "integer", - "default": -8, - "description": "DADA2 gap penalty" - }, - "clustering": { - "type": "string", - "default": "vsearch", - "enum": ["none", "vsearch", "swarm", "shmatching"], - "description": "Sequence clustering method" - }, - "otu_id": { - "type": "number", - "default": 0.98, - "description": "Sequence similarity threshold for OTU clustering", - "minimum": 0, - "maximum": 1 - }, - "otu_iddef": { - "type": "integer", - "default": 2, - "description": "Sequence similarity definition type for OTU clustering", - "minimum": 1, - "maximum": 4 - }, - "otu_qmask": { - "type": "string", - "default": "dust", - "description": "Quality masking for OTU clustering" - }, - "swarm_d": { - "type": "integer", - "default": 1, - "description": "SWARM distance parameter" - }, - "swarm_fastidious": { - "type": "boolean", - "default": true, - "description": "Enable SWARM fastidious mode" - }, - "swarm_d1boundary": { - "type": "integer", - "default": 3, - "description": "SWARM boundary parameter for fastidious mode" - }, - "alignment_penalties": { - "type": "string", - "default": "default", - "enum": ["default", "UNITE"], - "description": "Alignment penalty configuration" - }, - "vsearch_gapopen": { - "type": "string", - "default": "20I/2E", - "description": "VSEARCH gap opening penalties" - }, - "vsearch_gapext": { - "type": "string", - "default": "2I/1E", - "description": "VSEARCH gap extension penalties" - }, - "max_MEEP": { - "type": "number", - "default": 0.5, - "description": "Maximum expected error as a percentage of read length (MEEP) threshold for singleton removal" - }, - "max_ChimeraScore": { - "type": "number", - "default": 0.6, - "description": "Maximum de novo chimera score" - }, - "recover_lowqsingletons": { - "type": "boolean", - "default": true, - "description": "Allow recovery of singletons" - }, - "recover_denovochimeras": { - "type": "boolean", - "default": true, - "description": "Allow recovery of de novo chimeras" - }, - "lulu": { - "type": "boolean", - "default": true, - "description": "Post-clustering curation using LULU algorithm" - }, - "lulu_match": { - "type": "integer", - "default": 95, - "description": "Minimum sequence similarity threshold for LULU" - }, - "lulu_ratio": { - "type": "integer", - "default": 1, - "description": "Minimum sequence abundance ratio for LULU" - }, - "lulu_ratiotype": { - "type": "string", - "default": "min", - "description": "Abundance ratio type for LULU", - "enum": ["min", "avg"] - }, - "lulu_relcooc": { - "type": "number", - "default": 0.95, - "description": "Relative co-occurrence threshold for LULU", - "minimum": 0, - "maximum": 1 - }, - "lulu_maxhits": { - "type": "integer", - "default": 0, - "description": "Maximum number of hits for LULU (0 = unlimited)" - }, - "sh": { - "type": "boolean", - "default": true, - "description": "Species-hypothesis (SH) matching [not implemented yet]" - }, - "sh_thresholds": { - "type": "string" - }, - "sh_coveragevariation": { - "type": "number", - "default": 0.96 - } - } - }, - "common_parameters": { - "title": "Common parameters for both steps", - "type": "object", - "properties": { - "outdir": { - "type": "string", - "format": "directory-path", - "description": "Path to the directory where the analysis results will be saved.", - "fa_icon": "fas fa-folder-open", - "default": "Step2_Results" - }, - "step": { - "type": "string", - "enum": ["Step1", "Step2", "seqstats"], - "description": "The step of the pipeline to run.", - "fa_icon": "fas fa-step-forward", - "default": "Step1" - }, - "storagemode": { - "type": "string", - "default": "rellink", - "fa_icon": "fas fa-boxes", - "enum": ["symlink", "copy", "move", "rellink", "link"], - "description": "How to store output files: 'symlink' for symbolic links, 'copy' to copy files, 'move' to move files, 'rellink' for relative links, 'link' for hard links" - }, - "gzip_compression": { - "type": "number", - "default": 7, - "minimum": 1, - "maximum": 9, - "fa_icon": "fas fa-file-archive", - "description": "Controls GZIP compression level in output files." - }, - "max_cpus": { - "type": "integer", - "default": 40, - "fa_icon": "fas fa-microchip", - "description": "Maximum number of CPUs that can be requested (for any single job).", - "help_text": "Use to set an upper-limit for the CPU requirement for each process. Should be an integer e.g. `--max_cpus 1`", - "minimum": 1, - "maximum": 200 - }, - "max_memory": { - "type": "string", - "default": "132.GB", - "fa_icon": "fas fa-memory", - "description": "Maximum amount of memory that can be requested (for any single job).", - "pattern": "^\\d+(\\.\\d+)?\\.?\\s*(K|M|G|T)?B$", - "help_text": "Use to set an upper-limit for the memory requirement for each process. Should be a string in the format integer-unit e.g. `--max_memory '8.GB'`" - }, - "max_time": { - "type": "string", - "default": "240.h", - "fa_icon": "fas fa-clock", - "description": "Maximum amount of time that can be requested (for any single job).", - "pattern": "^(\\d+\\.?\\s*(s|m|h|day)\\s*)+$", - "help_text": "Use to set an upper-limit for the time requirement for each process. Should be a string in the format integer-unit e.g. `--max_time '2.h'`" - }, - "tracedir": { - "type": "string", - "fa_icon": "fas fa-folder-minus", - "description": "Directory to store pipeline execution logs" - }, - "version": { - "type": "boolean", - "description": "Display version and exit", - "fa_icon": "fas fa-question-circle", - "hidden": true - }, - "help": { - "type": "boolean", - "description": "Display help text", - "fa_icon": "fas fa-question-circle", - "hidden": true - }, - "helpMsg": { - "type": "boolean", - "fa_icon": "far fa-question-circle", - "description": "Custom help message" - }, - "monochrome_logs": { - "type": "boolean", - "description": "Do not use coloured log outputs", - "fa_icon": "fas fa-palette", - "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 - }, - "show_hidden_params": { - "type": "boolean", - "description": "Show all params when using `--help`", - "help_text": "By default, parameters set as _hidden_ in the schema are not shown on the command line when a user runs with `--help`. Specifying this option will tell the pipeline to show all parameters.", - "fa_icon": "fas fa-eye-slash" - }, - "enable_conda": { - "type": "boolean", - "fa_icon": "fas fa-adjust", - "hidden": true - }, - "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})$", - "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 - }, - "schema_ignore_params": { - "type": "string" - } - }, - "required": ["step"], - "fa_icon": "fab fa-creative-commons-share" - }, - "illumina_specific_parameters": { - "title": "Illumina-specific parameters", - "type": "object", - "properties": { - "input_R1": { - "type": "string", - "description": "File with R1 reads for Illumina" - }, - "input_R2": { - "type": "string", - "description": "File with R2 reads for Illumina" - }, - "qc_avgphred": { - "type": "boolean", - "description": "Average Phred score (for Illumina PE reads only)" - }, - "qc_twocolor": { - "type": "boolean", - "description": "Reduced resolution Phred-scores (two-color Illumina chemistry)" - }, - "pe_minoverlap": { - "type": "integer", - "default": 20, - "description": "Paired-end reads minimum overlap (Illumina only)" - }, - "pe_diffperclimit": { - "type": "integer", - "default": 20, - "description": "Paired-end reads max percentage difference for the overlap (Illumina only)" - }, - "pe_difflimit": { - "type": "integer", - "default": 5, - "description": "Paired-end reads max difference for the overlap (Illumina only)" - }, - "pe_nlimit": { - "type": "integer", - "default": 10 - }, - "pe_minlen": { - "type": "integer", - "default": 30 - }, - "barcode_errors": { - "type": "integer", - "default": 1, - "description": "Number of allowed erros in demultiplexing (Illumina only)" - }, - "illumina_keep_notmerged": { - "type": "boolean", - "default": true - }, - "illumina_joinpadgap": { - "type": "string", - "default": "NNNNNNNNNN" - }, - "illumina_joinpadqual": { - "type": "string", - "default": "IIIIIIIIII" - }, - "barcode_window": { - "type": "integer", - "default": 30, - "description": "(Illumina only)" - }, - "barcode_overlap": { - "type": "integer", - "default": 11, - "description": "(Illumina only)" - }, - "trim_minlen": { - "type": "integer", - "default": 10 - }, - "qc_phredmin": { - "type": "integer", - "description": "Min Phred score threshold of unqualified bases (for two-color Illumina only)" - }, - "qc_phredperc": { - "type": "integer", - "description": "Max percentage of unqualified bases per read (for two-color Illumina only)" - }, - "qc_polyglen": { - "type": "integer", - "description": "Max poly-G length (for two-color Illumina only)" - } - }, - "description": "EXPERIMENTAL", - "fa_icon": "fas fa-flask" - } - }, - "allOf": [ - { - "$ref": "#/$defs/step_1_specific_parameters" - }, - { - "$ref": "#/$defs/step_2_specific_parameters" - }, - { - "$ref": "#/$defs/common_parameters" - }, - { - "$ref": "#/$defs/illumina_specific_parameters" - } - ] -} diff --git a/src/pipecraft-core/service_scripts/NextITS/subworkflows/chimera_removal_subworkflow.nf b/src/pipecraft-core/service_scripts/NextITS/subworkflows/chimera_removal_subworkflow.nf deleted file mode 100644 index 0a13786d..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/subworkflows/chimera_removal_subworkflow.nf +++ /dev/null @@ -1,296 +0,0 @@ -/* -============================================================================ - NextITS: Pipeline to process eukaryotic ITS amplicons -============================================================================ - License: Apache-2.0 - Github : https://github.com/vmikk/NextITS - Website: https://Next-ITS.github.io/ ----------------------------------------------------------------------------- -*/ - -// Subworkflow for chimera removal (reference-based and de novo) - -// Path to the output results -out_5_chim = params.outdir + "/05_Chimera" - -// Reference-based chimera removal -process chimera_ref { - - label "main_container" - - publishDir "${out_5_chim}", mode: "${params.storagemode}" - // cpus 1 - - // Add sample ID to the log file - tag "${input.getSimpleName().replaceAll(/_Homopolymer_compressed/, '')}" - - input: - path input - path DB - - output: - path "${input.getSimpleName().replaceAll(/_Homopolymer_compressed/, '')}_NoChimera.fa.gz", emit: nonchimeric, optional: true - path "${input.getSimpleName().replaceAll(/_Homopolymer_compressed/, '')}_Chimera.fa.gz", emit: chimeric, optional: true - - script: - sampID="${input.getSimpleName().replaceAll(/_Homopolymer_compressed/, '')}" - - """ - - ## Sample name will be added to the header of chimeric sequences - # sampID="\$(basename ${input} _Homopolymer_compressed.fa.gz)" - - ## Reference database based chimera filtering - echo -e "Reference-based chimera removal" - vsearch \ - --uchime_ref ${input} \ - --db ${DB} \ - --selfid \ - --fasta_width 0 \ - --threads ${task.cpus} \ - --sizein --sizeout \ - --chimeras chimeras.fasta \ - --nonchimeras nonchimeras.fasta \ - --borderline borderline.fasta - - # --selfid = ignore reference sequences that are 100% identical to the query - echo -e "..Done" - - - ## Add borderline sequences to non-chimeric sequences - if [ -e borderline.fasta ] - then - echo -e "\\nBorderline sequences were added to non-chimeric sequences" - cat borderline.fasta nonchimeras.fasta > nc_bo.fasta - mv nc_bo.fasta nonchimeras.fasta - rm borderline.fasta - fi - - ## Chimeric sequences - if [ -e chimeras.fasta ] - then - ## Add sample ID to the header and compress the file - sed 's/>.*/&;sample='"${sampID}"';/' chimeras.fasta \ - | gzip -${params.gzip_compression} \ - > "${sampID}_Chimera.fa.gz" - rm chimeras.fasta - else - echo -e "\\nNo chimeras detected" - fi - - ## Non-chimeric sequences - if [ -e nonchimeras.fasta ] - then - gzip -c nonchimeras.fasta > "${sampID}_NoChimera.fa.gz" - rm nonchimeras.fasta - else - echo "No non-chimeric sequences left" - fi - - """ -} - - -// Recovery of ref-based chimeric sequences with high occurrence -process chimera_rescue { - - label "main_container" - - publishDir "${out_5_chim}", mode: "${params.storagemode}" - // cpus 1 - - input: - path input - - output: - path "*_RescuedChimera.fa.gz", emit: rescuedchimeric, optional: true - - script: - """ - - ## Aggregate chimeric sequences from different samples - echo -e "\\nAggregating chimeric sequences" - find . -name "*_Chimera.fa.gz" \ - | parallel -j1 "zcat {}" \ - | seqkit fx2tab \ - | sed -r 's:\t+:\t:g' | sed 's/\t\$//g' \ - | gzip -${params.gzip_compression} > All_chimeras.txt.gz - echo -e "..Done" - - ### Inspect chimerae occurrence - ## Rescue sequences that were annotated as chimeric, - ## but have high occurrence within sequenceing run (e.g., occurrence > 2) - echo -e "\\nInspecting occurrence of chimeric sequences" - - chimera_rescue.R \ - "All_chimeras.txt.gz" \ - ${params.chimera_rescueoccurrence} \ - "Rescued_Chimeric_sequences.fa.gz" - - echo -e "..Done" - - ## Split rescured sequences by sample - if [ -e Rescued_Chimeric_sequences.fa.gz ] - then - echo -e "\\n..Splitting rescued sequences by sample" - seqkit split -i \ - --id-regexp ";sample=(.*);" \ - --threads ${task.cpus} \ - -w 0 \ - -O Rescued_by_sample \ - Rescued_Chimeric_sequences.fa.gz - - rename \ - --filename 's/^Rescued_Chimeric_sequences.id_//g ; s/^Rescued_Chimeric_sequences.part_//g ; s/.fa.gz/_RescuedChimera.fa.gz/' \ - \$(find Rescued_by_sample -name "*.fa.gz") - - mv Rescued_by_sample/*_RescuedChimera.fa.gz . - - echo -e "..Done" - fi - - ## Remove temporary files - rm All_chimeras.txt.gz - if [ -f Rescued_Chimeric_sequences.fa.gz ]; then rm Rescued_Chimeric_sequences.fa.gz; fi - - """ -} - - - -// De novo chimera identification -// NB! in uchime_denovo, sequences are compared on their plus strand only! -process chimera_denovo { - - label "main_container" - - publishDir "${out_5_chim}", mode: "${params.storagemode}" - // cpus 1 - - // Add sample ID to the log file - tag "${input.getSimpleName().replaceAll(/_Homopolymer_compressed/, '')}" - - input: - path input - - output: - path "${input.getSimpleName().replaceAll(/_Homopolymer_compressed/, '')}_DeNovoChim.txt", emit: denovochim, optional: true - tuple val("${task.process}"), val('vsearch'), eval('vsearch --version 2>&1 | head -n 1 | sed "s/vsearch //g" | sed "s/,.*//g" | sed "s/^v//" | sed "s/_.*//"'), topic: versions - - script: - sampID="${input.getSimpleName().replaceAll(/_Homopolymer_compressed/, '')}" - - """ - - ## Input order matters for chimera detection, - ## so sequences will be automatically sorted by decreasing abundance first - - echo -e "De novo chimera identification" - - vsearch \ - --uchime_denovo ${input} \ - --abskew ${params.chimeranov_abskew} \ - --dn ${params.chimeranov_dn} \ - --mindiffs ${params.chimeranov_mindiffs} \ - --mindiv ${params.chimeranov_mindiv} \ - --minh ${params.chimeranov_minh} \ - --xn ${params.chimeranov_xn} \ - --threads 1 \ - --qmask dust \ - --sizein --xsize \ - --fasta_width 0 \ - --fasta_score \ - --chimeras - \ - | seqkit seq --name \ - | sed 's/;+/;/g ; s/;/\t/g ; s/uchime_denovo=//' \ - | sed 's/\$/\t${sampID}/' \ - > ${sampID}_DeNovoChim.txt - - # --uchimeout uchimeout.txt - - ## Remove file, if empty - find . -maxdepth 1 -name ${sampID}_DeNovoChim.txt -size 0 -print -delete - - echo -e "..Done" - - """ -} - -// Aggregate de novo chimeras into a single file -process chimera_denovo_agg { - - label "main_container" - // cpus 1 - - input: - path input - - output: - path "DeNovo_Chimera.txt", emit: alldenovochim, optional: true - - script: - """ - echo -e "Aggregating de novo chimeric sequences" - - find . -name "*_DeNovoChim.txt" \ - | parallel -j1 "cat {}" \ - > DeNovo_Chimera.txt - - echo -e "..Done" - - """ -} - - - -// Chimera indetification, removal, and recovery -workflow CHIMERA_REMOVAL { - - take: - seqs - db - - main: - def methodsRaw = (params.chimera_methods ?: '').toString().toLowerCase() - def chim_rm = methodsRaw && methodsRaw != 'none' - def doRef = chim_rm && methodsRaw.split(',').contains('ref') - def doDenovo = chim_rm && methodsRaw.split(',').contains('denovo') - - if( doRef ) { - // Reference-based chimera identification and removal - chimera_ref(seqs, db) - nonchim = chimera_ref.out.nonchimeric - chim = chimera_ref.out.chimeric - - // Rescue chimeras per current logic - chimera_rescue(chim.collect()) - rescued = chimera_rescue.out.rescuedchimeric - } else { - nonchim = seqs - chim = Channel.empty() - rescued = Channel.empty() - } - - if( doDenovo ) { - - // De novo chimera identification - chimera_denovo(seqs) - - // Aggregate de novo chimeras into a single file - chimera_denovo_agg(chimera_denovo.out.denovochim.collect()) - dnvAgg = chimera_denovo_agg.out.alldenovochim - } else { - dnvAgg = Channel.empty() - } - - // Final sequences for downstream pooling - filtered = nonchim.concat(rescued).collect() - - emit: - filtered = filtered - chimeric = chim - rescued = rescued - denovo_agg = dnvAgg - -} // end of chimera_removal subworkflow - diff --git a/src/pipecraft-core/service_scripts/NextITS/subworkflows/clustering_subworkflow.nf b/src/pipecraft-core/service_scripts/NextITS/subworkflows/clustering_subworkflow.nf deleted file mode 100644 index d5e7f1bb..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/subworkflows/clustering_subworkflow.nf +++ /dev/null @@ -1,540 +0,0 @@ -/* -============================================================================ - NextITS: Pipeline to process eukaryotic ITS amplicons -============================================================================ - License: Apache-2.0 - Github : https://github.com/vmikk/NextITS - Website: https://Next-ITS.github.io/ ----------------------------------------------------------------------------- -*/ - -// Subworkflow for clustering sequences (with optional pre-clustering or denoising) - - -// Homopolymer correction (global, for pooled and dereplicated data) -process homopolymer { - - label "main_container" - - publishDir( - "${params.outdir}/02.Homopolymer", - mode: "${params.storagemode}", - enabled: params.chunking_n == null || params.chunking_n < 2 - ) - - // cpus 1 - - input: - path input - - output: - path "HomopolymerCompressed.fa.gz", emit: hp - path "HomopolymerCompressed.uc.gz", emit: hp_uc - - script: - """ - ## Run homopolyer correction globally - - echo -e "Running homopolymer correction" - - echo -e "\\nCompressing repeats" - zcat ${input} \ - | homopolymer_compression.sh \ - | gzip -2 \ - > homo_compressed.fa.gz - - echo -e "\\nAdditional dereplication" - vsearch \ - --derep_fulllength homo_compressed.fa.gz \ - --output - \ - --strand both \ - --fasta_width 0 \ - --threads 1 \ - --sizein --sizeout \ - --uc HomopolymerCompressed.uc \ - > homo_compressed_dereplicated.fa - - ## Substitute homopolymer-comressed sequences with uncompressed ones - ## (update size annotaions) - echo -e "\\nExtracting representative sequences" - - seqkit fx2tab ${input} > inp_tab.txt - seqkit fx2tab homo_compressed_dereplicated.fa > clust_tab.txt - - if [ -s inp_tab.txt ]; then - substitute_compressed_seqs.R \ - inp_tab.txt clust_tab.txt \ - HomopolymerCompressed_tmp.fa - - echo -e "..Done" - else - echo -e "..Input data looks empty, nothing to proceed with" - fi - - - ## Sort by number of reads - vsearch \ - --sortbysize HomopolymerCompressed_tmp.fa \ - --sizein --sizeout \ - --threads ${task.cpus} \ - --fasta_width 0 \ - --output - \ - | gzip -${params.gzip_compression} \ - > HomopolymerCompressed.fa.gz - - - #### combine_derep_and_hpcorrection.R - - echo -e "\\nHomopolymer correction finished\\n" - - ## Compress results - echo -e "\\nCompressing results" - gzip -${params.gzip_compression} HomopolymerCompressed.uc - - ## Remove temporary files - echo -e "\\nRemoving temporary files" - rm homo_compressed.fa.gz - rm homo_compressed_dereplicated.fa - rm HomopolymerCompressed_tmp.fa - rm inp_tab.txt - rm clust_tab.txt - """ -} - - -// Denoize sequences with UNOISE -process unoise { - - label "main_container" - - publishDir( - "${params.outdir}/02.UNOISE", - mode: "${params.storagemode}", - enabled: params.chunking_n == null || params.chunking_n < 2 - ) - - // cpus 8 - - input: - path input - - output: - path "UNOISE.fa.gz", emit: unoise - path "UNOISE.uc.gz", emit: unoise_uc - tuple val("${task.process}"), val('vsearch'), eval('vsearch --version 2>&1 | head -n 1 | sed "s/vsearch //g" | sed "s/,.*//g" | sed "s/^v//" | sed "s/_.*//"'), topic: versions - - script: - """ - echo -e "Denoizing sequences with UNOISE\\n" - - vsearch \ - --cluster_unoise ${input} \ - --unoise_alpha ${params.unoise_alpha} \ - --minsize ${params.unoise_minsize} \ - --iddef ${params.otu_iddef} \ - --qmask ${params.otu_qmask} \ - --gapopen ${params.vsearch_gapopen} \ - --gapext ${params.vsearch_gapext } \ - --threads ${task.cpus} \ - --fasta_width 0 \ - --sizein --sizeout \ - --centroids UNOISE.fa \ - --uc UNOISE.uc - - echo -e "..UNOISE done\\n" - - ## Compress results - echo -e "\\nCompressing UNOISE results" - parallel -j 1 \ - "pigz -p ${task.cpus} -${params.gzip_compression} {}" \ - ::: "UNOISE.fa" "UNOISE.uc" - - """ -} - - - -// Denoize sequences with DADA2 -process dada2 { - - label "main_container" - - publishDir( - "${params.outdir}/02.DADA2", - mode: "${params.storagemode}", - enabled: params.chunking_n == null || params.chunking_n < 2 - ) - - // cpus 8 - - input: - path input - - output: - path "DADA2_denoised.fa.gz", emit: dada - path "DADA2_denoised.uc.gz", emit: dada_uc - path "DADA2_UC.qs", emit: dada_ucr - path "DADA2_denoising_summary.txt", emit: dada_summary - // path "DADA2_ErrorRates_noqualErrfun.RData" - // path "DADA2_InferedSeqs_noqualErrfun.RData" - tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //" | cut -d" " -f1'), topic: versions - tuple val("${task.process}"), val('dada2'), eval('Rscript -e "cat(as.character(packageVersion(\'dada2\')))"'), topic: versions - tuple val("${task.process}"), val('data.table'), eval('Rscript -e "cat(as.character(packageVersion(\'data.table\')))"'), topic: versions - - script: - """ - echo -e "Denoizing sequences with DADA2\\n" - - ## DADA2 works with ACGT alphabet only - ## 1. So check if there are any sequences with ambiguities - ## 2. If any, remove them - ## 3. Sort by sequence abundance - ## 4. Convert FASTA to pseudo-FASTQ - ## 5. Denoise - - ## Remove sequences with ambiguities - echo -e "..Preparing sequences\\n" - zcat ${input} \ - | awk '{if (/^>/) {a = \$0} else {if (/^[ACGT]*\$/) {printf "%s\\n%s\\n", a, \$0}}}' \ - | vsearch --sortbysize - --output - --fasta_width 0 \ - | awk 'BEGIN {RS = ">" ; FS = "\\n"} NR > 1 {print "@"\$1"\\n"\$2"\\n+"\$1"\\n"gensub(/./, "I", "g", \$2)}' \ - | gzip -${params.gzip_compression} > no_ambigs.fq.gz - - echo -e "\\n\\n..Running DADA2\\n" - dada2_no_quals.R \ - --input no_ambigs.fq.gz \ - --nbases ${params.dada2_nbases} \ - --bandsize ${params.dada2_bandsize} \ - --detectsingletons ${params.dada2_detectsingletons} \ - --omegaA ${params.dada2_omegaA} \ - --omegaC ${params.dada2_omegaC} \ - --omegaP ${params.dada2_omegaP} \ - --maxconsist ${params.dada2_maxconsist} \ - --match ${params.dada2_match} \ - --mismatch ${params.dada2_mismatch} \ - --gappenalty ${params.dada2_gappenalty} \ - --threads ${task.cpus} - - echo -e "..Denoizing with DADA2 finished\\n" - """ -} - - - - -// Preclustering with SWARM and d1 -process precluster_swarm { - - label "main_container" - - publishDir( - "${params.outdir}/02.Preclustered_SWARM_d1", - mode: "${params.storagemode}", - enabled: params.chunking_n == null || params.chunking_n < 2 - ) - - // cpus 8 - - input: - path input - - output: - path "SWARM_representatives.fa.gz", emit: clust - path "SWARM.uc.gz", emit: clust_uc - path "SWARM.swarms.gz", emit: swarms - path "SWARM.struct.gz", emit: struct - path "SWARM.stats.gz", emit: stats - tuple val("${task.process}"), val('swarm'), eval('swarm --version 2>&1 | head -n 1 | sed "s/Swarm //"'), topic: versions - - script: - """ - echo -e "Pre-clustering sequences with SWARM d=1\\n" - echo -e "Note: sequences with ambiguous nucleotides will be excluded!\\n" - - ## Remove sequences with ambiguities - zcat ${input} \ - | awk '{if (/^>/) {a = \$0} else {if (/^[ACGT]*\$/) {printf "%s\\n%s\\n", a, \$0}}}' \ - | swarm \ - --differences 1 \ - --boundary ${params.swarm_d1boundary} \ - --fastidious \ - --threads ${task.cpus} \ - --usearch-abundance \ - --statistics-file SWARM.stats \ - --internal-structure SWARM.struct \ - --uclust-file SWARM.uc \ - --seeds SWARM_representatives.fa \ - > SWARM.swarms - - echo -e "\\n..Swarm pre-clustering finished\\n" - - ## Compress results - echo -e "\\n..Compressing results\\n" - parallel -j 1 \ - "pigz -p ${task.cpus} -${params.gzip_compression} {}" \ - ::: "SWARM_representatives.fa" "SWARM.uc" "SWARM.swarms" "SWARM.struct" "SWARM.stats" - - echo -e "..Done\\n" - """ -} - - - -// Cluster sequences with VSEARCH (fixed similarity threshold) -process cluster_vsearch { - - label "main_container" - - publishDir( - "${params.outdir}/03.Clustered_VSEARCH", - mode: "${params.storagemode}", - enabled: params.chunking_n == null || params.chunking_n < 2 - ) - - // cpus 8 - - input: - path input - - output: - path "Clustered.fa.gz", emit: clust - path "Clustered.uc.gz", emit: clust_uc - tuple val("${task.process}"), val('vsearch'), eval('vsearch --version 2>&1 | head -n 1 | sed "s/vsearch //g" | sed "s/,.*//g" | sed "s/^v//" | sed "s/_.*//"'), topic: versions - - script: - """ - echo -e "Clustering sequences with VSEARCH\\n" - - vsearch \ - --cluster_size ${input} \ - --id ${params.otu_id} \ - --iddef ${params.otu_iddef} \ - --qmask ${params.otu_qmask} \ - --gapopen ${params.vsearch_gapopen} \ - --gapext ${params.vsearch_gapext } \ - --threads ${task.cpus} \ - --sizein --sizeout \ - --strand both \ - --fasta_width 0 \ - --uc Clustered.uc \ - --centroids - \ - | gzip -${params.gzip_compression} > Clustered.fa.gz - - echo -e "..Done" - - ## Compress UC file - echo -e "\\nCompressing UC file" - pigz -p ${task.cpus} -${params.gzip_compression} Clustered.uc - - """ -} - - - -// Cluster sequences with SWARM (dynamic similarity threshold) -process cluster_swarm { - - label "main_container" - - publishDir( - "${params.outdir}/03.Clustered_SWARM", - mode: "${params.storagemode}", - enabled: params.chunking_n == null || params.chunking_n < 2 - ) - - // cpus 8 - - input: - path input - - output: - path "SWARM_representatives.fa.gz", emit: clust - path "SWARM.uc.gz", emit: clust_uc - path "SWARM.swarms.gz", emit: swarms - path "SWARM.struct.gz", emit: struct - path "SWARM.stats.gz", emit: stats - tuple val("${task.process}"), val('swarm'), eval('swarm --version 2>&1 | head -n 1 | sed "s/Swarm //"'), topic: versions - - exec: - fastidious = (params.swarm_fastidious.toBoolean() == true & params.swarm_d.toInteger() == 1) ? "--fastidious --boundary ${params.swarm_d1boundary}" : "" - // println("swarm_fastidious: ${params.swarm_fastidious}, swarm_d: ${params.swarm_d}") - // println("fastid option: ${fastidious}") - - script: - """ - echo -e "Clustering sequences with SWARM\\n" - echo -e "Note: sequences with ambiguous nucleotides will be excluded!\\n" - - ## Swarm works with ACGTU alphabet only - ## 1. So check if there are any sequences with ambiguities - ## 2. If any, remove them - ## 3. Cluster - - ## Count number of sequences with ambiguities (will go through the entire file) - # AMBIGS=\$(seqkit grep --count --by-seq --use-regexp --ignore-case --pattern "[RYSWKMBDHVN]" ${input}) - - ## Remove sequences with ambiguities - zcat ${input} \ - | awk '{if (/^>/) {a = \$0} else {if (/^[ACGT]*\$/) {printf "%s\\n%s\\n", a, \$0}}}' \ - | swarm \ - --differences ${params.swarm_d} \ - ${fastidious} \ - --threads ${task.cpus} \ - --usearch-abundance \ - --statistics-file SWARM.stats \ - --internal-structure SWARM.struct \ - --uclust-file SWARM.uc \ - --seeds SWARM_representatives.fa \ - > SWARM.swarms - - # --output-file SWARM.swarms # to avoid buffering, it's better to stream data into a file (with >) - # -r, --mothur # output using mothur-like format - - echo -e "\\n..Swarm clustering finished\\n" - - ## Compress results - echo -e "..Compressing results\\n" - parallel -j 1 \ - "pigz -p ${task.cpus} -${params.gzip_compression} {}" \ - ::: "SWARM_representatives.fa" "SWARM.uc" "SWARM.swarms" "SWARM.struct" "SWARM.stats" - - echo -e "..Done\\n" - """ -} - - -// Pre-clustering / denoising / clustering subworkflow -workflow CLUSTERING { - - take: - derep_ch - - main: - - /* - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Pre-clustering / denoising - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - */ - - // No pre-clustering or denoizing - if ( params.preclustering == "none" || params.preclustering == null ) { - denoise_ch = derep_ch - preclustuc_ch = file('NoPrecluster') - preclustaf_ch = file('NoPreclusterFASTA') - - // Denoise with UNOISE - } else if ( params.preclustering == "unoise" ) { - unoise(derep_ch) - denoise_ch = unoise.out.unoise - preclustuc_ch = unoise.out.unoise_uc - preclustaf_ch = denoise_ch - - // Denoise with DADA2 - } else if ( params.preclustering == "dada2" ) { - - // Denoise all dereplicated sequences - if(params.dada2_pooling == "global"){ - dada2(derep_ch) - denoise_ch = dada2.out.dada - preclustuc_ch = dada2.out.dada_uc - preclustaf_ch = denoise_ch - } - - // // Dereplicate and denoise by sequencing run - // if(params.dada2_pooling == "byrun"){ - // - // dereplication_byrun(ch_seqs) - // dada2(dereplication_byrun.out.dereps.flatten()) - // - // dada2pool( - // dereplication.out.derep_uc, - // dada2.out.dada_ucr.collect() - // ) - // - // /* - // denoise_ch = dada2pool.out.dada - // preclustuc_ch = dada2pool.out.dada_uc - // preclustaf_ch = file('NoPreclusterFASTA') - // - // */ - // } - - - // Precluster with SWARM - } else if ( params.preclustering == "swarm_d1" ){ - precluster_swarm(derep_ch) - denoise_ch = precluster_swarm.out.clust - preclustuc_ch = precluster_swarm.out.clust_uc - preclustaf_ch = denoise_ch - - // Global homopolymer correction - } else if ( params.preclustering == "homopolymer" ){ - homopolymer(derep_ch) - denoise_ch = homopolymer.out.hp - preclustuc_ch = homopolymer.out.hp_uc - preclustaf_ch = denoise_ch - } - - - /* - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Clustering - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - */ - - - // Greedy clustering with VSEARCH - if ( params.clustering == "vsearch" ) { - cluster_vsearch(denoise_ch) - cluster_ch = cluster_vsearch.out.clust - clustuc_ch = cluster_vsearch.out.clust_uc - - // Clustering with SWARM - } else if ( params.clustering == "swarm" ) { - - // If pre-clustering was already done with the same d, just take the previous results - if(params.preclustering == "swarm_d1" & params.swarm_d == 1){ - cluster_ch = precluster_swarm.out.clust - clustuc_ch = precluster_swarm.out.clust_uc - preclustuc_ch = file('NoPrecluster') - preclustaf_ch = file('NoPreclusterFASTA') - - // Otherwise, run SWARM - } else { - cluster_swarm(denoise_ch) - cluster_ch = cluster_swarm.out.clust - clustuc_ch = cluster_swarm.out.clust_uc - } - - // Do not cluster, use zOTUs from UNOISE - } else if ( params.preclustering == "unoise" & params.clustering == "none" ) { - cluster_ch = unoise.out.unoise - clustuc_ch = unoise.out.unoise_uc - preclustuc_ch = file('NoPrecluster') - preclustaf_ch = file('NoPreclusterFASTA') - - // Do not cluster, use ASVs from DADA2 - } else if ( params.preclustering == "dada2" & params.clustering == "none" ){ - - if(params.dada2_pooling == "global"){ - cluster_ch = dada2.out.dada - clustuc_ch = dada2.out.dada_uc - } - - preclustuc_ch = file('NoPrecluster') - preclustaf_ch = file('NoPreclusterFASTA') - - } else if ( params.preclustering == "none" & params.clustering == "none" ){ - println "No pre-clustering or clustering was done" - // This is done outside the clustering subworkflow - } - - - emit: - preclustuc_ch = preclustuc_ch // UC file for pre-clustering - preclustaf_ch = preclustaf_ch // FASTA file for pre-clustering - cluster_ch = cluster_ch // FASTA file for clustering - clustuc_ch = clustuc_ch // UC file for clustering - -} // end of subworkflow diff --git a/src/pipecraft-core/service_scripts/NextITS/subworkflows/itsx_subworkflow.nf b/src/pipecraft-core/service_scripts/NextITS/subworkflows/itsx_subworkflow.nf deleted file mode 100644 index 629eea07..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/subworkflows/itsx_subworkflow.nf +++ /dev/null @@ -1,564 +0,0 @@ -/* -============================================================================ - NextITS: Pipeline to process eukaryotic ITS amplicons -============================================================================ - License: Apache-2.0 - Github : https://github.com/vmikk/NextITS - Website: https://Next-ITS.github.io/ ----------------------------------------------------------------------------- -*/ - -// Subworkflow for ITSx processing -// (which splits large dereplicated FASTAs into chunks). -// The workflow is as follows: -// 1. Trim primers and dereplicate at sample level -// 2. Split the dereplicated primer-trimmed sequences (at sample level) into chunks while preserving metadata -// 3. Run ITSx on each chunk -// 4. Group results back by original sample ID and concatenate + convert ITSx output to Parquet - -// Path to the output results -out_3_itsx = params.outdir + "/03_ITSx" -out_3_itsxp = params.outdir + "/03_ITSx_PooledParts" - - -// Trim primers and dereplicate at sample level -process primer_trim { - - label "main_container" - - publishDir "${out_3_itsx}", mode: "${params.storagemode}" - // cpus 2 - - // Add sample ID to the log file - tag "${meta.id}" - - input: - tuple val(meta), path(fastq) - - output: - tuple val(meta), path("${meta.id}_derep.fasta.gz"), emit: derep, optional: true - tuple val(meta), path("${meta.id}_hash_table.txt.gz"), emit: hashes, optional: true - tuple val(meta), path("${meta.id}_uc.uc.gz"), emit: uc, optional: true - tuple val(meta), path("${meta.id}_primertrimmed_sorted.fq.gz"), emit: trimmed_seqs, optional: true - tuple val("${task.process}"), val('cutadapt'), eval('cutadapt --version'), topic: versions - tuple val("${task.process}"), val('vsearch'), eval('vsearch --version 2>&1 | head -n 1 | sed "s/vsearch //g" | sed "s/,.*//g" | sed "s/^v//" | sed "s/_.*//"'), topic: versions - tuple val("${task.process}"), val('seqkit'), eval('seqkit version | sed "s/seqkit v//"'), topic: versions - tuple val("${task.process}"), val('phredsort'), eval('phredsort -v | sed "s/phredsort //"'), topic: versions - tuple val("${task.process}"), val('seqhasher'), eval('seqhasher -v | sed "s/SeqHasher //"'), topic: versions - tuple val("${task.process}"), val('parallel'), eval('parallel --version | head -n 1 | sed "s/GNU parallel //"'), topic: versions - tuple val("${task.process}"), val('brename'), eval('brename --help | head -n 4 | tail -1 | sed "s/Version: //"'), topic: versions - - script: - sampID="${meta.id}" - """ - echo -e "Primer trimming and dereplication at sample level\\n" - echo -e "Input sample: " ${sampID} - - ## Trim primers - echo -e "Trimming primers\\n" - - ## Reverse-complement rev primer - RR=\$(rc.sh ${params.primer_reverse}) - - cutadapt \ - -a ${params.primer_forward}";required;min_overlap=${params.primer_foverlap}"..."\$RR"";required;min_overlap=${params.primer_roverlap}" \ - --errors ${params.primer_mismatches} \ - --revcomp --rename "{id}" \ - --discard-untrimmed \ - --minimum-length ${params.trim_minlen} \ - --cores ${task.cpus} \ - --action trim \ - --output ${sampID}_primertrimmed.fq.gz \ - ${fastq} - - echo -e "..Done\\n" - - ## Check if there are sequences in the output - NUMSEQS=\$( seqkit stat --tabular --quiet ${sampID}_primertrimmed.fq.gz | awk -F'\t' 'NR==2 {print \$4}' ) - echo -e "Number of sequences after primer trimming: " \$NUMSEQS - if [ \$NUMSEQS -lt 1 ]; then - echo -e "\\nIt looks like no reads remained after trimming the primers\\n" - exit 0 - fi - - ## Estimate sequence quality and sort sequences by quality - echo -e "\\nSorting by sequence quality" - seqkit replace -p "\\s.+" ${sampID}_primertrimmed.fq.gz \ - | phredsort -i - -o - --metric meep --header avgphred,maxee,meep \ - | gzip -1 > ${sampID}_primertrimmed_sorted.fq.gz - echo -e "..Done" - - ## Hash sequences, add sample ID to the header - ## columns: Sample ID - Hash - PacBioID - AvgPhredScore - MaxEE - MEEP - Sequence - Quality - Length - ## Convert to Parquet format - echo -e "\\nCreating hash table" - seqhasher --hash sha1 --name ${sampID} ${sampID}_primertrimmed_sorted.fq.gz - \ - | seqkit fx2tab --length \ - | sed 's/;/\t/ ; s/;/\t/ ; s/ avgphred=/\t/ ; s/ maxee=/\t/ ; s/ meep=/\t/' \ - > ${sampID}_hash_table.txt - echo -e "..Done" - - ## Check the number of fields per record (should be 9!) - # awk '{print NF}' ${sampID}_hash_table.txt | sort | uniq -c - # awk 'NF > 9 {print \$0 }' ${sampID}_hash_table.txt - - ## Dereplicate at sample level (use quality-sorted sequences to make sure that the representative sequence is with the highest quality) - echo -e "\\nDereplicating at sample level" - seqkit fq2fa -w 0 ${sampID}_primertrimmed_sorted.fq.gz \ - | vsearch \ - --derep_fulllength - \ - --output - \ - --strand both \ - --fasta_width 0 \ - --threads 1 \ - --relabel_sha1 \ - --sizein --sizeout \ - --minseqlength ${params.trim_minlen} \ - --uc ${sampID}_uc.uc \ - --quiet \ - > ${sampID}_derep.fasta - - ## Remove temporary file - rm ${sampID}_primertrimmed.fq.gz - - ## Compress results - echo -e "\\nCompressing results" - parallel -j${task.cpus} "gzip -${params.gzip_compression} {}" ::: \ - ${sampID}_hash_table.txt \ - ${sampID}_uc.uc \ - ${sampID}_derep.fasta - - echo -e "..Done" - - """ -} - - -// Extract ITS region with ITSx -// NB. In input data, sequence header should not contain spaces! -process itsx { - - label "main_container" - - // No need to publish intermediate results for chunked workflow, as they will be concatenated later - publishDir "${out_3_itsx}", - mode: "${params.storagemode}", - enabled: params.ITSx_chunk_size == 0 - - // cpus 2 - - // Add sample ID to the log file - tag { meta.chunk_id != null ? "${meta.id}__chunk${meta.chunk_id}" : "${meta.id}" } - - input: - tuple val(meta), path(input) // FASTA file with dereplicated sequences - - output: - tuple val(meta), path( "${meta.id}*.full.fasta.gz"), emit: itsx_full, optional: true - tuple val(meta), path( "${meta.id}*.SSU.fasta.gz"), emit: itsx_ssu, optional: true - tuple val(meta), path( "${meta.id}*.ITS1.fasta.gz"), emit: itsx_its1, optional: true - tuple val(meta), path( "${meta.id}*.5_8S.fasta.gz"), emit: itsx_58s, optional: true - tuple val(meta), path( "${meta.id}*.ITS2.fasta.gz"), emit: itsx_its2, optional: true - tuple val(meta), path( "${meta.id}*.LSU.fasta.gz"), emit: itsx_lsu, optional: true - tuple val(meta), path( "${meta.id}*.positions.txt"), emit: itsx_positions, optional: true - tuple val(meta), path( "${meta.id}*.problematic.txt"), emit: itsx_problematic, optional: true - tuple val(meta), path( "${meta.id}*_no_detections.fasta.gz"), emit: itsx_nondetects, optional: true - tuple val(meta), path( "${meta.id}*.summary.txt"), emit: itsx_summary, optional: true - tuple val(meta), path( "${meta.id}*.extraction.results.gz"), emit: itsx_details, optional: true - tuple val(meta), path( "${meta.id}*.SSU.full_and_partial.fasta.gz"), emit: itsx_ssu_part, optional: true - tuple val(meta), path( "${meta.id}*.ITS1.full_and_partial.fasta.gz"), emit: itsx_its1_part, optional: true - tuple val(meta), path( "${meta.id}*.5_8S.full_and_partial.fasta.gz"), emit: itsx_58s_part, optional: true - tuple val(meta), path( "${meta.id}*.ITS2.full_and_partial.fasta.gz"), emit: itsx_its2_part, optional: true - tuple val(meta), path( "${meta.id}*.LSU.full_and_partial.fasta.gz"), emit: itsx_lsu_part, optional: true - tuple val("${task.process}"), val('ITSx'), eval('ITSx --help 2>&1 | head -n 3 | tail -n 1 | sed "s/Version: //"'), topic: versions - tuple val("${task.process}"), val('cutadapt'), eval('cutadapt --version'), topic: versions - tuple val("${task.process}"), val('vsearch'), eval('vsearch --version 2>&1 | head -n 1 | sed "s/vsearch //g" | sed "s/,.*//g" | sed "s/^v//" | sed "s/_.*//"'), topic: versions - tuple val("${task.process}"), val('seqkit'), eval('seqkit version | sed "s/seqkit v//"'), topic: versions - tuple val("${task.process}"), val('phredsort'), eval('phredsort -v | sed "s/phredsort //"'), topic: versions - tuple val("${task.process}"), val('seqhasher'), eval('seqhasher -v | sed "s/SeqHasher //"'), topic: versions - tuple val("${task.process}"), val('parallel'), eval('parallel --version | head -n 1 | sed "s/GNU parallel //"'), topic: versions - tuple val("${task.process}"), val('brename'), eval('brename --help | head -n 4 | tail -1 | sed "s/Version: //"'), topic: versions - tuple val("${task.process}"), val('duckdb'), eval('duckdb --version | cut -d" " -f1 | sed "s/^v//"'), topic: versions - - script: - sampID="${meta.id}" - chunkPrefix="${meta.id}_chunk${meta.chunk_id}" - - // Allow inclusion of sequences that only find a single domain, given that they meet the given E-value and score thresholds, on with parameters 1e-9,0 by default - // singledomain = params.ITSx_singledomain ? "--allow_single_domain 1e-9,0" : "" - - """ - echo -e "Extraction of rRNA regions using ITSx\\n" - echo -e "Input sample: " ${sampID} - echo -e "Chunk ID: " ${meta.chunk_id} - - ## Check if input file is gz-compressed (by magic bytes `1f 8b`) - tmp_created=0 - tmpfile="" - if [[ -f "${input}" ]] && head -c 2 -- "${input}" | LC_ALL=C od -An -tx1 | tr -d ' \n' | grep -qi '^1f8b'; then - echo -e "Input file is gz-compressed, decompressing..." - tmpfile="\$(mktemp "tmp.decompressed.input.XXXXXX")" - gunzip -c -- "${input}" > "\$tmpfile" - tmp_created=1 - itsxinput="\$tmpfile" - itsxoutput="${sampID}" - else - itsxinput="${input}" - itsxoutput="${chunkPrefix}" - fi - - ## ITSx extraction - echo -e "\\nITSx extraction" - ITSx \ - -i "\$itsxinput" \ - --complement ${params.ITSx_complement} \ - --save_regions all \ - --graphical F \ - --detailed_results T \ - --positions T \ - --not_found T \ - -E ${params.ITSx_evalue} \ - -t ${params.ITSx_tax} \ - --partial ${params.ITSx_partial} \ - --cpu ${task.cpus} \ - --preserve T \ - -o "\$itsxoutput" - - echo -e "..Done" - - # ITSx.full.fasta - # ITSx.SSU.fasta - # ITSx.ITS1.fasta - # ITSx.5_8S.fasta - # ITSx.ITS2.fasta - # ITSx.LSU.fasta - # ITSx.positions.txt - # ITSx.problematic.txt - # ITSx_no_detections.fasta - # ITSx_no_detections.txt - # ITSx.summary.txt - # ITSx.extraction.results - # ITSx.SSU.full_and_partial.fasta - # ITSx.ITS1.full_and_partial.fasta - # ITSx.5_8S.full_and_partial.fasta - # ITSx.ITS2.full_and_partial.fasta - # ITSx.LSU.full_and_partial.fasta - - - ## If partial sequences were required, remove empty sequences - if [ \$(find . -type f -name "*.full_and_partial.fasta" | wc -l) -gt 0 ]; then - echo -e "Partial files found, removing empty sequences\\n." - - find . -name "*.full_and_partial.fasta" \ - | parallel -j${task.cpus} "seqkit seq -m 1 -w 0 {} > {.}_tmp.fasta" - - rm *.full_and_partial.fasta - brename -p "_tmp" -r "" -f "_tmp.fasta\$" - - fi - - ## Remove empty files (no sequences) - echo -e "\\nRemoving empty files" - find . -type f -name "*.fasta" -empty -print -delete - echo -e "..Done" - - ## Remove temporary file (if input file was gz-compressed) - if (( tmp_created )); then - rm -f -- "\$tmpfile" - fi - - ## Compress results - echo -e "\\nCompressing files" - - ## ITSx results (no symlinked derep input) - find . -type f -name "*.fasta" \ - | parallel -j${task.cpus} "gzip -${params.gzip_compression} {}" - - gzip -${params.gzip_compression} "\$itsxoutput".extraction.results - - echo -e "..Done" - """ -} - - -// Concatenate ITSx output from all chunks (per samples) -// Convert ITSx output to Parquet -process itsx_concatenate { - - label "main_container" - - publishDir "${out_3_itsx}", mode: "${params.storagemode}" - - tag "${meta.id}" - - input: - tuple val(meta), path(fasta_chunks, stageAs: "chunks/") // all files from ITSx for all chunks for each sample - - output: - path( "${meta.id}.full.fasta.gz"), emit: itsx_full, optional: true - path( "${meta.id}.SSU.fasta.gz"), emit: itsx_ssu, optional: true - path( "${meta.id}.ITS1.fasta.gz"), emit: itsx_its1, optional: true - path( "${meta.id}.5_8S.fasta.gz"), emit: itsx_58s, optional: true - path( "${meta.id}.ITS2.fasta.gz"), emit: itsx_its2, optional: true - path( "${meta.id}.LSU.fasta.gz"), emit: itsx_lsu, optional: true - path( "${meta.id}.positions.txt"), emit: itsx_positions, optional: true - path( "${meta.id}.problematic.txt"), emit: itsx_problematic, optional: true - path( "${meta.id}_no_detections.fasta.gz"), emit: itsx_nondetects, optional: true - path( "${meta.id}.summary.txt"), emit: itsx_summary, optional: true - path( "${meta.id}.extraction.results.gz"), emit: itsx_details, optional: true - path( "${meta.id}.SSU.full_and_partial.fasta.gz"), emit: itsx_ssu_part, optional: true - path( "${meta.id}.ITS1.full_and_partial.fasta.gz"), emit: itsx_its1_part, optional: true - path( "${meta.id}.5_8S.full_and_partial.fasta.gz"), emit: itsx_58s_part, optional: true - path( "${meta.id}.ITS2.full_and_partial.fasta.gz"), emit: itsx_its2_part, optional: true - path( "${meta.id}.LSU.full_and_partial.fasta.gz"), emit: itsx_lsu_part, optional: true - path( "parquet/*.parquet"), emit: parquet, optional: true - - script: - sampID="${meta.id}" - """ - - echo -e "Concatenating ITSx output from all chunks" - echo -e "Input sample: " ${sampID} - - shopt -s nullglob - - ## Concatenate ITSx FASTA outputs - echo -e "Concatenating:" - - full_files=( chunks/${sampID}_chunk*.full.fasta.gz ) - echo -e " - full ITS sequences: \${#full_files[@]}" - if [ \${#full_files[@]} -gt 0 ]; then - for f in "\${full_files[@]}"; do - echo -e " \$f" - done - cat "\${full_files[@]}" > ${sampID}.full.fasta.gz - fi - - ssu_files=( chunks/${sampID}_chunk*.SSU.fasta.gz ) - echo -e " - SSU sequences: \${#ssu_files[@]}" - if [ \${#ssu_files[@]} -gt 0 ]; then - for f in "\${ssu_files[@]}"; do - echo -e " \$f" - done - cat "\${ssu_files[@]}" > ${sampID}.SSU.fasta.gz - fi - - its1_files=( chunks/${sampID}_chunk*.ITS1.fasta.gz ) - echo -e " - ITS1 sequences: \${#its1_files[@]}" - if [ \${#its1_files[@]} -gt 0 ]; then - for f in "\${its1_files[@]}"; do - echo -e " \$f" - done - cat "\${its1_files[@]}" > ${sampID}.ITS1.fasta.gz - fi - - s58_files=( chunks/${sampID}_chunk*.5_8S.fasta.gz ) - echo -e " - 5.8S sequences: \${#s58_files[@]}" - if [ \${#s58_files[@]} -gt 0 ]; then - for f in "\${s58_files[@]}"; do - echo -e " \$f" - done - cat "\${s58_files[@]}" > ${sampID}.5_8S.fasta.gz - fi - - its2_files=( chunks/${sampID}_chunk*.ITS2.fasta.gz ) - echo -e " - ITS2 sequences: \${#its2_files[@]}" - if [ \${#its2_files[@]} -gt 0 ]; then - for f in "\${its2_files[@]}"; do - echo -e " \$f" - done - cat "\${its2_files[@]}" > ${sampID}.ITS2.fasta.gz - fi - - lsu_files=( chunks/${sampID}_chunk*.LSU.fasta.gz ) - echo -e " - LSU sequences: \${#lsu_files[@]}" - if [ \${#lsu_files[@]} -gt 0 ]; then - for f in "\${lsu_files[@]}"; do - echo -e " \$f" - done - cat "\${lsu_files[@]}" > ${sampID}.LSU.fasta.gz - fi - - nd_files=( chunks/${sampID}_chunk*_no_detections.fasta.gz ) - echo -e " - no detections sequences: \${#nd_files[@]}" - if [ \${#nd_files[@]} -gt 0 ]; then - for f in "\${nd_files[@]}"; do - echo -e " \$f" - done - cat "\${nd_files[@]}" > ${sampID}_no_detections.fasta.gz - fi - - ## Concatenate partial outputs if present - ssu_part_files=( chunks/${sampID}_chunk*.SSU.full_and_partial.fasta.gz ) - echo -e " - SSU partial sequences: \${#ssu_part_files[@]}" - if [ \${#ssu_part_files[@]} -gt 0 ]; then - for f in "\${ssu_part_files[@]}"; do - echo -e " \$f" - done - cat "\${ssu_part_files[@]}" > ${sampID}.SSU.full_and_partial.fasta.gz - fi - - its1_part_files=( chunks/${sampID}_chunk*.ITS1.full_and_partial.fasta.gz ) - echo -e " - ITS1 partial sequences: \${#its1_part_files[@]}" - if [ \${#its1_part_files[@]} -gt 0 ]; then - for f in "\${its1_part_files[@]}"; do - echo -e " \$f" - done - cat "\${its1_part_files[@]}" > ${sampID}.ITS1.full_and_partial.fasta.gz - fi - - s58_part_files=( chunks/${sampID}_chunk*.5_8S.full_and_partial.fasta.gz ) - echo -e " - 5.8S partial sequences: \${#s58_part_files[@]}" - if [ \${#s58_part_files[@]} -gt 0 ]; then - for f in "\${s58_part_files[@]}"; do - echo -e " \$f" - done - cat "\${s58_part_files[@]}" > ${sampID}.5_8S.full_and_partial.fasta.gz - fi - - its2_part_files=( chunks/${sampID}_chunk*.ITS2.full_and_partial.fasta.gz ) - echo -e " - ITS2 partial sequences: \${#its2_part_files[@]}" - if [ \${#its2_part_files[@]} -gt 0 ]; then - for f in "\${its2_part_files[@]}"; do - echo -e " \$f" - done - cat "\${its2_part_files[@]}" > ${sampID}.ITS2.full_and_partial.fasta.gz - fi - - lsu_part_files=( chunks/${sampID}_chunk*.LSU.full_and_partial.fasta.gz ) - echo -e " - LSU partial sequences: \${#lsu_part_files[@]}" - if [ \${#lsu_part_files[@]} -gt 0 ]; then - for f in "\${lsu_part_files[@]}"; do - echo -e " \$f" - done - cat "\${lsu_part_files[@]}" > ${sampID}.LSU.full_and_partial.fasta.gz - fi - - ## Concatenate text outputs - pos_files=( chunks/${sampID}_chunk*.positions.txt ) - echo -e " - positions: \${#pos_files[@]}" - if [ \${#pos_files[@]} -gt 0 ]; then - for f in "\${pos_files[@]}"; do - echo -e " \$f" - done - cat "\${pos_files[@]}" > ${sampID}.positions.txt - fi - - prob_files=( chunks/${sampID}_chunk*.problematic.txt ) - echo -e " - problematic sequences: \${#prob_files[@]}" - if [ \${#prob_files[@]} -gt 0 ]; then - for f in "\${prob_files[@]}"; do - echo -e " \$f" - done - cat "\${prob_files[@]}" > ${sampID}.problematic.txt - fi - - sum_files=( chunks/${sampID}_chunk*.summary.txt ) - echo -e " - ITSx summary reports: \${#sum_files[@]}" - if [ \${#sum_files[@]} -gt 0 ]; then - for f in "\${sum_files[@]}"; do - echo -e " \$f" - done - cat "\${sum_files[@]}" > ${sampID}.summary.txt - fi - - det_files=( chunks/${sampID}_chunk*.extraction.results.gz ) - echo -e " - ITSx extraction results: \${#det_files[@]}" - if [ \${#det_files[@]} -gt 0 ]; then - for f in "\${det_files[@]}"; do - echo -e " \$f" - done - cat "\${det_files[@]}" > ${sampID}.extraction.results.gz - fi - - echo -e "\\n" - - ## Convert ITSx output to Parquet - if [ ${params.ITSx_to_parquet} == true ]; then - - echo -e "\\nConverting ITSx output to Parquet" - mkdir -p parquet - - if [ -f ${sampID}.full.fasta.gz ]; then - ITSx_to_DuckDB.sh -i ${sampID}.full.fasta.gz -o parquet/${sampID}.full.parquet - fi - - if [ -f ${sampID}.SSU.fasta.gz ]; then - ITSx_to_DuckDB.sh -i ${sampID}.SSU.fasta.gz -o parquet/${sampID}.SSU.parquet - fi - - if [ -f ${sampID}.ITS1.fasta.gz ]; then - ITSx_to_DuckDB.sh -i ${sampID}.ITS1.fasta.gz -o parquet/${sampID}.ITS1.parquet - fi - - if [ -f ${sampID}.5_8S.fasta.gz ]; then - ITSx_to_DuckDB.sh -i ${sampID}.5_8S.fasta.gz -o parquet/${sampID}.5_8S.parquet - fi - - if [ -f ${sampID}.ITS2.fasta.gz ]; then - ITSx_to_DuckDB.sh -i ${sampID}.ITS2.fasta.gz -o parquet/${sampID}.ITS2.parquet - fi - - if [ -f ${sampID}.LSU.fasta.gz ]; then - ITSx_to_DuckDB.sh -i ${sampID}.LSU.fasta.gz -o parquet/${sampID}.LSU.parquet - fi - - echo -e "Parquet files created\\n" - - fi - - """ -} - - - - -// ITSx processing workflow -workflow ITSx { - - take: - seqs - - main: - - // Add metadata to the channel (fetch sample ID from the FASTQ file name) - ch_seqs = seqs.map { fastq -> - def sample_id = fastq.getSimpleName().replaceAll(/_PrimerChecked/, '') - def meta = [id: sample_id] - [meta, fastq] - } - - // Trim primers and dereplicate at sample level - primer_trim(ch_seqs) - - // Size of dereplicated input for ITSx - // if null, use default value (currently, 10000) - // if 0, use all sequences in one chunk - def chunk_size = (params.ITSx_chunk_size == null ? 10000 : params.ITSx_chunk_size as int) - - if( chunk_size == 0 ) { - // Single-chunk workflow (no data splitting) - // NB! here, fasta will be gz-compressed -> will be handled in the itsx process - chunks_ch = primer_trim.out.derep - .map { meta, fasta -> [ meta + [chunk_id: null], fasta ] } - } - else { - // Chunking mode: split the dereplicated primer-trimmed sequences (at sample level) into chunks while preserving metadata - // NB! here, fasta will be uncompressed - chunks_ch = primer_trim.out.derep - .flatMap { meta, fasta -> - def chunks = fasta.splitFasta(by: chunk_size, file: true, decompress: true, compress: false) - def result = [] - chunks.eachWithIndex { chunk_file, idx -> - result << [ meta + [chunk_id: idx], chunk_file ] - } - return result - } - } - - // Run ITSx - itsx(chunks_ch) - - - -} // end of ITSx workflow diff --git a/src/pipecraft-core/service_scripts/NextITS/test_data/Test_ChimeraDB.udb b/src/pipecraft-core/service_scripts/NextITS/test_data/Test_ChimeraDB.udb deleted file mode 100644 index c7556dd1050c1b29f64bf8ab0b4ddeb49ccc5b3e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 587232 zcmcfqTeD^R)9rOVo)3s@&TSqa^X8Ly!6WVACB)F^{Hx=tB-rzPuK!ofhxqRg{{Q*^ z9^n7a?f?6A51sjc`nvvSudDih{;hwdzx$tjyTbp+uZt!e_P_eZ#phG*ig&%ruew~M zi?b^~v*E&ge7lSF-t)z!x4*n|e9hhxjx72G zm0hp$t8TWPJ^Q%rd3jx&yU&Zlncm%xv!rj*0*}Y%xadeUa#f& z!|334TsGEC18UM}L}_=OeqznYEP_jx`2&EC(8?|c8w z>giwBKY8QB3;pWf%#V+8?uj0Ixy7sF|LHf-vnm(7Ht%zJtA2d6%dG6Rj2CwCgZDfO ze7X59vF8cnVJ1EdZg%%KSMYjwzyCw-(N52E6o3IUS0LQ z-m1TTH20J}>vF+U|3-h{diN>AyBB+SY>rz^b-ee0{V(XdPgojP9M7w#^$gR}{O2{V zsJ}r2r)iJ#R@2#^zS@PC%jx*{Gm4%0Rhz5cGn&Ph_j)k!Br&?~N z&^x<5?o%(nr=CCZGJ93dxcHy=(&zt4Z?|m5PII@vx_Ylk_4NxY^10{Xjo;@Gw>`~f zb=p4-=W`abdzQTFX?b@Zx^kZfi>1FaFX*S&fN!~w-`)G;lSkNU@3YzY{e0b->MrNR z-Q1sP1U*goVtdx`<#?F6y>o=SKli1_(dOUh-tu7^v|-i7J=>j0<7;XhfAjIMUiW=Q zaT~ntjJvh>jXC_zdKNHRetLYlvpI7;kF<2vpJHdzGy|)z<{5WAZ{JJ-=T)zcSJQ=w z#oS-|@XOWzR|Zd}Eqkp=`rXcYx<}`#Z?3Dl3m3Mt^#%RA`!P!kr}e&$U+>ky7yDE@ zN4#3TXV|l<|D|uncACZa^G|2)_u$3fzpkF6NcUfUYx4NtzOMURd47|9e`e{O(LNe^ z&!xu6>tDUP<1=6NcloD&v1aX) z;|0u~&H+mUcZS8P3;O=-C!Y4Eqov!tGvTW@C!5<|y;+_9=^o-;>$!8iZrkVM`|jfA zZ%%XXjpkSLcP1_Uy%+fPy=TW?+&tgi)y(A=Tdw&%x4y6C1>d{>&AvabUvsPFO~Y^J z)6<31m|3lQ_09V{*zX{WB13^yPWH3hGu8fp3Bas-?;s9 z)pz#l>+MOub$wx`>Ei1Jy&P`m-D74m&7JvIU!pt<`)I<< zEr0R)g3r#pnI9kbIoh-L^h5dMJh1dv`?L!tH^24zsmpu+?Qu2h+&*vC@7DeKD9rUL zANPkBtGhk#&ax5iXKZJqaUU`dXXgXoeldRd{JJh4Z?5cGyjuL~`(xhxdFt<6emLId z&C2oWo6Rm>tvBm?Z_fa?&m`>bZ&sKotK;;G-TR^|`*SyJwY=gA{bu-=;Pt^jZ_;1B zzI*3UzX5j+{c5V?adOY^MmpL!zUuAs5o(!EPgh>-Piix1y$_vn=hJQM8Pe^Aqvun; z98H;yMjsdE&!hW3#hIxuuI7#YynoMQ-cGr>r#j5_$s1mN@^9XIzWV7s(`;c^KD-vM zzR}D^fBq_(k1PL2-|AQTyMH`&ob&MU{&1FN(*4^Nuc+?2!RO+9;eSg{TXtWI<*Dai z-m}DaIrG)uzWJ~`x~ggN*naUB_0!(^f~MLT;LC-H-Sd9|PQGX0(`)C<$@x@=+rRU1 z=F4Va@kPC7QvXTk^Eb`)FoRpZy2}~URO{vNVrT1V>EzW_JKy5X7iQkwyH6J`$L;LS zEM9$kvv5Dt)b~4Rz3*4`MSOeq96a#))jFG;mJe*-7rbPi-cy}NorXuXFrzQS%4$9^ zKM$sl6Zo>{!z0a}A$+N~Te=A4-Dd&s3wVm?b=g#26KH=V5_|@{gSa?{ui--5)3;G^>zXPuVcJq_w zVrIf`y*)mx%X@Y2$=TzpWttcC74=i~j|EqZ7?<~*jlP=El?Vr6B^zP}6zCZS} zRs4VOEms$2WUrO4tu#+Ltl;(T{`!&kWx9_pf0mAa(mr8n#qajF!?S9JpZ)8z9BsPd zdc4t}@85h}_0GV;H?}t~r(1sKp1Afo?V~dFaFNVIs^k zt1fQl=PYm4`y=fqe>3U2=$fwn_rGO$*~R+>ePLzc%w;=paz0P7yW;H$@AI*6UVGl? z&mY}S=#$RpHZRk=&sktRpUzHe7hiui%KvGOr~NX&cwZB$|FhRsF64Ljqle#YX3z1Y zp~KP3yDv=_uDWO7wemb((D(D22{V^Y9q#WP_0NOS3$w6h)Nh+xgZl8Yddu`3a^yc8a;j8<bLXp@iF4UmzhTZf+=uMXjdbulU}p7Yb-HFm+RJahJP%kNVLqShCT^Pgz)D$KpRzkbZC z=T_$XSJfMd-`L+pwWsT!u3&0>nNM}m{=##H?@XHhdFgV^dboAu1&-9Icoe6PJPe0d}N#_i+QlXkJ4QQvdjK3(@9;#ULTyw9Tf z^u^{p`Rp^NcfR%dGI@PiO?4W2o@woibA?yO?Hrs>Jui8=_@}Tlj^6XD?{ZJRcGY_Z zcis%Im-H)E=RJliFJHg&xUfB2I^m4w%;0~OX`8L)QTFwvd3^QG)C=d(^V)uW!fANQ zW>y399P~0T{brmTr+(ULHjQhs-_`NWb=Lgtl6zKsVfq*KJ(}%GZ{|DsjjMMLylC`$ zhUM^HyLw)|&+T;vvt_gLa$Cpmh1ycJ3^C`Q>#j2mQSEHvbyKwS9 z%O3u`Hrpr1c|Kw4JF~sop3P4+{h#FKUG(b-`;SN|uPr#YCj>2G$H zi~p{_=gJ3m^X`aq?T(!fPjj)=(_CzO=b+slcJ&K8XI8&BZ8_X^``^u^-)LVm_UhB{lEZ!%wDRiLJKWCd`!ftq&Ts2xM4T&ZcW+I< znJ0`MuE%etSuDM4@oKmn*4Kb~UR%e*eXY>P73)CEvc_qzKzI8D*d2;L90U&QM}{rZR70e4SnH}GLNd39lnt1rGY>eYtbnLR_i zUVfSh`_pKjKT9}Ev+8@E?N?(TYb+4ypX8Fa}^Qm4hbvl0Xo%PQ;;CAZeJHNX2ZBHxbu{BTGERF1WxgWav z#e8jAziK`=J1>2C{kEq0^j^f9+1}o;t!bXH)y6-4t+Q+A`@Gg~rnOU_Mvaqup8LCF zet6luym&J%_|s0BvS*y%eeLbwp)afLk<$vbyr1t+@1Oqt_9tKa%e=PUeemm_#<^wM zr+IsDpYnP*yZgk!dCGgnuyo~cXMC04UsHV8#>KbK&(+NO*Qf10(0jS}pV@Rb7TY>q zz02J8>ii46{6^m&!<++t`|vw^HJ)R-4LUfVes?U=bZyWtre5yW_H`cI&Fd4_wR*fv zbNh=gUmaiEdwZilc>T@h!_}X%S-ad%`mK4T^Q_F4(~4&Ig0Ciw2EQ1tEMWRFzU&^B zSJUb5S;G1AF|%21zU+D6%3UudwyH_4W94=Jmfbdfv4AENOhrpe>8LI~-3>`@2kUe)Zn>u+?mh|6Sgh z>U$r{=cV6s!0`2pg%_XjH~Z%Aeq1?jJ?z-L_0Cq~F0Rktj$fT4A6{GY>F>1f^MT9u z=x^p7zV+(v`J97yeRY2(Sk8MmulW6Ua>9hYe-HSUeVxS1D+^oPv-po+xIZJWUTw~7 z*?(UIx94wewfgQ09wvA^aWv)2n~U@HxgABEeK7cHR~KK{L$?~->eO+~w9m7}mHAaa zMf2Bx`==S$-XpAhJz;XZ=M*OH-X|aT`n3CL{*yQSSJ{sB-P&wt68N$@Zfm}Grv8%` zZL?kcpS*ULxnJegU%#ih)!g@&bIDKhaqF>~JzS^ZP z7vavmGhgKSz(ekH(@Y-fGxmi$-=FTvBfs_S=hmkku(JA9evgZ9PcMHT`c_b>S z&Dh(&w{in_@#c!%^+x{s(fiWB7jqSM@VZ}L(&O%|bA-E>#a6%g)-+)k|I}Z6J*qE% z&mX@U_Y7Z-H{ZCvwwS@2S)AY1)Z@(2JmsrtM{#kl<(vKaOC3+vhVv&g^ms+WuZ6sz0%Nx!^WrtfR1SZ-_l<|lr! zTi5UV=JM;UUQO8cYT3+FPV+k*-g{S{-#zy}v!{JwPxG*wugB%}Prf^w&h;nw-q+RF z#|vjzY`J|Vw9X>0ertNz<|ppc8G7wjzx|#;&8L3r_V4>3u8X&v?gk%t+I==~e!uhj z70*jf>)LzA-+b@uo{1N{`RW^VWwAOqU7F>$HeX)7esgnv9~Hi_uOHl=Jx`pTZgZTu z@}B9_99-t7=eKz85x)zA+gEl!m(y=@fedRT2)}A!g+s7BU>i!vY;SAN+GpyjXzHjE| zkMYj9yHf3*+vDW;;!_Wo-|F{AXE5*V`qeI$SJys!+TF7?{^m6MXQ<2T)BVbC&u*r? z_#xGa> zDRz#=J?tJBO>>@U*z*6ytAFx^`MpAOPmzxHiMOlQ!Fy(B%GA9L;)G7x(oI!>QeKJ@4vbwfeB@8+Ns;znor9Tj=GmdwQsdq2w)vF8l^_&-;K1~>%p02#wYS(I-E1s~$zeL|YtncDmUk(2>)!T>B zUtBMzeTt`f*pnXiwSKHU~Ddcze{^%Jkrrvvn z?T#?>X)m|0OWNhD3uj!;UL0M07v2o7ayFL-kHCpU;nM0XEDAP+h<~Kv7Iv;Pm6!b^l@9)+Yx>_e0e#w9KUh#dgrXZe6z4} zUw;el!|L$$2rHhjH}n3uzKegV)4OQ?hj^IX%$MJC)%e9}_viQJe^>wN`S89uPuQ*N z`<%`<4W0ageu}GEowGFnFR!;hH!$`0Z|OgMeOYg_&F~Mqe_x2F{XAsz&F)^f9KQiC zi|RZI_3jPRS9cZrIZ+R%DLZfc#-2I+YW3xfdY_YUKO^bQ$nY|(?4Iz;RqL;g9!@9k zJ+B7ed^L{W=D77)PMybI@Xfn3{K<1QzpL%DWA*&||8N#@?&H4RUxIHM@4bCL@s(Ha z^YC&sPxly?-i&jF+rPY;KAm{N;-2s)jE}tgVVm=Kx~C`ai`8Fjy|+%c&&AH`asB9c zT_64B^<_KcJ;$A=x7YK7r~g%$OY=#)o=>_@bK$P$KE?W9J@0b5<#%p5Gk4#`dBJ6K z`)v5*R;!kG?&@j6x2|u_&*=3}u`|`pl;LX6M(<4eyu%h($BBI4WzXS1ebt>c4F4*> zpTpNL`qikNrMv+TQ`40FtP#&YJYTvv@rk1?tD9LLxQs6si`VclXzyJ>YWw_ln~q(=VrX#&Fa$g z+*vbW{(0eI;q>zIJl%)y%V}`*<$wKkJ^6&W|7!iKeAI^*yktAed{$R|!)&=TxR-QW z*YmF~F8@Y<{n5GXrRII{Px|KW&%wN|2mDk1m40_zUA=SXz4h*b-}%*S?LAH3bLNo_ zE{DahCaj>jTsSO$J^jvB7t6hoXN)h6+3@SJ*i+4`Ip@58-r>*NOt`?+SJT`~zj
53Zr@1@*f72GHgURcyPV2p#jyC*LG)u=vHiQ4)Q|+ox>oqeMzOnO*SM#E|IZs%6KgTY<{H^bqZlC9d_ssgl z-+pJ-Gaaq7^4u9Z+RJUf_1zW6tNLkIxcD?5f6qd@_+o0_dk!-+a@y*6nRdCwx~KZ5 zncjQ9x@SIeI|_bV)5tjItS@%CG++uuj5hVDKu zc-p7fGv3{+=i_Tuz3WLAX6DwP^!AtCNA>!wCY+9ESQ=lq!mlQ*SYNelhTnVQ=b5nf z>>1PZh!@VSFQ;qI>eIPi<#%p1;TyYu>onW1M|D@P|KhLqE1E0h)olG%{qVqVEF8cF&XN`rh+wzjOKBezAT3tOreb<7%F` ztI?PD9^8944Zqum`@63+GGBkj2@mVP&A0p3^Qm5K9={6O^-2HK!{na1`LgxYC7jAH}mx8H`-?Ry;m)0ZvGFy2^Oz*?R@Wr&hU?c-}+kf zD1G|t1*6@Z*4J;idx|rwrjf%p>K9X&^AdWze;40AI^S#S?>tSJ&#hNeorh~@-1F71 z=5qQptJ^cDiTl;r)7-e8{N<~!|1`7OPxJ7b*LUw>J!v*?ZH~9udfd}L16U9BevYk= zTDGgadoEuee|^ozFV}f_#@*PtIQLh*bD!>)fBN>C-!sG&xOeyY`!jvLzn9rL zdjEb3`kXfHnpgSLeboOd^4(`byZbLz zzdOAicSfJSdiyYuS3y&T!OPBBw$pv{zWsXg@;zGLjD39K#A0-~+r#aB>b-8bJm_~< zd_BF|>u+YyuyymrVrJgVkJlu;c;c6PtA2d+n*2b1UGYlgb$r_S0BFpdYaM8RA_PhMF^I~3WUibOI@7(QI_lcv~Oe@D#-`>pPu$$BP zGn4!Ay3q2uIUYv)r}=N0bDo{4-u_SbUR~awd@kNUV_0nK>-(wSnY%jAuq7tMB~p`eruh<+$~)U#@=RiCb*z zJBO3;^%uiE<=<(qpZmM`dh^`;RPRjn^}PMn*YE4R{O()4nlH`e^`H2j!6y#hxvE`x z;^f8r{0wV{XKkN8%?+ON@-P{7S|6D{s*wfoKGvp-9I4L*F> z*$4V3D9?@CFQ?|;>r(CV``vVWaOM4TPnxiT_TBx!FGp84n`UeMV%6KHd5TY(``S21 z9?O}R$yi{;+PTjO3~{QsoiS#z+bv*p=;Z?ow2 zFU+a;oYnU}^ycX8&1ZuiMqgjOf8UgLz5j!6gD`va-pBQ@ZzDe?-0leb-@UGX<#qME z`g|?(D(jo0d6i$!rQKb=*4PnW@OgJXJ}!P|_Iy7k-d?zJbNZ{>zWQm$Vq2$w@(mYP zU+ZOpWgGYrtLD*c*r)rY z`&C}gYUjDHQ68(ko;$l-c;T%2r~HJOHD6Cx{S;3;Om42){XY3zEd9=VM(N;Bp3ALg zyYp~wPqVAv`pNT8?!{bQ)q4-H@V(&ckB=*E|3=>*-QQyAi=9(1XD7_e*YLLwxbCIU z`x)bn{=jL{{m%b-@G9SZVf?BE%(>v(SG(fP*s)(z@H-z?cs=De`t!$~+nQ?6v2|yv zceZ|(FYfnz{F{A$v?tCz;djS>^o^%q&CXqZb=B*6IsKlKU)U$yd*OO-&&Q6NY2|n` zyY~sFyV(w&m*;!&`h8!S&(FPH@A~!OarNd_-#N_gpFei5nl9hw_8z>qX?91ovy_Y7 z`$m3#f7ppD|F^Fz-0!2p>Ur(%C(e20G#k5#_GA{xo;* zQ7^Aw^PfJC(0=`se6{ydNbRnFJN24o>yPv)XL;{qL^$Rnvg` zIpcb7U;Uqd6T{2uD7C#|&Qx72_eOrkcwBGV&8vl*li|B}&$M`2p5~sid9!-jUwK?^ z`={Ct-oMJ`<(>83Z{B%*PKTMPev0*qProx;Kh0lWFZ0~|)T^jT=i;jFZd^}GvLy@(y95->US=^z}Ne;0!*g$?^5KIzgXSY@wbPC zJ5!i=(qFFKbmgZy@9^D0_kzD-X9L#vt$2Ivy)M4q;n$y5cAn;Twt8ou&cUNh%cI^! z$8){RxgWh*Gj{M_&(*-2UoBs}+}?D~^huZAnP}EO{!^r1t@$TCon16$7Wd~A&(6Ix zLs#Z`z4UwU=5W8tX~mseF3si5l{;V8o+JFmdQ`jrYR@3dbNKgOHSbsXJ@$F{`pLU| ze=iZXGvAG-*!k6}Wh1K(yFA@yGij^cCr!O)5|-u$j8{27p77?LyzqP8uma|_pvA+} zshgv#F7$gQc-bCUUR~jxxvSdzy$|PkNxtUnt556a#>M06dvEM~weu|gg1_Pk!<8@Z ztl`z=)oksaUy?7)-mg0SQ{9ZPgLnD9KK(ya!+k$)mR`Mm*v1nV=1Qx++U@uGFK*V( zeN9$}U;U?g&vfVbhj0C~3y-_|u7_RC(aOCp>YZDxx>)Ru{PlHRjXB!-`#x|lX;xpo z=Z!DISN8=DwtjKz!!KOD=c)EQ;=6p#yvo1qjTg@4Z0of<@a8_{bk6dm`Q+&wfB$XB z8R2DdG2G(SyUT2O_1o{)oc7bDDfGTC>Yeqidhg}s_eE)4>HK@M_urqP=T)``c6%D< zSble!r^l7=vsw>x^!BXJJ}X^V@#0-2vY-smoW()7j*3=Yg4z+qzz?wz?~y z)x(>qE*86gj@?;jGV?Y3?Sp&c84TO3j`#Jz`_ioQ*!>URyqa0f_V;XN(-iF$Jj}?e zNt=$(o*T}G@28xu9rW%yy`6pT{Wk%xh_|m9nz&EFn^wPhIllkK%don?mlgl?`oG|> z;H57&6W-;%U@zz^+`F9CS-wfWCp+W_eqrlTPX|w5>`eXsY(i_kzUwLY$8G(j$MbcM zW#_TOv(nS@2V0qRdpy4)4Uc-~y?$|` z8L{|BQW+nF=aj?yI#yzH~&foV~+WWnA&%itF*7d8aFJNVLb47j6kqz+a#tMeRer_o0~f2-tM6_*-5=f@&veef3s=Tf z3m&-M1EM{}dg;H2*9UX;)jofmm4E$e(-*wU@!pSd^~GwoSC@-+?0a?3pzhC~`O>DZ zFFSj^om-oE(ce9I#r5^O-se#sMO?x2)}DE1OG_uG4|l=KMLP@q^?0*C_&sNwdC#7Y zKEHzQ-TgSH{_ag5<~8wqkuaWi@%Yt_tF^D$%hlV{eRSm=UR|~;ou4saFX$)D`m0x) zzKGj%eSzOT#F>R(Oi!2B27Yl`nf{)`O!r?bZ+7n~%(MC$m=E+f`x$2ceAbv<&H&rq z`y?)3-m8gg$6~Y>OZyb=zqzfO@BZy=miNCk_2Shv zjXAp8x2N}p_ecF|>B9xS9=}-iUg`8cSDWkk`tLFAzq@q3cD}mU*XE79Kkn?})t9Sx ze%kG?J`KJc?~1!&UN%d2`*eJ}ziOG57mwSAhq-vb%Ny}sd{$4#OJ82B8Np}!u%?le-m{vG?|oG)=3L&}db-ty{f`?~MU>UhMf$pK7|Rx!zCru%4glcEtHQS8r$aQ`jx1QI`w- zyLQ}d1xSqbOZuV2Gj=tBn!0lOkUF`Z2 zoe#};&gMz@f9m1ocG!D;R!3X@r2AX;UM=79jVJBZZ0~-;KFx-A(XMVe&v?C7`w7oC zUGsLNE%;aW9{7ifSHSr<*OdlV?!8gwi+e#|Q4gc}M*WoNWMnhu!!Ey^8TnTnFIs22 z{>#Uu+vjLU+2;^1v%ku;``V_5^RKUVr77Tc)5ocK^NcHa@zCSGM6VCc``QdEr#GuF z3x4I|Y5!t$eBV<)?n8fbdiS^skYw>TZF?20Qo3*J8)(#z?#uD^jVyH*+SHF6~p$ z<8BYXIi2fr`rmosH+zlqc=D&cJN6lPZD4mM+_SkmZcn%8y7SILtC!ca-^UluB!@Mx zSJzj&CwxAzSNYS=ef4y*v&l64>o-;xUcX*XwVyq4>-V?R%|Cf~4RANN!?`aPj!Sp> zde{>mW}a^I`iDC!ulVr$XSJ}n{pU1cw6F53uIGu@m;L;QYwlC5rh4P4r+5FWhjj+J z_$Qy8+d3^DbLHMoe%?us+xq`l^9uX&&R{m%RH&BAWJzI_epS#%$1J!5~qctPK>>LM*&Gj_ynU0--cX5Q%gI(uGm%YUl*`)6bI;>qXfoMA4r@PG&HlT?VNcroe0KIzy*{0P`#W>*@A7&c^3H9IFK>N) z^fEm!v+?Si%{F5mR;GvPW%%ahoCUw!Q|-MO=Y4WL)+?Rs;(GU{FR$mGg9cW{({JwIMWVl+*LUZtmtPJJ%h%Pe%YW*d!Kv$Azw+GA+txa$k}%?Cbm@~xl7f5-PVa5dH2=ed}#nc+`yXVYA4 z`}KbE`-FMky#9y!@ZWi_CvTd~`#G21|9>LQer}ps|1f>oz0lr_|8$;D9#?<=-2{8m zb!OLnPtZAs`zwc)^Voe`zo?($NuR(>o^~ycU#xoLX)gY0Hq2%2Nr&U3mz$|JE3Y0_ z-a5@w&@2~r_dMy$#4WGB_!`ZbDOK* z=dqgV!v5-g-|+}9e%0IaR=z(v%TwmHyYFm$=PBfO_x`xE{OI&F_3HJghMNtmhSv*N z+0R4OuGP_o-CA$Xc~^^DtXlBcS=?fDxbpJ#Pa60=hv&5TYJcawbJb7!u(-a?{yVR$ zsNc9g%cmBaSutgw51ns3&7m}b3U>EP|^^IhM(eSTCvAQ?%<6~Uff=>Pw_BC)mG`GR$ z<}|djGc2dZ@$nk$bBUuXZ*K=bnT{{r&FRH*YFs%iU*2J#B2Dq+^Jd=fnblowJxn%R z?iq!D^4N2(rrGPKr%NMn`f`|S@jtD1w%5E^OkZ}U_(FbnKl+Pb-t(Zlnf~TDV>|S+z_0H~$`s(4Ef9J8<-_^TswQw1?{inL~#o>J|yB_hh`yNSWR{n&+ z)7=YK7Bt&ecaEp$`n2C{dHKbw-P^^Rxx22O1!dr;DeNaq?>X zOt2W;KJ)8Uf4>f!+x+Ak&zmpZr@nKVk<+U4Ecm>;A0Jouv;)>_r#9_AFOJ_#v+n^| zI<*Yjo*!(pn&wlclRxP`;d}no@RM=-d|^+yIoj)yKFn3WL2o{;8cusLe&+h;>xKT^ z{rDJ;Tko*#e@y&U|4C=&6aOpxi3{so-lP1Tzy4_sj+4!AzcqgI^1D|ZzZ$a_(>Le+ z2iuvjG?$~1mv^|^e0emA$@d#;Yx=4R`Cy|Sminum<% z@sfN$H!cisF6?*V46Ea_SoPg?`A@y`tgqVlu$^7~?);>Ir!@=Pe(TkG59~}@7yMT_ zEuW_|fAR|7*naEPRO2@9{%~JcXg+zsJ(Jb%y)3@9`SK@xeduZJeVV5+vpMcpJnd?n z!?SwQz+|5z`}w)MonJP$8rWv%%D2#e%4wXRH!ZGyUw8WW8$2$q=UvX{-d{Q%w6Lu`P4=b;> zx=7olhn2%F7w)}NO;i08JD--m>={)H=aF}YX7%`7a& z>*xKlpWCqXV&|UD2}@VV&b~Y@UAvsW9#`&q;=;`@@^(Sue>f7(U?_>Kp5`X8+ z$nnm+{$cff9freR&{x#oS=zGtt^U!Q3HO|7WV_a*+AQ3EFTz~-Ug38}ALr|j8GAdI zXSv^tnz4Il;Fr_K=k2<)@$C$EhA`1t%+TP<&RQKW^4h?yJ|4bW_~m%Q%P#Y{`n{iU z^Ksqtz6Z>C7HN3%q0#dTZ?4|;D!iX6(|- zox6QS8ne8=h+ll5E4z~07goJH;tTuU-Fser^LpdL3cJc-o?o1rHcW(>x%ukxJF70c zzs1(0x+`5?1?=7ZeE(0gW;R#b-D~NbzP?;-{nLK?ntS>@#y7vQ_dv5)yPRp~KGp7J z`TTg;v3#|iw0Lv%)x~O`{P1t}`{O5!CjLKsUFD~Hpu5?bHmjd@-ue1;8|Gnn8D6#n zCqHpJx3wAh?x&59E7Ri2i+c~IzkZ%C{c8A{r^&zH`{V1I%d0EA-x=)Z(5wBvd*{M8 zoCoH-ve|TduJm!P`rW^HT*0$C-tOum-dtH9M^i1Fryk#oJv``d#&716wpuS=kK3oS zf4Qgmbo&gizPVVmQhH2WU&XRw`fwy-&V%FIOuVm?^TA^(U-e=9xy&QGv&C&&B$rC<`pi?uTCw~mSJUn`+7Q)8JhGi z+*3CHUwk9YU#)Y5f70BU&e`0)f3HTr{#(ayf4$nGy7)W9Cm&`cFLn7R@K$U*S*B(z);4?bGeNvp(q- ztM+?MGoNz)_iJF<@bwJ4;cQp8y;@!m^RU~)f0y&_a^~yviCbM7&w*citN*2MdHFbB zchU%+i^cI=kE`Kvb2GGMyMSm)GZdeCpwRd6f4x+dci+E8h2G9%=I7RWH&wb2;zr)7e*@{$HVESFZ(qd0(6Q0>2*p^ZOz$uOhyyFsr7~mkYX`h4a3i;pWnQ3Uj_rq%(`t$9X2} z?cA`Z-^nyv>^;zzeJ*{8zJ1_Zt4{0sCi|ZFeFt7e`@-GJa-Zrwi+S4Jz4sZu&yZf; zxiHTWUrtl}i|_RPqqFn)M=z#%eYyAKJ?jhl?pa-!@gA-hY8v>>@m>$ypUl!U^EBV@ zQ}$kY%R3MA9L&LGGoDTTz0%U{T;A(TQ-;ILZT5cf?+3)Ccjldi(>U+#&H9@3>1Sx( z!g;YIt2Yu0(pY;K;vefQq* z&85flE*IP9)fL9~ResHeIk&n#9q&EY3;xSJ#9xg*y~wW}y9ZacPfjQJl?&PXy`HAC z7v^ch=$Jdv zzkTo9?(y>J@_O?7Cr96G*Xp-^QQv*SX~Ul)P50h^w~c6(4 zxBqD-EuFcioMxZ-)69KNtJ#|W{hcsPS)EodhxI-vVD7Oq+;j56;mzg~rpL?iH`4lA zrd~~$Gr_}d{0hUB)y=E-%&^ru|6=y9-Z^2bF?%_h&9sZ{8N=6S>&vaj^}Tbe-Fxo+ z)xCYvf5O-AYV`IzX_rgi8JuG|SRUzc_iMPFa(ZD#pT-3%ho$8q#}$0SJC}aXnC?^1 z$5-p^DTnFtFX*T9;^F11#igZJJNx44ea~=)ey75hcE29Q_j@v$SNT2c3NVYjJq@3o zje}P^dv$TO_37bdacf%q&HFp0xW1n5S+0Kj-L<$nA6~!Hn%QiIhfK44bTh%gxc9N%^Q?E)Tjv*MW@|HPHdYf(w;t|q&&MMjK1|OCwtkDz#nZ(9D*hjb z+@E8jcscL-{z-l8zqigBU?+_4r=4`)%^nIKQS@@y@>|FKF0Ow2t@*&@@PGds>Gh85 za>g>f{B7iyiMG78p)fm^zu4`~_gd|l;hW=E7q@=#yW5#yv~+s9zJAi?U99Gf{QhGe zdo}SVK7T)R%{&F4<(#?SRjQx(#n$I{&%HT%{$&^6)zDo|U%s09{k>#<>)Fiz=^JL& z*Zerz>J3_Xxm%~nhqkPyrFr^(ceQ3V?>Xtq_xo8GzWHiDtHI0s7NakVco(c}Zn0Zm zjb5gIdM0t{R@>*Xb1%P|@EcG3V%4r@cgOXr_qE`O3o|d%*>QKrUr!n{Y53t_`sK~_ z%+%#H?UKX#e1N^mujq3s&7FO-KmL4w=j@=fpBJ2Oy|d)i6<2IdvvKG3a#~)!zryva zsV>UihwA03yPTsszhck-Mt)#=UiG^NUtaIUt9Sl#&Bd=q9oIe@JDfco%uLzqhWG5Q zHt#O8ukw3@nYVv+%eT*t@ZG;N%wf(0FUP^F_lhsfZ*9i2@j9i!J$c0o_mJMq&9t(8 z&O{#%qkYPD;+n~WFMM;)*B(2{do6DVth{sU9Ui~hc=~Fw`(QWI@Qde#GgIGL)4G3o z&t%@5{42b9`_#?BKgIU;RF})xp;bzrMWEcXiIL zoqx68BfOxm&O>jX95416EWf+L3)uR;nfJc`!?))CdCFWq)%!KseEI6qr@eUE!g-y~ zy<8mM42}7+NY}1nXSR-Wez+W0Y=3LL{GRyhUgwb0SL@5`(V4b?wSVbhJp)=kF#fc7 zy?d{JihOyO*Uygqv)g)@m6!We+qL{+y!!Ve)vK-UjC)QzpUZ7uO?T(?o9`KZH5<3j zyMBMha*pt7XMw|B)K}Og2i{u0xtjO=z4~(1JGcH%Tzp~X-Tn9ocRxONodw@SzXQSl zaCm#C@6XshOJ1wF_4nTBqA*uN0?3Vrqc`*vZ@T<)U(hsamd!@huTAHtk( zc{MNojp4poe9ec!d;QI&fv-n+d*k|d0R)YivYKXd`n!t<-R0L${kuK9=T^SxtVlD_ha7|@a+SiC(QdV&Ux1J z&R)G4c(dwet7*#m>b=tT`;h%JY5YbSn3~>Be0`d(jqSHivppZPaf@vqZeM!;9!=jz zfvrt^onVf6uIQyRN>+Vdy&kxUg{i=(&zTVS@ZNSs<#_RXY zy>IVX^C);#HzW9%3s;-f?;O9d>azXy&G=eLTfTt{Z-(Y+*VF9k;?1i+^>Fhtd~@9P z>mA56=ik%M&i+ zuQ$(h%jxm?+WA&~{@AQ@l=E1hCiT_g>8x>!JC~WV?@86Ww_fo0bPwyRHj~GG@2{t+ z=2`Cd4@LL4pW|_1=IX1>zRJIZ{X3V)%oD#HLpx=-%%|S#LMuD38Q+6;rka;?z|zM( z`I||*VK=_a7neRyc>Qvp>ec<1HQ$0>4(1&OU+mlHA0_jN`IY%GxjOuQ&)#`+o7dwB zHy=kUUw!-IoMZW|Y07GR{RXd#)0n?}Jxt!2C;sZ_%YM#Y&DQ(%CHu*TXZD(^hY9e&=B2Cw#GcjyU|My#7yhb5CcyyY8%g@at3G*B0KZ-#XrN z=CygX;oCQB$KtCC$Eo${Za6R8jQ(c&bbc=G+4Z}nyGJkQrIr_~-bmBF^3&@RPV*|i z2fV!B9}BT8KT3O}S@Y?4R^NUe&LZb$9@boS5m(JC zOqh$~A4gBOzG2M?H81$){5R5?gTvBR@A+YAHpkT$^1FL~{1wK>Ij_f^ z)yq4#JJodi9^Sh7z3-iC*1X!jdUH5h9%bBO@NypJR@)x2b=daKuoyq{GCb{5z5C6S z!}+(*PI}zxw$5WCZe!=+R}&t#xE{twhMT=PJ}uqNS6^Rr|2y{ue*55`4e?q|9p?($ znnrdnGQ62jXGjzOyP%O*4}01X=R9XqDzE<2 zIj{eIec72OFSE<-=U03I)4$R8$7b;%cJh9!e&Dp}((L!3tF7m!Zaxgo4_|%?bGW@1 zUh8|YPkZ>d-!iOSTd!xFeT&nFRnt_v2b#@vFWFb{T#pDdZ`J!_zP>NR&Fjk-)5|n^ zd3SET-r=}=7QST~-eE;}*qvRSx~zVcU%^-ZDcghRvzb4Pwv1a0zSz#Hy$Ao)h- zsQKBmc=hh>{`GXS`6oXZ?dE;<*JE|nc=-BWj^b73ZCl4C@ z?L9wvbz9q;P91M9OdVdoS6E?IexrZ=h`;%-%71pG9rkeh)PKV79G%>MYe~Rlc=7G|x z{VT6ed^1mTaJ%d8@7H)f&BqB|^=dQxX>Y#zU-5Z$?dL+h*Y(cU|89Sp&ak@bi=}&t zbe+e%oHp*CCCtmb^qZ?)bTXa3yxMB_yqaqFv-+#4=ds+UnegJ~H~RC(Cy(FF-5Gj2 zZ*SwT=FHMQ^~>Fw_sv)L)PFkfea+~8w_|m_2DKON4C@2?6P>x$cGf-b?yB}aYi8@G zKJ12_*B721_vsvI;_NqDhVAUX+Q2LJ>*gDIe>CUomHjl^uWx*uzpHHCoU?4d*!t3h z?Zp$;{9^UKuC5<_zUlQ(`*$vVkq$0@5#R793}42}i_zh3#>@EaVPzM-jIX}W%=3%e z@SI?t!IM6J&(d62uSNLX={)jF@>B5Y-!CZA*^B!kzHwWpU0=1FR$bmYj{oK8@M&Rk z+|JSBcLo<$(3tPpslNoT4`$$HIvUSOpGMHv!@XWKGJds}qml9Q6CUQG`&Hh1UR@sE zU+_QmP4_9*bN$p$vx{wQ$9+xoPuga}yWBfHuDn>i;3@O?#KSk64Xf_rbNAAEAEu*y zm0xu;bo}CqdY&}f!@k7d^!^z)tW4+cA>!crPkGOr##}j0U;XsG6aS*WM_AY1f23XR zGi>L3mIdt2*XO|}o%&UN&4szbiZuS70PpZy-?`P$%D8&|o)4crn|lf4A=ByeF8I8= zA0NYeFPMu7tLKm7@vkL%Mg0x4UKcZ0Tkrkuet6kkZ@$_&WPG_u%gbDuetoO$f0ciU z=8Buc{*~9o%ir&#i?4Rk^?EwnKmU!_r>*v$ZKl4cj^DG!h10*vFHLxR3j5#P51;y{ z*PHHUKJw-3m)rVk-B+68YU73bzR$rdTz}`_cF5_}^6urc`Ds_!(|lMVzq|MISN|^G z+4L8y@7lA|$jynxwsvpn!mnT0#nTqJ>Yb}E7E3EUyE08VP5-^9qTctYKF4~mn_DmQ zFh2EtF8I8$OP>ywwhN~(+xaTLf4+r_t;2TK=VBg(vsUv~^GSQpk;bfNVorA7diPe} zIqp8Zed+9Izi7^R!s@S=IefX;xi|9u*m=zCxvTAlx2r#I*Z1D2uP(}+KTPy|VBs!) za+*(HyYq_Q*g0n?JMU`43%lhv`s2L+5#W{E*N))zB?U(f$?d~QzX{cAqWteCdkc(Y%`w-4RF&%vwRnYb<Y-f62acVxxFSa$$u<9H5p83Y^sy0i%y}8x#3+viGJ-qtHzE|Vw z(--FK&lmS4m_9t7cKM5Kzx$e5uG+o8+oMju0XKt#>&y2(U{AO?zWmD0Qw5BDvBR{bAcNKiz-4Bg^ zvl&=FZx-ueFYOcep8EL=F!TB{k35&RgLXM{IQe?O%YKisSoQkuPPOxu`B(3I>H6Hh zIeu}1w|+Ho8+I>Ny)&1){q+w9`?&8#PFI=nrf&Pv`{*22n ztkAoE&n{DJ%zYn1c@80$tVZ~~; zezTuHdLCvrSL<_VuPfX+;x=|B{PwH;U2WFB+nZnfV)c90w5wOAyS3hZrxBiAeL5E& zzIi#gIlJKH{@-_EdGFiB=wzG>m*H~%PExgAt-o`ho++JdexFwyoGw2*!}zam>-!9L z-fTYU>brd2zsjG!AJyO3T~GC5&F;>)<+rAzSx#L}NAoJb*Wq36E3V*My>s<9>QjynR?il@bw|C2yb2=CU$OXd%I7cPx04dXYzC& znNM87qg>GOsc*(bPYdsL@b6dhi1TwnSYgk*d-sc(rCknYR;KYD4F9yxOu4zP&MwT= zI}87E_p?DS$5-!b)LhSEBW!)+>YGhl=J_hWbJgLr^@Zo`UY6%)-+SW6M`sPOvl$-F z!pCI13+6R2126M4Q!XwJ!{_yHe$(aOIg0o_le6=STi$*0&7=3M-_4lINA3UD^KZSd z=J&qT>sS4Bz2L7{E}(wT4U_jQ)$Rc=^Id%y-t0H%`(AVR9QJsIo8yGK`80*S@9xJv zI-8xVg~_xudmg?wH&fWp>-KNe`y+0#>hQgv^{VdQ_bNMQb>Z{SpNpR~wB=j7hcC0? z?J3jvT4V=&U#s=u5yr>cspR>c65IAtNdQOe>qK&R}mhTCy%fK_wIgvmd;Xk9y`ML<9Wz?R(trEFFS9V z_<}C3GjAVn-fVojBF;WK_np2gz3(}%^1EJLE}Adw(U-&crxCREtMT`!-R2vNLrC;T+EOpME3YbTSXGL-^I&zpt^^B5k#O=F7sozFo}~aeV8y zcD6fjXL`X;|Es)yPjibo&l6@xn&sg)_w`|Qe$PX<=UU86x$Eli&CpbLuGPjpz0T(4 zW<^*YUH|nr&}+?`ro0}W-M6v7q}BI#q5JO^^fN@iYsT@DU(`=K>IIE+EzS>ChHdU= z`{sEq*Nj;H>Gj9cZ(eTeG`qVVUsmt^Exvlsp4P0KR)qb>uj}bP(%8Y1E^nX9IOn(b zzscYg^|<`-xXsQ;Z+2(W+gWYK*}@9H|9N+RJ%b(Q>GWlF+}6#u@1Bb$9QWiC7Qgye z`x91Q(3pX>e}C@S?*(ZJGrd+_&Ln$|>FjxxUp0(YjxXxH2dbC1H=RE-SBw76PO&@n zF!@XP@FCuXu6GXeG|s9wbF+E)`oPNR;{NgL>N$4R@4VR;{Fi&Zn7L-m$+Z1_fgoq1ZlY(IW;nkPQoh5w|@ zFYbo<@I3g?>C<^V;y(SIwK#kFbB+1%4VvALUu^66oqOtel-o^z@%rxPpS-TsgcXau zkslw^_*&Htd)({J@-vOwY%XmVu9^PXd9hf1y4@XCI7@XiUHs{B_0C5VSM0v6)A3zh z*gp+;1%AJ_znS+(e!J65=lSoxi9L(B-K$R4=k>q*y3DU6iaho4Ne2?vwxDlJj5BX7zV>9PH+}4IVK1`f9x#erLk) zX7u}-;L;bj=UY}k>FykjIlIHf6NZzYG-3O-Z#7%z@zlfQyDNOTt@qj8Ilb(8$$Zuy zc4y#aKbP)3f6~LtJlbC^Urvvwb?$f(w^w-2BuwDztNFlhzBRy3OkKt9EXgPNvs;E_%Ei&I?vvef9R~yWqHX zJ$b$0zs!df&EA>y{t}%J_JZ&H6Yh+0=|9b^jt}hVT(Gos)t_)0`RN?Evikbnx#in$ zwoEI>KgIQU(tN_~!#j7qS`K?U%kpW8i`93T*FS0R{g_Gn6!^PywQ=|FTz=9&;bAVm zPdeP6@aIG4PDisd>Z{v6jj#39IA>gbv2@$3Wqds>uN%0hytDk8ul8QM-w*ku z+q(Wn{NB4hfBX75R>Zq#E~kIWJj)`#8=Z;2Go_)$n}hpjJ^o$}Uba8I8JVWM{Puh9 z#n)psi_^=V>wW(5zw(F|&MCv?yFdJ5cNXsq<*snptNf02{yW3d?9S2VQQRIDzI%Db z7t6nqA6UIv?-hMqv(C-O*Oqwajr)_JlbccVf`u;@F5*62H037`b9D9lc~CEQ|Gxj? z`J~wyI809WD~~+wd(z?Pc8?u#g`syTYPDMbiQa0m8=B zP4gh0-}w7~74QE!_jhyTs(o%I3eSjj=bU4XIo4cj?{o8JR#w#&KJs(#@ZG|6^@T0IckmE7bZAy9Zf*b8sd_ele?V_T^SDx5`()d=2l^z=1o{iOWxa>e9j6 zdvy*q+`QT2-|^yv@3YWiIZ$`3#od`SIfTb^feZ z%gjVGOnmxkteTZZCKo@zw=cBzZuITOJ~|Jfp?w0v-sYz z_?f-^-4j0#RI?XOm_FwXGsmkL!s9G&<1}!J*X*}gas6uI)l|dXbNBtDr7uqWvnPBl zeyg*;+AIEQ(R_u)Ut2yrtofPeQjTw)JPbeogSTXR)t4{kot1vLn5V<*m7l))ayIVs zM0as1GI$z?xI``r<7Bg#@IxK&O zZJpK~FYFHA{^CBb{<}dw%i-re-D$P|&W^J_oecKOp1Iy?R($^q6Zbsl_Nnpq;nP?? z&Gr3$JigzTZ|}zSl)IaM`IggH&oliv?xT9OKe|@Wpu6AiMOXUYW%k?V#mupF@$ycr zzSDeWS93Ks-hO=b(^p@<`ni9&T6f68>;cP%o%g?WTFa@v`^e3x=j5!ecsU=%_}(-5 z>Fo_KcWyOjk)OU^nE(FmNBhRL>SZ*_H-TcJ6)h=!N@?;N0oG!opW;HN?#m=WFebdt&C{X_v?C zOnpy3>lgHHH1#Ny-~IaKPHEP^y>M6_M*AaA@8r;^-rsdvQ{%VxHL*Uv9`~~STVGsU z9VdTkY+cWK_0WnI@pzwwI57M($GOAx(EIf_pjq(qa+tn6EWfcH z9JTVyd%Ev%Sbci?Y~Wqt!|2@kFunOK3|F4d&e{|G@>hEH&;c(O$8iQ&-in*WQ7{l_Mjm*L%#7UHt-q_m^7bfRTukfC z;k^F5JFm8VVt#vZovpms`=BRp#o1V%&#IYadBxd@;pcab<@R37-@ZEaEbk^JBEY6*tH$GPU5}c4_4V`nXa6ws=|6M$E05o7 zeSbOsTuI+KSGd_}#KBWLb9#DKCvJaMy0*OVIE!t(eXnAC9)%UX&T{Rkfc%1lmdj6g8pVV*}-DmdcYa04@FN}SM&$>Qbet8?K@19|+ zQ!QJp&w-qt{6W6H=-V7e&&k16KXd)vAr2q6m?j^d;pMwSz8Y>A3oBRi`CLxiazEFV zvy(^X#9#5lad{eQa2nO`-aq{Hdj6^YZqN3GpMLi|YxSLHT7EG*x$rbk-D-5J74y~b zi`SL|$E8sYJ2~?F$ysb`r{309PrRRhXQX{<={f!OrRUEccJI}K_dTrmZiaBW?&^Xb zrhmOSLh-Ig)7uw3bv5~qhu2*8R>#lx+1MGw%J0sjw)-!y`qs)b*faa?*PYKSvEco8 z6|>bTwjRa#0$=`=-$Ql&y>JDq(HyPLy7-m;lgwSw(0uN{`F#wR&d15l{E6Ow^?Sj+i&wMUD||l7 za#m;OUCygn;r#X#?;XUESJQKPHD@hfJNf0X#2V^@^e<4ymJ*>^__P1==+FQt-fYmzPx%??fkQk zbCx(6{4TF%pZ3W3Y8ejRVrqHc|Ksh?7S-_TePwYne^%-5;f0&2Iz4>ns>Z5?*LU6% zPM(ICc6q&>lp6y|yB zb;EVs`6_U9=adi>(MXL!axv(LZo%y_GJ_T!inH-BQ? zwdb8qb$l^jz1rSOHA_9tT6I>h9FLDvt-R%*Ib7kx@Vp=9;PLq08$C4hW+*;qHyd1k zn)S)^=ZeE#%~YKR?s@-kccI6AW~2kh(|^u@w|!}y=c04+>8i`=$@%kharTv;_q;RH z%{y~FO#P!;Ts(d2)$TO#aMo`euN?NO4}RzMHiy`Af!RZy4!nG88*gpjw{ZG6^xO%) zdGc1jmR2uk_3MS@VR`(x^l|XafBJYZ+^{TMt-ShT<$YhU{}n#08F1gA_qkTDIG&mP zED^?N$+zZv5}i0ZU;j)setz+=a<*~UnV%S~{Hf*pEKc?AC!gV~=NU|%SLb2!?uVWG zgx~EO&olG;?)0|5yyd@7?ipRs1J|F&t(M*G`K)|8j~ur=9Y)hWaNfDMrBHyY z`g>kqdN#P}(kaf$Gya)<{`Kss+UG9a)ebMu>S?wgUJI7*pIz>+tylUm`_~gsPepMY zH8bhs<7e{t;z!}_OZ$XdYOna!?{q%G?zHgp&9J5xw&#?;;==8>I>jxY9q)=Be(wXu zuU;6<6UWENd)6Ou^)-Ly>~i|yzI`}M0@So-0wW`onZsE; z9jU)X?+V|)f3y!f_074uxI5F%T%8VF&eO>68OqIAZ#6x6dRO@1sWnSnGx6I~jDLlX zc=d`i9DM#+{qz+NviTXy^Y*E)d~2}$&IUj4N-uss3wH8AtuRY5%c|GDs;qA*};<)wa zGdK@B^Kd=I*_m%o^)rY0ez^7GU2!v*_yK<5%jz!m6~3JR-gvb=ue8j6`j7Tmt@Do0 zzVlr;>-DdmI?P<*Fq~J|)=y3vX1>#l-}A>Q&ra@{KUv@HgU3Jbz1k}seP83#ICWrm zc$(+=cAr;0ICsxX&*}B>>gC>zvxZ}z^7Gd~J?r&_`3x6_Tji%!v)9(PM||h^*W+~d z@0R1!Ezdsk)zVojhn<@7=oRn1E?&*OEWZ3W?W3MQ^Dy=F#8>!;HxFa*yg91D#m>68 z&yHF0S?>k@1DG%MTVPx|XTAKcwmkJT!#fKspM|qCE_a?d{eJX(2I1Q$4_jPp-*~Lr z9bVnM|K^Hm;%|>yJy~;BH?QZYH=ptLS)HxXJF%zA^J^USkm4U*%baR?`|aPIzu<@A z%U}6)TFw~H9>tk`!rj56aD6#@;dlGTrOn3|Zx2j;X9&y8ynQDxomMzL?UQ>^-nnAE z<+~4EI8A>0#qHl(9dqg9>*=mH4?BD1d2xHxC-3_P9oPlE-^cEQ$H$|kSFKzeZw>Cu z<<|0waZg;Xp8uLX_2tbB+p~t9Gts}pCv$_p9URR{VeRx;OoU7 z@wXPgz4e`b|M{KT_7^X{aqmVJ28TTgH(!>!cd|2X8{M!tJ{xqljzg=OJ@Wm#sy!p` z(ALtGuV$DtoqOWNX{Eb=)^Mfo_fot@Yqis$xpml{otBJxn6E4SbuX4JJo9pD1o7KBee^!lcUznb=zPLS4j(q*@75-7a zv*7XDyV&lvoShe+U!MN0!T4(G;=K1o?YtK=YQ+%Lu3BW};^BVF8h{r}_Tdc!mMo&%=G_q+K1y8?Q9AAef%!e~@}74P2h z()3xV=Uj0ZO&-4fwt*%#^Z)8pvtKd8$&0J`-cxMPGhY6w-E3K$YWw@#-D~6c_R<%Y(GOqSynB-8 zTlDm(%k#9CYyRqfFXhkcs&$@xYc;p;D!uQ&oc57#96zh3{s0DB91q6V<1GBvicef# z@198?3=huNx3)j4EvNqS{b!-uC)Sgzg#`}tzYT8;RKj_F){z%_!f9y;L8)YcQKE<-h5%^D5g=& zV0qt5i@#=_+UNW`V|sGhcQcqh|4I)h9ag-2d!4@q;j7Ed2VcJ&v%9Oa2FIg=mk-P0 zFc|*U;?To)_a~2z9{iK1Up;SjSe(7T@;JK(Zu&dB{qk1ayWa4unqOn}KMHR(I=heh z@^=qCcRW4#oo}&ld+E=w=GMczpN!@+`}9@aJ}TvR&m?@c&c5wGYx?eV_3?W5`PNS( z-p+!br(X=O&wVa_!Qau;vwr*QVd1N1kLqWwoKE%naLS7_`}S)EEW9|ZJF%z!)oO-# zy}Nq0&y4oDXrCGFK-}V;Xc{gv!8AK)r#BOyK1KVnjXHI`{Bc?uQ?aa z){E_1eD0Z!{_P9T7CU)cd$K?Oy8E75m^9_<>GNAF4nJ%3!kra|uQz*@FHQr$XYAiY zkB58b%~CH5JGoTRnIoc7MQ^etA;{vLqt1;6+CJ7-~O?0Le{pjq9jc4wZcn)%MX z&-&)Y`1%*ySRH?BX4zVG7T$R>I?ktdhd(<$_2`H9-wR;+^K9e2+wk6<<}OwfU(mzM zh!-f1>m3Pe;aAJ@V$T~^POJFrEzjc9*AuQ@oXPno`uu87v#6ap++5Y$6Hgv*F*&T9 zEhbJsli&HU#l?@tscn9qy?pzMVT;pv<@dAQGq3kt+uNPJGSB+1e`i_^+%(0VW3|*^ z<;D2!JUoLxvrqSO-p|evx4I?wzAQKWk4)Wuaf?}Xd7oAMt#7Q(SL^R-t*Pb3>Sqqm zpL{dx$;0B`{rrI&&w}Nx?$C3)!iUk`KmV@azV$xYpR~&Dkp|P}orq(PIC{>woNX`c zOMUA7bAW7TDpxOF3qNaTzWlp5zWw&Ban;mM?QpBlzka;ar+HdE?p*K^Sx*4h2+@&>*CEJDY9 zHJ4xW;=Z7&2cz*3oFD#DUFt}KLl|Fl??%w%0 zSv_fH_IPHWW-}wNZokEg!&$jH{#lQ^efTh5zq-EiUEvwtopZhjM`-WFuZz~So>95km78mc{8>`pY&v6+n-+OFd z@ATG}FIF$l~tLek3U%b8H&8C*Osx4l=y49R3yyvPnk9&ph7PdG( z0=@0u`S7Z4VRa`pVbzSiPl=g;5u)9`n6>S1U9;>G8TcfIuaQ{&VrPY0iW z_3NJc-K>jkY|s4pcQdwDT;D6ZtHI9w(%4#aI*U!7Nr z>(89d+G@wUo28y}#p`_g=N(~ddgz|_p>*!Xwe7_{eVhzleCnw8=fXZ; zIBIzqpJuC^6?ay(aP@HafWw<_x%IBmBD&X`LhpaJ$~37UtR4yhtFL7eLur5 zFWv>quZ|wv>e=FbzcxcP-#gQx6}~;?YxMcmTb_4=*4EB=HNM3E+WJR4`{9&}n@xFdcUZ+>UM3~!lX^Wtph$5rFwiE+ZR@?Eezv;V6cp3aZolH!aPr)57_KC@5W z+W!5_F8*kK>w(+D`()p^#eR<4`f~J#eeYUyukhXOu!?t|k8u2+bFsCx;8%W3NQ z?DS#I)ma(d9|A593##YjtLM!^yPm8Xo;?4NdGGbn+bO2 ztj_e|!N10zJ#wCgoQLoHu>8q`Eq)ifqiX7W8f)|m{p0B2X@xr z#M`p_IM?=XynSNzyttY?Z(msTZ29(vzj`(_wpXn75P$m;c5374JL_kE`FV!bpmWz3 zwsn2W-TfEWd-mUP!cHAC_#@W2@4~w7uTdS`e=8WN)UiC2Xda<7R_KekO(~O&`i67wcW%WPe_0Qnw z^YNRd-1j=QQ#UTF=GUp^73+_ymbb#Qc8T&(mBCUhQrzckAK1hdJY%c--){yZpqKFLsx>?In&= zY?Z$!@I9}``PAs}`m4k4xXV*tuNSWz7EcV9SF1-~ zc_xo1c9zwE_1P_!>&erorni6Qeg3`vYNz2Ze#f()8ozyFwY>S%cpP~?Z1FQ+?it5* zH`VBTq=9p4ZNJ*xITqV}#Pr=APjfx)>2dTrqdhhk>&fGZ^R$XHd;0Z(4*bb_eueRI z(<_HPncw|-a+bewoix)wYu{D=99e$x=ASdaoPABdSyS7)wJ(3S+ebD0J5HE=#hcf= zc)7Y-^;TAG>kshyqAt%brU#ba82|hk5vP6}*cf-?s3-j zonGJSZ%hYIe;n^HP8c36zL+kK^M`M*9OmU_N=EBpH-7P174gzcq7JR8^hL{4No4Yy%sMHGlN>*s=s*sl|Bq# z4SsrBbgH-K_NguQN@sC1oIagbc5?B;-39C;PY1`I;g(t+#;cWo$!N#J&$};}{7ZWA zd*?c@-%r(RA9@$`+1%wU&feGdigEKa?URS`Vf#7kbDXW-a>ISr>8WoYjDEGnaKm=p zX_|=!9r>D1}zPwrF?($9(M~_+SG4t+&AC~Pb z{df1ar+R#v_6W1o&g7|Rn2~KGr{Yq#_+12cpPF8olkkt1niJ?_PGF z>gBCwK5O>8N)J=_{L69j{_auaQ?I&v;kUQgpN*y8Qg5FYeRy7cYU(*>yjmXS=Ycfq z{mAUG``z)k-oDwFde!lHz0OtcEP7sfy{okto&4R|Pu+THRTJx#SF`Fdd-ZU+zBp>t z89h0VbNcG^#pQH%550JKhA-ba>(k3GAI8(jhn@K5bj+z2=dK?oU!4YA509t5x!$;o zzoid-KQqAUIp^J8Go70H;%+})Sf+M91Nf8m{JMJ;->v0;DK^V1o*D7Z{k9JWR_=2U zR!%SPe0XYcvv57)e8xYsPh9)a+MiwaepiE*nYKoso?OpeUai%B^~taBVViHfdn{g4 zd6w_zoxJnt%Zsn@vwg$s!J!u~E4Rv*U%s`?@znF`)fiqmj#|&Cv+U1G_{)Xq^J{ZC zUcQxHOMZp#oMKje=UlwFns-Q^-#-0|#bM?skCXAm^;_kPE)VC!?ODA0;NDiLP>!W=^E0;B-W6=sn=h+B%b(bz@Zt;Zi}EepO#6NNB+)d}S$~z| z^YJ!Lck@>|;kfZzW~#P!>dE=#{!`+w>Bq_MYM-sy_r%IOm$TyK=`B`_52ryJPV)}0 zmhsCemWwxkG!CcSUgG@O8+P|upsgm4pYGynJNMl_>S<(g?yyJktxhM*KI*4$rfhlJ zuNG$Ct?T3C$HNipt*%!5k>Syn*HcWJ241=SP7W?lQ}6bPar4U;hvU*&zq}Ja`~1I; z_SdWQv(n`>vIZ~t=76jpCIt1j=?v~0Og_rV z$#vd%#W=j#^wE`{zWVmiNuSY>$LZc^snhX3#K$RS{@a53;BjI47F*uN_1L?)+Q0nn zXB51v{NAH~-xc?qub%PE6ux&5wliPp`?D#ghbO1M+QoRM=Kk!OZaF?5p2=yo7rh7J zEnf{BJtx1utvLrwy}etE4s5+RE*?F#a(gYWy62#t7pv7ntJpqaxZ#ogX772nMmmjY+-YnN+&=trV9qYT z^ZN1|Z!diNVR+3Y#=|KuuAcANTUYp*>A`nz@zrWL@1dGiozYQ~SHIKHSIyq$=I=E2 z?Dm4;(+pQ@zw>9*{=UpwXV2;}vpvE$F3)hc7FN#8z{A%s-yASLe(~mE_T$~>9rsml zI*H@wBq^w-S&#(xwGQ^y#pS8&6a>MUx=A?7>y~@4k_0zD2ec&|U<+x`o&$In?3AUaVUv2kVuDdkbs~-KQrv?!Ff+C#_!@!E^s9J;J+roIjtoyaq8J)}IBi zQ}4{s_C%!$!r>4D@x7fz7ICRxN^5_0>_gU-xw}SE8>z~0X zZ|#iZgfTG>U%wjg<$GTi+wa+n)8f+;pFGd6?-S=cwLR|4YkK(v-imfYr*ybB=y?FOi`;uQhaXvHiGy8mI7>}Db2P_VZw!Zao)V)h) zO6!$2j$S_Pjs4kKcvfv^EVrs>`tLj}4RQX}`ieSu7kf|YiGv3}HRENt&+NN<>sxQXFn`WX>rO|nTHf5w zp>}f1Pb@Bs-~Ke|I)8Q8_TYxW*XxI+!&XnMw*IbH{jRo{_W_o-+7nJMA7&nP9=`mK zVtqVr`H89JKdLS7qnL)7`Hy<4>091n8!zWo@5wpy^qOsT#r)UIe)q0zAFVu`?>))3 z_C%gv7pHU9Hy7ic{;ZeZal+twI_DjJ^6+^*%MdWHq zdVn`y&feYsq*uegwSr%&cDJ=eEHSVJTX0c7jdlWnfrwE)rvFzE>^RT{LwgHUg4{G za{k@)Zy4|7s=une;@7wLckZ;>!{fsw1`{a2%{;a*)3m#vb zhM3NsE{>X9&w4d}YsEB9eEF|9;UDp~MxPeHzlVgKT=_?SdpFht+M%f(USad`Nbe0=|(jJV}A_;7aq`g_O0eWkT$ zc-8+3r=5?pT4zm8ix1zvjjJuM{3APiKjNyL`f1$h;8iQO?sRZZ4sIUj4y$(Vvs$lW zTIxLfm5=wz)c9At@Z}VHKjF?q1OBBP@AUfkyXRI-AKv->X6yZ$QXNOn`uJ9O@!j5Z z*Y6#`(_p6#4v)9K+DG%^##^1u&wcge&z`N_#mhhIYNvNEaDC?Ld~Ds(p2FKIl8H@wxq+XHrbdZ)(=-&h{D_jhvQofgO18m%xo zw0U@U)o1?ve*&uK#aH;S&F}74{oD(lh-Y-hIQ8m}a~j_M{i)^L_3Po6597C1e22sG zmOeQSoDc7Nfcoi|r?%L}XT4fjt9$M)vi#kikK*-34?X;k>ecXzzw~`=`SuHYWy|@J zc3fvouekG{eK>jX6@Dk)y{pT4z2$4=YA60tzO}n}_qo%*!&b-K{ML%=-#tF!*=zY} zY`*bXJNK%-X75)T_Sb{QFAl3`-~Tvj;+<>b>YJZ^)!E6z&0Dyq$4w){m&4@z_QCQk zHCpntPV?`8h2MQ{>ZMOFF3rWxz2jxec_OdB+ABu$Y+k(i>W24g!}8`)y?BBJ;wa+=n_N(#r zs^@9YeC74Ls(%UJ{xdFztrw?VY}UoAo#r{i@mHgo_y6RT?_T=x_&932XBd3_K0`RI z{PvwW{yEpquv%xleXC73kK-(~&BfE+dYFFo{HeeD;GMeq^x~cR+qdz_U!9HV@%BA) zxS##cy=b~O`}6AQ(uU<>@%=j^{kzUM;aNPGzV+e<`R0rL@$#)~YhkZ`{n|U&xgR3) zg)U#I`zgP>r{#4Ar*`$QqbzX8k`B ztJyQ`A<>Br@ zUB5h^(LepO`uuv<> zprdZ@IK}Y0|4&97{l!^)J^9tUtDpQhtX}Uz*zP5+zj$@b9d4=L&9hop%ez+GvsOLZ z=aM$tSZ^MlH$%AE)=w?x3CF`(uf}gptY+WEtEuz5cij3Ij+}>|UVYCWXEo!5S>@{I z%+<2x7Jtd;ooB0W`|n~{o{m1X@;i%fZN0wjJ@e(BF>bo2MqF6_oVWTKF0A~-)YiY^ zn28Qw@Aee$uSM$S#pSEBJFFV~)Yw`a@1kGps_QX-*c$DyV!4>Fern)WD^}m0<-}vJ z@aks9&EN4)o*b7qS33QjgmAU6{k~kjH8HPFb8)rB_H*g^#4{_eH_z-L&!ul`<4TzeE}bkcCXN8zm|wnuq!eW#XMULB{NOkNB-wejxo?JH*X ztLHpN9DCuv!qn->^{+3s;<@+s-=BG^rMKL$H8ZdNiQ(gvi}SzvM%Z`y4f@Q*-Sg2v zGHX7=FII!|I6E_J^~HHBjcoTjYjT`t$LHsV(W)miQ!#AM5uTkqT;AS#<$C+)KmPl= zeUIwbX@6&+JpHuvm$%CMeUADSK4P`<`dTnE!F%Rv=EAv3-}yDj@2kD1xb=STO{>|> zdWG+Iw{xnWdxy8|)A?C@sl#6Rcv;?zMeUIyBqm>Jz-|!mq*hK z^5vZI-7I$;7;X70zcX(f|8B13c)rVZ=k_xP55MzwoW(ZQzu3m=`LD%K-kGP>o$S5f zF?@`x!=W#t)1bO_I~b8j+~x6zkAAGVT=3o8d}Y`b1pZ|);8Af4&?dO z5cA9Xr^NdC-RBOYy)`|Z1Ad+r zAMPEw<6q_Xy{lW{ceWbN7Oy?nYV1ta)mBHJ`sTaG;^OMBOzqWP_4#`__Hlo!`O&+T zw%+Y2c2>A~cuwjw_T%Z7=eCPCGF=eEi~bPCDVs4aYt4YCC)R z*O)!xrn$DY@ZEcPV*Sg5nfFz`&%rtJzVGQbQ-1ephWP5`_7ZQdAHRK0EWIq8r6H%6 z$BjovoWC>u{(L=77`)tG>*?e1@w3gFOP=5FbKkr2Q|p2r7QSbCgWp|RW}^RIyn@B< zV*Ap_Zy-zUy{q|KjNi_@}CIm|iB`@0#L-s0+6^-SM0 z`}9@)6Y_bKCapee@%y!3-`0zJPn_92dGYpL@WZwqAkW9&9yL6BmE#rn@BguSGQJo$ zzxc-SPTTs%cwfSNzjCL$|MnJdzWR$7(~48>Y|GmkoUg`w-<0FzTmMGkg~#9WE9?%} zch<^x`Fz3j7uXLrEsx%2hgzj{5bV)zw4^1Qi<8LXa6|K{QJ z?&hkFUwoQcz3I@3XaC}Oam*05-0FUvUOXO)W5127!<}LGzWaYwF2A@wzebe%cQf$v z)~oq0%ER#V#@A!6YVHF6+5P;AU#3o{_p080hI*xUl^$jve1GSp;;Fk@)tHz!w|aSI zKD;OT{2JEMTZ|v0yU+0QzMr35ytMq8(Dtb>cE-+%YZiNkf5}db=AonJymFfD6P7jG z+L^1-NYk&yG~(1(Ep6w$!Uv|;zU6xD$Jev=U*3{e^~(19h_jks;?dilyM3HN?c~et zck#&Pk(?ZJm-n=k)aeSXDpH+Vem=Ea-GIdgq?9@n0)`h9lrX_+}( z&CeXGrN+O~qnpQBovrVlba6Ld?8NE8c{so4gyH5bwbQHT_pa&2FUBh`&iFp3em?P; zeGoqQvij+qd&rZpH&1r+;x+mDJ)d6t;F(LFrkLM-{(ECu_M-zYZ{F(Ow`wi<6~6x- zs+i%GFV|V`G~=GyVOjXjDdv0j#qs=^i<;g%t9G7EE$^T6)l=*Y<@)UBy}L?}{dxQC z9M!UFEgE52IsOAUzR(OlV|r)Jyw$Dt=ySr;>i5IqY8#vD#4pspoL<<*I6Hgso+-{*-&t0#xZ3(+dH0a%mse+M zdH1HS7Pokpne&Ush2d0Bv*(nR-+HYdd#9TtTh8Y2)hyoUS)SS7yLA4uuvce(c7OVMr$^ts zpV-g4ET_-om`nbv!>#6Q#r5|8>(oGORFmS3InEPi(SF!yLr?@@lW z>a~A;<^Sng=2Q!}oS9~LhP$|)C*!H1o=?l_+{O87_0r@o=$(J=Yp=L1^?KBJXIP#Z zjITdae`cRPn{wyC%hQ1QS;1$ZI!kF^9#r*Rc8u`xf zzr7JUYZ%>q{$aHA*H-)|Z$7?%*Xar$+O*D|@UX>C?$*+~%W3G1m%*Rer{24L?{w*H zjpllL@%ngNb@TA*`AlE&SqnR#0nYOAs-;Ie{A+Ca8`taayY{nqR((0X7e0z_Urvsi zKHonUS92e7oI8JV&RkFU*5rBnxi>r<8tZYwXx#Df)HYu|F&p1H9wUb|s9v+QX zcfECY0!w3Kd6;?qT<3o5fA--C?mujK<_XW_`kv_Xs~Pg{z$|Jp zH>T&m#a`{S6Vp$-+09%pJl^8v?Z;o?XXgob55-KJ7ptBARsEx0J^ZP;x%qKVANPDm z;^3`TvHQV!#fy8^{NAA28XnE96{~NIoA*xOnbC7S^VO+_7yimtTO7BzWsj{@XZFAk zU+%_gJ6rY6X`eV^J}f);pdC-Vyc646@oD(^Q!_4HoX^7bK7h9`C$_n>ZEu*nSng^T z$HOh}vzqD6`?+p)itDpyJ}&$UKa0y*oQxN54$H5#M;6yQ^XAH&4UeyO#;`n#O zzFHdW0cZsu-y{AwGQe~sB={n__5x;S~Wr2}_P_qw&>EUtd>qww~Hmb;eY z=vn+OzQfXh=U>J4T0ZRV-1`}(=?==zvz&XM+Isf;0FK@}%{V*X_NZ02!szj*u6+CT z!SZYMWNET9wqNzFmHQr9{3X+S>Nz8PdCtV|zYlxI55rY&uAXtrf2XRNXSV;%vkQK= zI9Ynx>OGN9m_2dT^{rQ<-`|_hl5o4;llkq-QR2QXI`G$fA|^0wr4q~{_?&Q-_5c;yU*gq)$Z&Sj{8yl+|St- z7q1S?dE|NTFaG*7*59nv)XQHP&dwSauJ6oo!u2jUy!TPPT5&7eSJ0c`JQ6G3wrO-Pa|$UxbBulz18DetD&A}wB&a_t?U&S&YQQMS8-f-67QAS zyZWBJel`2%Ps2I!^3~Wn9Ud2--=0_Tx&La?)q7Wi<@fj4<=!QG@OoB1Y;iR>@0pz3 z_}Sw2dsaSw)_2(Qi_@}S{M9bc;^E@w@3`@^uc^7axP5;791LHboiRLHUiosI)0}*G zyjGZ>f3NV7?>WQP;%Bt-_JQX!c;})SuD3X&amNXJh3nmYwd5r^EAyTuVxQe zKFnUlYj|NBm!I6_sO4Mrwuk-Hdq#BqGa9u$Gwh;2Oi#Glsa=lu?E1vrTtBbp)T`(G zZ;gc27q@w>D|~-9ojc5#>=h=?i^J12Lq6-D31+zc9u$vvr*X`KUvGBiyHB-D|1N@#Tv1cYB2Idc~nnZ$B?; zJWhGM@^tMZzMzMd`}H-z40!f>kf%;toqjwsEJhDjpS|$+*Qa#U^Y#g+p}w{1S)A(q zxjX(9KCHYmXK~8cHs3f-IqW=xIr<)^hFfn|-Z{NH?H}J+mR~G4XZglg`Y`<^TR;1s z>?bFlzTL|?V0^vw?tIwl#ot-Vv+XV3z6as$3tlPS){D&ww|}{N#o?=^x4+hix8MG! zuRnu7v#BgFO%PS*pvC`Yq-_@ozEW=r+Iev>RT(` zdikB{Q7gZDcktHNvwM_R&%(tI;4MG#lY{%p>(8IDeDChmm-F`7oQ~R^Uw(D@o%iqg z(ZP{#Kdc#ES^cNRnRf?iwA_b2e({U;uxjD-_=+2E4tH;Kms6a@ZQ&H_6JO8+%ZIHk zCyni`*7Eb8zU_h4w^qJdYVhT!$HeOPYPME+pQp1Pu2;;*bw5}5&Ra~JZ`HGUG}@c) z6~62Gxlmu6;@;Kb{v9@^mS=IZ#h%C$ql-i1d^YOaOVhvaa&qh)UM$zg!}a?av^&{9 zGq0Z2=UyMc8%B?>R%~zhD<3~IPrQ$~YLBM3FDF*MdeLz5ckhIH-0C#)TdNM^cQ2Tp zy$3v=)_OJl>x<8NyGJ%>;1g@ zo)IUDx6k*~Gea6#?|nw!yyyP#C;QXa`o6}tpZ2T1<(fZkvH9idXAhowK7;f6P9HZ6 z9-qPFwD0nhi$lBr7kXySXZ9)Iem!x->g&~L$vZ>)oO|JUukdMB7q35gbk9Fm4R_yp zzrGavzI%nA&88<#Ypq(={&;4Qt1li_U0*p1%kbs)+PjGdw;z69ZF$AjI!kq&{!EGn ze?jka#IKKjwRm!Vadm$8w1@LlYh`}j>;H)kOTRepxo&TI_VMfY{+-9G{66c(=B_v6 zpV{*(PT9^8*LxFg-*{Q`#^I}HVewnV`yM*e?sHMzP2bjG@1!dTV`9ENvvPOoZq?NF z$nQK1H*cx&%Yo%HwP*JH>aJkTd1vu{<*jh1bJue>;~g(-xj2jMp76!ChhEO5Zg?p0rydG7Y{POZj1KRv6PW=4B+JlK=@X|C`% zy9X{`kC-<{wKes{K8oX?J!Zb^b9Z{S|HR7oT&sr*YtJyg@9lD!yyuvC51!fc>!}+W zW{$HCk4HDo@;3Gk#s3=nNNfA;!B=|~Z{NmW;#YT<`t;<*YU{U-w|--Nu=dRO@Utd= z5MO-J=b5EPvzFBmR~P5&m0#g!247B?71=vdym)yAv##{c#e zsPok}#$BwqwKHs8PaaoYj@N>%6E=( zw)oDnIPT7~v3g$L){Eh|_Tu&4@znI5-d_CHS0DZgQ|I^d6Q=J=Gpge^*Z%ATU(MoI z_jK~g*Lv2@PMZ(knz;Trr=8gL#dOnz$KeFkvOJ<7AK z_xEu8o?~(G-8^b(?dS6qH(ot#+sWiF7{G4 zGf&TX@~7b}4$JGUu7B&rRvbOe*t6e$edfnM`_;qudx(C1`_0D3NvFKHrFMl6Q!h~u z)7$*zc;Oizo|VJOX|&(JJ8EHmPg~s5Pa_|`wl#dcxbizLO}(49&*FaW4a+=(<=D&K zPv&>O+Gi`L&en=o(+tbG!}Q$Mwihof?p+RFt>SZ~@sXwN@6q7Vq~#r|u2x*{=3xv@ zF9^a$w69FE6YW*7?E&BK z1MU_de%`hE)%w}@^~H;K#XakE_g#<2oH>uP8u9uwvy9d=`*bEVq=5s&wU7KGk9*E> z*MoakgL!Y|`TEi~Z`hjo)#YZ=FR#}u%iTDRyf`c0z6Us%pWpV`)RWQ5S3hSe@9gxi z@Nr&$XQV#oyX%c_k96E4y=qw=CjZE{CtVux*H&-w;;p^v*JB@fz4nK>gYwl@ho@D& zWnb9(_!-VK`*dbCb$aFqx9mrwe2qrfS;NEOaqY4GRgG3YzL|@6?&?`M6Z7HX)30vt z+uEK_A56adyZGeE^Wyr7_vh)|2Y0=Cy_s5gYd?p$aonBH`2{_&I5hHEetRF_8-^c- zuYTuOhi=}g$A6!CzxS*bjorKY=JfK;S{%o^!uPXP%lK#Z`fD0l+TNGl>ruSEtlrKM z-tv6R4&T1C%I#UaTE%DI-S_48(wi@@*E?|P#L4uv|G96uxwc+R{|X=fO?y~5?P9a8 zcJ<}NE4N2nebvJCR1;V8>rVf#2(|Dy>}pX&JH@;GV!>9x-9LDkzg@10tWYFU^)Pp>bYIaeb*Q+sBgYr=S3d%^RIo%>XC ze`c=78MhbCtL51pR&8?$?Y4kn0pL0B;y{}?- zm4=?xxZrnF&&Q#~N353NZoIVz>5W&s8fyI6S8jjta_{(^&;8pgp8l|6-(#-u{VZxe zJFA~&c%1H%zruHIpM7^7CtDr4y&i-&PMEcL@tp<^UvKBiv--RfVdd5Ht$iQ%POj3s zxzBgt3epI#hpnDFsD`_9>7i|&oh=T0=M1Bz?q_^?y*S00Ja2{TxzmjY@2q~#(AQjX z+CQFihn@FxK@ZbId-p%jSuKmp^z-2~wqIS1MtQOQ^`E)EbTa+h>%8i#vGGG-zNDSm zL)?G&$FmIc@AMWky{qrsaq6$e#CdTVdcunP9(sNT!mGpkI|XJ|yPyY0ZXbK)_2lL1 z^>>f$k;Zb0zt;4{r?YYUg=hArpWj$Lug2G(mDAhaNAc~;E1k-E#CQ2(@pc}4aK3xY z>g`_djBZ|>w&z^W;v3^QTQ$#a=PR~l4fK-Os*5wBxdPdcy1tlP`boy}CQ_y@6)FT72;pK4N)wd&KKm zm+Og_;p61(TfC+xjGlLBx$VR1DL0GSYQ=@Ocb4v*>C21b*`pX9)?8}t#9lPYoq2ng zcTqkwynMXXLc}?B@Rx^6ui9cKSMB8KcaPQWFP6u}f2CK?+HY<$v3}Z9dcPa{eJz(p z{5#$1w9dGE&9BdN&OYynp6WRHv#&hdJ9UNM;nSdVa@1*8&*0DO(^q)>v&ZKz+-I{` zPd>}T^R0T97uKJXb!Yn?E$1q|&k#-{JWFFgQ*llUGehz2<$2YkH@`~_rk`ehIdAFv z9k{dl;?l{VcGlwC4|n@c{wq$nInBUVqnXDG|8ZX~J7;*J&#!vAOTF}B)Y~0+cIWRQ zSG#wr-d$IEYxX$z)LT=rdcL*$RWA=e`^vKuf1;m%)t}GgG#0N8Utb*WT=6s9XZHGQ zJX*2LvcJ}uhsxO#-o4n;9e?YElon4aYoF4QH&uE#C z4qv@lvSNI987^L4F)mJf)6mmC;qC8zh*NH+D}0c#dh4m0l+ekV`6Ula4K`1Kc`{dB6~*y}(2?C1Y){~e!d|MFV>{fqwYU1#aPDIE5j z*NXFBu9e~b#ufMP=l$cg`t!*CnN$7YohzN<)wt4!`R^6C&yTLv&$WMat!A}v`}O~& z>CaXF>{?lSr1x*G^}TDE;Xht+bJTDD{y9|Ey!}6d%A2pg;&|r%f37$yZ|?v7TK)g< zI!9Q(ebwsC!uoUe=Ia^!`)hSq&d`4C-CS|AKYY5!*Lcm|jP~gN_x}6W>TbG|_V0eO z?&|Nam0tgShVG%~ou%JgX~wVK9d?%biaU34_gen5Yjq!c$DPAH$L}6`*P0{k-iLbI zw>>?J-i6MR)!)Ax(ma2At$(~$)_z%i|L3)S=UV+U;Lg(?@&CiM!rHsL>Rz()Ke$%+ zS#Niib;r%n?_E2~-(Bmkuhsd}bGOa>m)A-s>pZ`DtW$KfhM}-&`wg zv-Hg4_pGY-%*%g%t#rH3-j(X!rDp0p-Dk16de+@Xyyk6>=1Hf&v#>h9edRy7*6&^G zKV2)m_D=KvzE(4L$Ia1M|LR)p^*7h*IdnI@KmTy8?!0%b{kp4W=v+U%R{Q+)T7Pz} zzql4|_n(=syZn=D#rcJ6Rqr#+}u5x=>@(hBnoyN4fNtC@Nx-(IVFb7s9iS$i~V@5v9Y)%n}I zJ87<->n~m_UiZ*hdXD`%+dX%V?xN?~egDd}diLMH)~{cyyXl?$t!woh`aJYGX||tS z%d>BuX7KF0gFm~L=N&)&?u*sixqo!6&egol6qXftj&H7&RN+-NI;-%Tle|oLv?heZT?pi(f&L5{) zyT3Sb+o!wx!)w*k+`se|cfR)Px%N!srI&@(+q=~Jl6HNar=Iri{BhdTv+VwUdaY*u z{cEMyzVW)>U%6J?&edwqZ?4s6D4pKle{rquBK?23R+>G>pI_@Quhkqsx>j>^|JB3) z>RP=6;eCJcJ*AnOss8RX?e^`tb*|o*Z?Dx}`K;$&{Xf3>?lbMQ(*5JNxM$kFY5c;q z{^nY7dnV1_=civI{^D9$`@1)DbXV=w{ruosJ^S|WPC5sjKfB`ApI__y*UEnJTD?5_DsABKfL1Zta`eCd#!J-)jc=MFJG(sZjbEOuhm}N zee*SI`t8|$W$wwdXuo26cINiY|H8F;|MI<8?f;M0@_D|(_dEId6{p>P-K%%0S>yM< zc1G`PobIvrKko0n-z5DN3b(h_Bdv)gj`C8p|`{w`swbCz+ z+xwr+UtFvAzrC8T*{c8jwSMDTJ;PtVR<-W5`cJRb-8Vzly)|!pclLjAt?s+$_3PK_ z{rjzJ{fBG)&b4|EesZm5=ni`3e{wCleGa>CciGlt@Ho&9Im>KW`^ z{mqs4Iqy9GaII$S&bo)@>^#laop&Fd<)2@xJFMQlba(qshIJRcBhA#@-Qy3gm0q9U zKfBg9*XrHTdxej4b$8#CTo$*p*Y6I~>9b#N&!)TR{+a`)^LLic-?Q$FnZ2rocXz$_ zt)5ArljiRZdVbw?J@I?qKfYGhT-{l^@tY@JchdYVSa()`XXstYzIUzexcA|YuH`w^ zQ!Re;{ODR)?}mGC)_C7stNK4&t2sM+b7kqK|GU@f+4r9H?7Od?Nq5$}ALdTl?=Y{l@Fe?bq|_tnt$CnU{BWJ%i5G4DI#n*Xr}rZ2!Zx{@JyfvF{t{#qYD! zEWI0PHAi>byV>WcXWQqd^Q6~%nC5R?t7qAa-EVi$48M1+-v7?jtbP9b{QmS>&HUHb z`kQMtQ+M4v)AzLQ>EB+f8M5?xK4$3IcFy(>>l|tP=WG4^T7Pw|_U_#68;^x|p5E8a zoNhBVSJoWqSMPZ=L+?cR?_Mhl>;CecJ1g#PzPVQX&bPlF{N0uJ?7E-stNZDUJ&WG4 zp5M3ED(^Y;+`517chB;B*Gl)BYgLQazUjtq#_C!5?_R6^G@G~ccDLVLtLK(x_n*~Q zy}mTd>*-xczh~3C(A{+A?lf+C&HsaIb>H1%+}@S?+dsX|_x)@2-u&=d-NBEq)w|Ig z|9GvQO?xz3dp1MQ>YHozjMJ&-+iO+tzSGUB{p}-qKD%^#m(r{Dqig-bwVJybd(WG* z^L+1G&Ct1epVIxsYo*Z{nl}ySiTCZb(#)E%?=9_DUw6`-H*<4#FTE4JYwh{7Yo%R( z?`-S;xmNbO*Q)oguhncl)AaH^pPom#d;IAYcX##w`L$RY&D#t;gL->ke(75EcQ1Xv z>b%Y5`E=I(OgCen>+ZeTesZmzQ)lXP)9in6tv|e0pS3@}R%h)#ecu1{iaYbquhooy zajn0+)?Z!gzg?@d_8#{9{_a|Tf31JGR_AKYU%ghdc7JJod#%pb*{c1V`o@M9m zOrBTI;Lonr9i&lj_0HCvbe8rk@1FZy#*6>$wSMPXy*tI#dv?wDuio-{)9KlC51qfW zcAoa_toa{YtIutE&CuO-N7eqtwZh`2{m-t|bNi)hWxso^>YcHDdtT|)+gW->%^0V8 zXZC!($F!QYd;6EysyE+W&G5~&;y0HW()#9Fy&FC2?xlC6_n`gaHA}Pq^J{g+bnE-+ zwZ3<)_DjFJt+)Ga_WC+odHYuD4zsj-{ypEGP50G%{*TwH*7I-nRy@x?E%oAW-*R`@ z{dbP~d!Ehl&9$2Ix3Bf>wW|H~wf^B+X?3pOdh^ZmlWX;SyUS|v+dqrn9rZn?GdEw) z;@7X$`O;~hA786G>~qxFJNF-5>-_wDdtu$<&#(2D*ZQk#HQTGtOy|g&r|0^wuGROZ zZ?4te-5*YK{)a48L-% z?z!))zjm$OjoyQ1^8NSwSKPbM9d*BZPrchcv+k>B*xh$;eQwXY`;!an-R*n)Pp{Rp z?cV<4TJG%cuXxYgobCDTwVJ0h|Lj_wxpQ@v&e?OU)*W=NxXltbtoNYj+*!Nd-qr4^ zcmJDf{kLn?Up>or58uC5camOr(!HnE9k*w-xZPcI6n9tgn(+^=6`po^z1_Lbc+a!5 z_gUzie}1j@>`r?Z+OzkfS(>4_|MXg&vHPwst+d;xyKAoQt!J7}{OX>|-(7L@_8ih| z->liXlb&hX^`-aCwSIK1@@DDX>fG@=Z~Jt&?a^na_0wzp;kABvt@J$8{4ZUrXO~tz zVfo*@R?oAZ?k#JM_G;$tAp60!;&+Dl&EUM9r@Q~f!Fuo0YyHEu+P}Cn^?j?)NLuN* zgRpO})t!FtTHWI}*J|JNyW{xXUGH>S|MgnkY0soR-NO|==IPgko>i-}bT9SyS?PT4 zp*_=X-!#+d+5OSA+Uu{c_4n5b?{0dx;KjXP-AgO3b9Uz5x1M9~(Vt)IFRu09t`(le z?|ki*_BYpR##S@@{94^rTIuv0`S$STCe+xL$YmQdB^%T#{S$AICyFniFVu|rK@J(Fn zy8-`$@aLch{HBo?YEHX5`8d#MUeBq!_^Tie9cO|5RdDvxppOi({l-r`J@c3FA~?4Z zUIwxDgFQ6k>3imVdY-<~sh-YS=ZABAbf>n0eP4&`@Np2&6F-{q{$GJ7G>U}}4)Tg= zFK&C`Fz8!8h^sHek&hO>*z;2`rbpiQ{@rgg|0l<--lzZZkqb8->5Y5Az2KXA;2;*A zYR?PW^a_sd4?oolH}{PfYPE#Tz>n>)6KH)B^yF7To$#mgRnW`&9Ov(Y-l2)U-M|YT z9R=R;%(=@jXZ0m7?70d!7^4N}-{XrTZTgw7JmHsfa^4PN^NUXJ&a47IaJG+M;#)J^ z1$_8z-MhW!mtgSTI@(8X)s@|`|8 z4e#>MtJXCAXI}A?KC$@D3);+iF5ynl2jb$Tr^M8^4+9Q+!5Yu-5Kk<5^$4Hd1b5GO zkS_6Qqg{V*g!TUYuvgdnQ*SQeX_(&Q*Yu8lQ-9y9^`1NEw|+Ybr$OJHhl_9(Zii0- zkLlP6yjDN`y9(DqEe)J+1?T8@mS5`lue9)w9(gu{T)dP^f5=DM+^_Y!rJwZAhg$iK z<7i&>z8m%eum6?y%hu_qP0#Sx_&AIny?j6DEB%1yS7G?*XK{G29cZD?9(8sneRmA; z`E?X{%ny3y(z|MDoj1K zy;u1;y^6m$;;Jo{I%uRxf*Zo_2Y> z^Nb!nrAPEPFYsLgpLYYVc|yxh_*1Zkw>ZwggTOyp`KAwmC%p8|OZ&ul77VpeM>WS& z4*Ne1>Wz;xOV|w0!+aLDoQEZtbM83EVcW_CEOgV2>Pf>m$8!7WCTq(d%z}#FkGT#2>D5`#eltryn+&6OV7|`XHze&(s#j^)oz_$6kEEH$Z%Paeo%nnw~%3JneBO zd1;O(IPwzr$uFKBUxNJV;T(TgVK?jrJVrOqE`r_=8+RO!g7ePuiD!7&LqD(7!EbAR zk5)0%-ub@=cWB)|I_XnSocwmyo#NlUKr4Mu0)Nz+X5PqU-uPSK=iNZRvpi70t*{dw z2m9rF9$ti3fj8d<-wod`T$b=dz;`{Tcz){%n(5-Pns0=u!8(uM`^MwJ7hG^qOC0!& zgMQLCdqJJnd-SZ|YQ@{Dpk{oVe)G;Ne(=KY@lZ{1#20@)^2u*?1E<+uO-zq;s=JK@uShrRN6S999=JbvM+p1eE?hB-Y&>*S+rKP*9Qa2EGX zki$?1H4%O-THRE*N46H?|q=3b~^D@t0fp4fk$Gw zcj7PMi*PUS!G7N0Km9x&dx|R;e|TmO4|l`VP7FR-$CVy6QwJP)$}{KaTLpXhC(hJu zYKE&hwV&@YzpaaF;Lcn8#8(6BwCOQ-mPT`Y2E%W9$EWenIrI4OU9Iu;)IWNR7QgYc zhTC@dXHXNIy{ijd>Vk{?>h~~+&sW~yttZt;eDx8NM%vWV{BXowj28h911Is^bMf?( zKHdy8%P+Sl(0w<^YrlQ+%7Ggn^eawazdh?5_{3X%p+;9hJzj>nYt9%;@Gkd8z}^0L z!93pEK|I(Gbb@!D@|q`8GyHI#yGOroDW84EiWEU43U5KC@4xMK}>p_ z(VLF~E|bfCaV~>F$2Y;cp%xznI`xv?l4tsf#?ONI^zl=SgCL)Iy=2a_}orbS@TzFuPU$4TqK`;8vKN|7z-JAI0=;0;s+c|6Y z$&HtJn&l9G3Hm~vc)A3B%4eUt+qV(qlIuL+SSP!YtGR_hrt(~@L4VC<%t^GnI@7~PacPo6?hm=qsLo5kb1UGw z6L5PH_@V~<;+5K~As_kteNfNcuow1&9ykiu;d!6||3|^RIo$Y!@5Ji$w-|K(6lmi= zzv)&-y>J!OMqlYozR4k$b-Y$VU(v1(-j9PidGeQ!^z+VH{MP&4`mBkIkG!7Z(8+6O zoK;)8cxetVd)4AaP%~P2I^X(Uf8%0J9o1wNyF+2LC_3@@4z^AQ$b^H~+uAz1~|>!|5*` zO^?wvHC?~|H0}ppPc7}GRgdtFHua;M*ZPnCy})Drre^fhrKbGyZojjwpg-Ixo;yR|n}ECVagYZ$e&S^~W4Om#;Yq+r{AYo$ zo_L9e_q#yLVK@r!1Ks)>=ebvO%Byzb^O+BF^H;t2`b)syzRh5dCqHra4dSobT!-Ow z*}I{3Ji*($v*N42p{~}%P%HWQBaU-|G1q7y!nJ1PD{{#p5xs{ z?=<0LkN%chUizHH11EXu*bZC48qRzd@6SOzbA~4$zX@02GMK~3KAH@BZU>s=_Pzvh z&w_Qj^}^&=KXn$1*9P6V;j#pLaS~TvXRYC_PV&%g-{)ZoJjTttI@`lzcSfyo*28Mz zH=pECLpj95*B*Ji(;>%p;02$>^Lsy7lb6Th*{=>_SmU)`vF^^h?|9)tlYRE=gz<$Z zFM|92Q&@%Tz%SqVJ3%k(1zK(edWP@%w`H?=yxpYZbPG_LkbA zo9EJ#*Y@oM_2Dz!_Tg-JKMZ=(oHGUva>}RvH0UYo`$2Br8TR5}KOQ&?$Mv334<2s? zd>)7C8QxCc(D^E?_Ya@ngtx&r()S1#^_X6A|MtQ`py?r1~UAFuU;!RJ*lM-vb2lM@%cf}4Emi9f!4QVZvI zg1XBmE-%F3HI8_x37^#3opRotn0^)GQQ)`SPXiD6H~!3VzC5+Y`z7$(Z#?a%Q67GHo(EpbWsm&Z;d$V*Jot(;zp4YyYN4)=!sGBH=zV=g3!S{<*VjS)_1&AWey9JlzdgKI1wQkOuZQ6% z9EX$eKAeWL;9gvW%WxIcl`m??Q=aI_Rp5cz;i32V#vkkS^Nwc*FU;w$@sU>NoX3qH z?gagKsi!^m^I0w&^tD{(^tbx_Da>7xcL{Sx?)I+M@;GN5_eVGHycEMz{jDtlr_sZE zan#7W{t#NB;!*Sqi7aq!*2U*Ap)apb{+2LAiK3QMrh{?j1OyKoicT5g^+)8d?WIi3W=ZyKzR z=5KmG4}S`{S$h>e4fgWKIsGA)`nngi@x@-e3_R45&ijFn?%_#%(` zd*aIzeC^S9^oWnYnChlyUI)F%PdVsv)_2}-{@ZU(-gHVxd)3CB??gpN#+0;%xUh`6Xd+g;C zug%{IH1H8$F>#iYmptI1b?4QR2cxOi-_GDE_ju=h2{_?xo*sGq*5?z?Z(N@Q`uz5d z;4^Oans-O1x9;_hqdh#s+dg-~*^h&m=F|;u9*BdB{qoT0`824fGuE89SF9fct~BBQ zL%`!Qtb((<;ccMtQQ((4@RWz1p9Q&&!cI`*`@w$P#OA?xYYsnW>BLDb%s&oGP%G#0 z3oQcx?o^ybpiA@kI>h z`1&BY!#LwBmKdwRpE;`*e%}f>(``S0<#K2IR+r7dOML0EZXV?J&HFxxMVG#JR(yNS zS<@rpJPXe7gGb)28_wb&7vAcHtM#7(pZG~DKkzoh#fJ~{@Jaktz`=fdal)A|&%-xC zetVqN_xAB=_^Jgy^6_>FPs5AwDtsICroR0-=*!)(7Y@QvI1VS_G@OO=a1r<{?`Gfy z9{77Vhoe~b(YJ*0la^J$!#Tf~Ag4k9mw{&w18(jnF8t&(O}xOF-e4F+D~h;5HE>fl{{oblVcyx?2T6LF{iaKMcZdca*50~b2n6LnCl$3ai3 zf!ylJSI^^MO&w@4FCO0RJY5Ff)}22O&jZi!-3;qaM5#M}KSvb2K^colms7L(brb|8>x7>Px?MyyXP( z&cYJ@EsXcS^v)wW)WY83FOPbPBR9`^q6gGKPtZd9RiMTCcqSH~@Hr2(*&~kg{5CHR zpKzvu7Il)_e*MFX?VwJ$$YX6k$fqtlfpuWP+~pg6dXb;TM&ReiLC?ym2A>9c zs4-2vR>!lz@2jAebn!-h=dJ%3e6Qrdf!E&gl2crwph`)LDG}L%(x!;Y^n`@yyBlWuSj*`yajY&zkz-#3SqS(4;CQ!*d>p=j^@kJnRL2s~sIU7;@Ujcm9awY0iBy@U(8t z*}Fk}v2hsh_@Mut!&$B7zVT=MKJb}teXtYw%r8BMFModTPUwXX?}0k*2cCQ#HUb~a z%dd{&;qC05fU|Wqk%u;L_9F0*K5Jszdm6;zrTR`?=~gReAKtw4M?S+ozRmZCXZEW# zzN_FK_>HlAwZ{IkG+e1;R>oY%__ zgWBkMo;?cAorG0TKQ(nmZ{7}c>Mi{!25!JlTE~Oy-e*6b@B;T%eqM_qF0bXJ2k7AW zWuO7KFDo z;qd02$JU>Re}<2O9P4$Y%e@twhdAqjTY;VzVK=A`Kk*cwXZ%u6dHE`zUU?Xd_rbe5 z;@~W={5JS0j~d}7mfqqCkJap6m>Q@bAD!o`9vRJa;P9`$N9w+W&9D`=1HF&Klc3MO z3b@j(wm8$OF1Yhfukf6H9`Q$w_;VC^sUCD$qh}-h7@T)c#HCj+Tn2mb!wZ*HpzkV( ziP!M3-@AB+!5%}N*TI@Iw*swxAB2;j{=8EwoQ*F7PkE=#`eFS$&L@54uH&IM@#Mjy zpjO_+;eot#(t#swpM=pjJ;uLPxDNK{KiuTA#=9>9F5-C7h=bbL2djXa9#EtGz*m~- zvgdZVAL#SjoijfAKabz`@p1{ytBsiMjd^j+(dCRe9`6Obmmt@jVB8C|=;5tEv-7`( z*FjI)3w$y!CXL{Z(<;v6pl*6lznQaNjNu`+{niZ}@pC6$1v&BYJ)lVsPEPlfclN3! zt+#I8<#HF)c6x)iypdne@YO!|h8|o%K0J8vzi!@fdK7qKP92@6*}cMppU!)i+dGap zIcIp6ljna3_TnOro>ybB=botP?`g2l+7b?Lo?_6!C!A;v52eHvWAT_)Fi{;X{pl$8EH(dLO-f{#TkWTc`Uv@Si5W z^PJzCfks^25#Fk&@o})9Cig}^eHru?4R||?vpn13*DzktDu#XZ>yh<4%$s|`Iokbp z7B9Z6_l0*naMTZmn7nZYzoUS!dgw(pv}Y&KtPX}e;`3i#PrWLJp2A06ox#_dSf7T` zgtuPBXBBvhhk3QcLu@g4JZt*ay#8@se7}b;ZiYMSF3O1)z7K=F?k_%`^z)wP@xq$e zcw3`iugfLY$HCljkmu>mJ1zKr66mz{b(lFhJ`4AQ^R&{nAMn}>bc|oLiY1P-o=3r1 zTzH1d--5yOF9Yu8@cBoe{kx!lXmnPeO`fH9eC5z1c;PtrPY=*9uYG!V?%!9vs|B5N z@Ay97m*2a02d(q&B%Fq`a2|YP_{kDGd$e;8=T*jOPlLSP#Z$YzAU`f*i)SCd410Zt>{SDN zX}%le^5nNZwg*>r5lTiXqo#a8@q$aL#@)o&`A!8hHiwi0k|Fm!OvP>QQ?1qq*y#wzU0wSc15E%N}Rs zbVgjB;9@UsOQ4BAdi{RD_xa85?|Z5RP2-dJ2f?6m3Hk;fIYE3+I>hwE(fh>GkIwLb zAJ%xd6>tQ(#8wOE%<<9t`d znI_!72=ZH#7bo?ab-%@O&UZ(A=lq^;(@yX5sJZ&^e-)0yb--JGzKf-H>Ld=0>Oebf zeB26RJ`Q5)%jZEY+`+GdZ-O4=%@09dp7<8*26HqygAX0@@f~+sdAb?w*PFa}5SC!C z-ce^~j>1LwQ!vD{A4h9`yL;PVFW`X#POD&zcT;1U^cr3Ad*VyGcX@H(n>)e>I(Uvh z4^P6az+-dg;csCxh|d#VjV62W!|g2Cj~7jPS>5G74(cSgSO)G_!C5@?pxE}{i61U{ z3CEe&qw*U(lItMYOM~C~idXwVP3gK9)Wn&)0dKwWH1L#n>azs<#KRdkJgwuw+x6bM z?6;V_FywW2rY`c+kM}=l-@F2v z)CO;H)Ju#f;b~C&?*m@)8Q!h)&|ZAilz03PUvBrnnp*4xzNwEI;4yXL8*Thpg1EG( zhe4zLa=OdoFW>00K67FjcY~fboV^|NtG=gu>cmemdGtJ}nZ0zV0lwnse|*;S(CD{b zBEf|v+nFk5T7S%r$Lu>PabIY`4Tq^2 z5Aalr^KcZza-RS0jrhlbXEfr+6S34{Kk#t$d#Ba;@yOXF$U(C;TrUHk#}D5rG39l) zL7193M-SfiPVUX#`GvDQ-u081;>>wx@Z`Jvc=|o-w|WOXAqHOhZ|3a9U(de` z_BlIq*6#(po`e_S$DqEnii4k8%B@d%N~fC2rQTwyl{n^oYsH&<;w^!;=fR-K*a%|d zxEZzrKb=>*Pl7t&sh|E3?AZzKmwkAMi~pt9Z^i@0^|)4WIOGjW6EUwfiQ}O|#yi zRgTg6ws(5<^l*H3tE27R&$ zeCN4)Wu7Ma?*-@aeI9lL4!knt#Z7IEB{*-+pkp}e8+trvzrAyJyxulndw5T6f6K|CDP$MBB(tw1Z?dcnYxPS0n7hW#7hwgi37kJn)dZ^FC4 ztA7Su<#f)OS3wQjskv9Z{Zq6WKJNPS zMR*x-!1wEb%eUceaEEF9DYzHYPrJSE1sZsB98SV%I1BoXhnL|hT!)Au+~C!J#9NsnC93;5un z=5yZJt)RX<G$`UT_^L%z>~;hhJx;VPznr)B({+UVVP;ltYS|0sx0v%aC9CXib! z{rKqSIa=(ObNY7ltoz{{Pw3M_VmuDC>SeY6uln_0_2^aSuLF;9S1jO z?#O=F4I9CIdvWv5Z(RB09vQr*OAL8vR_8gRPCS%Lt@Rqs>flb`r&rAL;K#5E)}8x2 zSi?oGTR|TF;9&iAxCk_W`r!Q_j8^qkCq9UyA67wJJ?A$*wBctScX4<%_fGxwlUTa} zXMdmY;g31-oW%vdUjyEB$V2bs#Kk+$ouS_u`t^yPz{xrL#8`rw>kIqD&|{NNe0&VC z#CA`4#8Yd4(>(Qs+R^F^U7KMeObz*sAHCwqdp8(g1m|&9f4_0I$NYG2zdigE3nw{d z-F|b1y?#$jXAE;o5T73J>Z>-s;quvo!^A!B-Fe{8^T1C%;xnen>^bAN9PX1%8b;HlMk+19DWi^*a&pix}!;|n;pr3Z%PCUO3-vpZVjK8J&|L33v zbex8>fG>Vm0as_}ej50+ALLa(>$HhUzgW1qlVZ}Ozi6T1QMeuEUOSJoIlA2onm2>9 z{KG|Ty!_^uTH&ESVms>`?fQKQJn)YH)WO{rXE=z(U;T%>AvQ1hN6U5C2)Nouhrw6z z@uR{1%fKsj<<;cDkN^7Z#~}W3I1l_$54BJOoDBN;VC^E{H9m{S6Az1%$IxeW61iYDB6C#K(g zab8`W)qCdg#Qh-P#DgW6*W-M`fd}T?2j>jGad{Th9q63lw>@(6o@Y4HASZrPZ{Fgz z5!4eG`&YsIX5j5!I1e<-YpIShIhA35+4OMl{`=A)4Z_JO)vqXnNO zIB(v%e&n$ldB?>*LoMyG{yc~yr&@Uz$GRuI?jWz1pdX$F@o;<_R$(`&6)nf%B%Fq` za29I)a~XJ~F3!{XOVC&NsXrg&eG$am3floES`GuQkAmFlYLB?`$l)9ghCQDJ z_1O&i+Tb6*>HQ=)W8as7F6VJ}&*k8mJo575Nq8FMAq9q{4F55XPczuM49 zw;Jq)gFqMW=sXFhVf@oCbX|q(fH%(g%CQ@qU4pvnCwlG&cX2=9gNNM4^RNnh$5$Nj z{Ki#HJpV@!%e=uip7GO~8p_8n_lF*y^FvHJ)q3h6*AnnBCoYfw9%xdZjUb1;de3=v z!AULj)<1&2z)>vzsEKp>3qN(SS4?&ATRi=H9-NWOZ@xVbVmN;g-Ul4jNzbX#>=)BH zux=j@=+QU4+zn!?mo+-XG}ddVPD^+co`$c()JhDsJqdaOXR+WMzGjY_z z9NjpN2J6E?UVAper-3HEg8Dl5cz!IP#Sj)|P-5{k)=se)Xj9L9nhjdEtzj;pZ;! z&VDi7X?Jo7>Mz$SnB%SZENOGa?ZMYK@UE@Cl3Q%&hZ*wetZ@1`;Xvz ztX}%cejL6C?h-HYGk5pq>FjZkG5py$$Z}^_!rVs29 zldd=6agg_ZSc2Tnicg=jU*5cnYwrEcQ!mjergNT#xHKA5cWZLsLCz~sJoF_G55vpwEQm`Fe)j7Pe5Nkq=oK-{8@!iCPQ2B|8V`9gy6uxkz12XCX;&vb zBewd{!w)?8K6g+r;KAD`fxr5V2KPvBeiQVqd;UZCDXf2IPWycp@Zi__Ui;izdoBWx zaFL%MV&jaLIbQPimoR-t$12duUpnRC*Fg|}&hST1(`~P(`=$nZj9zu6l}1|h@pTX% zA3l5jAsFWWJGD?NcT&EqfYZ9)cl%8{F0|@}^T2b%etgFZ9B5E4^K^+VhI&nJSo3b5 z@z;Pm-g?>aF27nE8$r+D@?p<7BPRcylW+9XCpM1w$f=+8C{1#&_o*7wxdc6{26{jp zrstmZ{#AGpUIu;lZBXAgLEq868}`C}Pq+G!S_hwkd===C@L{B~ZCIKv;ckxM_CS9kL` z>I3`f(jz>vZXeC&y}u4L@aiJG5ByR;_m(Gjf_T4%!$9L@Ft4t+gLvj>co9y6^X{^q znLgs#tAMxriq|Tr1@Fb+rP#Fa0w;66cj{q}{l_=&^zw(t_&URPJni8rKk1`gZS=A; ze(MV{c!BF@!8d_sUdxXQE&IVa{Ad=Jzh4G>d5nWS!;2q&^Tz$465!$aR# zx7RzKa*9i@n(|#dJpA4X_Tco7Aos@2`>Ll{#{D3d@mWwKeI&M8@mNm3y?gS~dA-Y9 zJ&NDlHMMg;)c~iHF!j_|>hdJ$olgS}zDvVVJ>3EQuP^=P>+9ey;i)J1M60v<0zX{r zl}CTA!cow(xVu9%8gv@+%crK^t<#5}K9Qe3HJzN!@|c##fgZJZ5vKR$q8mTGfHN;n z1HKpGI^aC_0_THp7Vx#kSGr%`fHuC#Nvrzeij&&lsP^hc`g`{)E+}k$2q8#tReA8yXFOQKX254V>;yHQdh!rI-l;49)rtS#2LAHV zJ>?_6cEeuS5B%moFU|u#V(2CL@%?AG3bcrYKQ1(=o8Pq4LkA73fH!WmTiXb{G5Dje zre^w%uX@SccqNB*g*4-FlG@HPUOFf&b#;=QnOgfks}aJ+6GU{yMlj;}3mz!bNb$ z-JSI{bK=Oy8~o^{T}*mt_gk&Se;33P3wQbTgnOWNyvCQ`^4rG~XYt<-*6`%7deW^{ z>gEhzX_KGUCGf#nUg3?W8scXk9pd~LRzZ!$;;VCb>f4vWyeDpWIr}2$CAoRAUNd@m zAP&y z^p59uK^*ULS*IDytCKnLx5KZ2E*|5~N1XpH;K^SxcY_>!w?>n9ISvD_)y8?c^n~8f zv(JMZzXTj`8GT}ylYb}3;fz|CSBu$eeHA8-vtr@k+x95Dx%n-pxyJ#Y@mx>wMohWv z;jqi$f0%`rM!yZ~Vok**Wt(poe~(__Y_#1HZ(T zk2l~B(nBZhbjxoqzdUi{4-I_O8~T{H{NRONw8m@s#g*$>;16H^8QulGiql~@3hx8H z^3kCec&A3ANi2Tv204cBR_|gNxZ3|wz(t+u=hN@i7DsW!;I$mc`)Ua25EaX8fHe4tV)q*{gnhejVu1C$t%Q8GrSVmkxWyqfs33w!`ya=o5V) zpLotb4tjw1wD5Gj7sb&Fp9JUBKn}jE_xQq-uYz^?UIt?$h~bR->jnIHfUiElLw-Eu z@pOLp(!hi1BmJ=ip6dgC$s^vIV4WXwj;GdeQ$O1Er?_JBbw3>4{2t$VC9eL&iAF;{ zJpIPYKHm)UYH=Clb(Yrg%0AxkmnZs8zvKBd@WB1S0$45;U+fiG}HSyn0LSE*niI_R4( z1JC6d&E}kukG`ECmUH}3+n2$x7thf+-(I|MItW)``amDkE+1~+1;g{>8`vw(e&CTl zl%KD9K<#Ls`tjB}E_~GQuY!By%bUzFD%a4=qgE{`twEp+uu-`}FeRvnF)1{|qyH5GX{O*C2tJ(p0D~`p4S2Ymx0e>j1M@=VT}e)bFc(^@f2(B ztUS)+qi^gPUG%&P`t&&X%~N~%fbS|e&%?LjJQ&t+H;1obA6{31&-93;2KrKOtCNBI zD&V93`c-VdLCnjr6{e=@@0_~gvlsNSy6%Q0m=|wyJ?R|}p2(rryx|#dtjpz$c^>e? zJKj6tc9=f@ymy+GfDgZDlm8&-%adRaKkWG?h|e27(+AJ!eiiK54epOK`s16h3bft{bn%4u_K0WAzUfIG?*(zx zRzBa2PXdpe$BRZ<)ks`5Qxo;&{YDVm6F0TF7nb1c^bg-Y>>V}LH+blS^}f*yc;j~& z^v7x6hn@co%52U#;=C7k}E8;2!%1sTJM(VF|R+LBE{;9^|x7JVPCqAf~^0 z@``2O{a{|4CE!AzJ>q#k3+k&za^d$Z;Pz_}^C-Lz`eGH-kFPY~>Tkq#;IlPzo^;8j z=4#6u=SGh+KLovp=MpZ%Uf|Ud>=%RYTLGu_7&KUOXZ{-SRx`QmmFscPAN+W|jo z^7$>coIJqA_&Df6{kb1}A7}>t@k0zXu+C>5i1Ww5S9!&x0ax5+kG>Vh5W_y-jP*Hn z(LaV5OP~P<9^Eey) z+z3k;f8}?F_5;rP6jwTULZiCP-vfD{1^3zAa*yP4H*m(?8UFFwetQfY@o{(EAu;Te zOMX24#(@^=!`E}(ao!Bhy$;Ut1y6IMlg6tsXFbK2U%!nn^y6wj?cT*RJnelF#HVLB zEMfW+ciPO!?+l&l^E?>tkN%ZYPl~D6^yy`APHtY#U3u9%&i3mgaW@0a_Ah~+d%-uw zUfPy0ebno3zju3X2K)4#@3VRt@=c%8Aupb|tEJkHuXN~RIpx|4JAps+=vC|V@k?A@ zsHeCS$35jcKDhA`4_d|Hg?fmq9@h4Pp2XWeaploVc*`a3r#J7`r)IRPhq(Chl6OmR zUwERg^c+41;V6uc_TtDl9{4@Hao7&D@oPWeau$4#X_R*f_yE3m$S1CN`0A0L!g+A+ zMc}hIa;Uw!c;f5t&3L=nyE7X>93DAm4=BW5s&w^TsOQ$`0#Ci4BSG2zhIOwD4F&@Fx$GM%LK5C~I)QKm~yE`BLn}v5j z2fd<(>Zf1S;yhf0>wv#LTLpZ@d>QcO-EfmbEOTP<8su04zVy?{{|2C>Im`;GBo{1RVY{jYBL^K>i7`FlRm_uuhKPp<-<VGK zFujf2)4&%T^pO5}8`P5zCrrye$NqVe_3lg{yb>0SNQ%h(8S!|29K|KN07?(U3YmE4uU&)5>CTe;KM~wtLuQ5e)j#q zokuv~Y_HgQfUlmk>MeDmjqX>$+`FKs)Z{Q61$||>C$!=GUC@)x>jihm;PLn&7jFFJ zi@bah=R;rcA8+vC4}LEKZsXJBrFA^Rd)>Fuuxfrc(85Q0jsq`g`q0nI=EhTfG``5C z7sPoOMRbp^W-q7xtz|+BbMBr>gM?% zd>zDdS8$TUJWt%MRiMe;@#L9Yn?ZhYi<}@XPxpiP=BD@Xrj<_fd~#Y(hV#A&dPGl&Z;p4L1baRU;!bW}@OUq%%gaEc`+=|D&da06`LE9IvY2;+c{RY* zyKlEOd-Nnvhxg;&apD(_*I^ap_%ayc=uvk>pKk}X!$}?<)2`PJgZ<*uE-sz&s)4;U zI(rh-%DP&875IwJXi+a7*{4VJ1ukOoU%zaHoxm@8Xy&1K>v#T}e)9@vT7C#LuYV(V z`@I+Rl$zs6^I13#7vVC@-S*8=pQ$AdcLP4Wq?I1LY113#?4yTItKbYhIF5dM=-}=9 zfD_+V;Z|@)Tz!F$p)UNgh7&*3L4S(HyI;a8_^p;=eRA_YeJrMd%k+x7?aW5dAADQ| z9;iLP?gU(?cRcmqD#*bDIiCdQ#5eYWH3J{>d~#-L&u4mR5Oclu5Btq8adDj5`)2S| z-KXY1_WpZy=kI@3^QkvI;sB3n(7X8J%6q;Wp9UJ#6>q-Hx6nRwYQt+<_@cfvIJXtV z)q?m@r>2eHRfT;;(FC;foOPT=q3FdjeaozKt1 zi|{hM3OKI&{$0Q6TEBN6)fq_O_~R?C;)2}H(97H9=AFm<;;DV#1i9sM zzxX}hO1w7%&+X-#TG_8|;*6)fr-x2=K)uDZM~~A+i$0~zUT1l2Yy>q`J44UNCGKZI zTzWmlu>UIHavIbHN9XwG>8v^T;CA2xPw3ElzCU8(W!`W3c}9agQ-k^T*au>&!`mRP z7~_Sr@`@vm7~o?!GziTR}{D z&C{wL>g!#OCAd3ys*(F3k9g|-DA2hZf z`oHhpJ$2vZ$5q_<)|hiO_-#qf6~+$dZ1OD>>cGLytMT8*s2Lj&r=Dfkt;uj8!-Z_k$cfa6fqM-ShC~dDW8^x%hz3aNxl~u!n#4 zh!5i@ZjS>!bHCISg|XAM~lb-aX~!F}+K0o_@akC2R(s z@ZP>Jf;;pq=zCoChB-CI4OhO%Y5gczqurW;vvv4G5Zj!*ILIfa+VYD&J*2j@Jqono zM=vghJMthn$4^>LgZk+e9OSVk&X+-5c}tsI&P-gsn8#6WT5*%p{BhvVZg5r~h|M3p zgQI!SPcH(kw3(Aj9n{I)agJ8Qn)MVxX&d7gx z^R$nDeBizP;;Ef}wm#n;eptg(4aC&%dYlhu!Q9c!6X%_<9e9Jc9&nx(o?4%rwCP*F z#qs^YS1)?^q)mP0xEA_kIwYPI-AF56BElzqJQ}C(hb4J*YN%5(i%Mg=RH*7Wm9l{PZ8q_;}|RE%xEb6Tii_{&_GC z!dbxM%YcKLIj@KGg__ZVquln>;VcenwSft^;3|AO@}5VK`WmhjuZSfE%uU z-wTHRa*jT6-Gj~WB)Bs?vq!$4!)Z|0{a~IC{Mrcgx_{ys=G30Ac)9<4aj*G8FHLGK zhC8eWcf(QOqqAG#VbE7N?FISOkw>orPKKx2-VfGr)I0LxGT$M6GCe^%zj?C+e6E6T z2hSb^ep$y&EexFH6PI7N!*x(sTGaH{K#Tf|OS4?~=}q;W-0I>y-E!#z`X2>c%+dPp z<{g)rzuWr{K|FQR|Kb?t#5T`2b>In(8$ooYG3*y#@9EEtFnh#ymX4#Kr>yH0 zzKf5GVK1$;&3R|Xi(Y@rNw+x8%dhS)10U#dujG3ic=jrM6V%83T!pS5q#Ztf(1j{}~1f=}kAF7`W%r`W!i_~AmYSmV*=&HT?j@p5|Cw*I=Jsq5x=o=pD z<=ueWWq2R(5g*e= z52g-Jdshp5UWTayFX_ zGde!hfIkj~TH&_d|GecPtXwHbWN#o`lQ z{G|&on%(X3<#F%4q8;BQ=u5oyqw~Ip;^M5Ay>ACSCYRh#!*1ZOT-H4|!d?(t9NyCG z%$MPQcoyVR3quU|KwW9&+0?}oCmd*WN5oN6TyT`jzL`@azt!O9V4c59zz+xO=6QNI zn3u;H>vZ$wi<{r>IxTYJYM*zx{1$5o?{3~Vd+rD4z4HR(RX6$MHk?%}F>&F6d&C!9 z!0xcU=$g@~6NfJ+d3@aSji${PvEYr!_v| zOpBcApr*&cn!R}M-~9GBa0$+iR!`>)XLzxMtDv{!)yKGrdm1i+ewGt=e9r?Oe5Q2? zlLH5y>b3U)FF7BE%dipdgntYCIthFc$5~$UOD*`n1oN9W@8a02uI6@w8oBrM@XH>x zTEb|vPaMyUps)PKZ|xCR+*P;?;+nJnWw;&08vpfy980hl7rJ>&6AlJ$ zdQx2`@6x;2t6+#PhC%P-rAHo`E(5QZpcZndnR9Y2few4eTYQ~&PwYPkcp7TB1UYtt zA@{@*duqtH>tMJmbZiFmp8h8BiQk48e2@blLw-IOv>5#6i#X4MSf0j4xD#-aPi*^X z+Y9pPf3?-G;5UzGbpH>-zC$ z={@t}nb${j^9%>M%+dQz;JI_WM*3b@Ij7n@&dX~?fGV%X0sHO56R z;VHM8%i%14%q>A2dc@(^Uf{p|_~XepXYJh&>W0hQC-dqrw|V#dB&ap*Je3nieu^jm zD#&Bc62w*?d#uwX_BR3V+kt-jY2*L+sQ%*fZ#=v1oexWJ#!y!r@fBbGTfsg)$ffS5 zfhUGOn?3w=_FWKPJ#nK|{Vs#vwqHDbYY$FK7!CO9z1@JDKEQ#ev^#$%$mPj%^{`jW zoiM$_U%JJco_X85zWyobZFvsDaZnrj&ca0)&h+pD+%>uRjnk{J7jRj^*3CQZ8v%D~ z{80yY1s{GI`18mfd|@}xqsC&WySwHd>Noktb%*dZ>@!abt=9dPM{UK}4EW2%tI^00 zzUm!(oWp}Qe(~uzsGBuB=)jX_{Q5MAsfXp@1DziS{U|n{)R`CRJhkcdH-7vyZx0^& zO^@(l6=u-=_h66OKMqr4-0sjDIHG&@Abr2fk*h!hojnl7v2PSXZo2})6+a~57lTD z)Jb0)hU1`S@5A)E{+M34>RrFFZqGD9S#`m~{1OcOaM#B?cb32Q=tXfn<(JF4 zUdNC3;^`Th@mqrXq(`P-`Sn>4e=F<+YtHb5r``>*o#)+pPflMf&GUQ})I}fhkazt5 zAHm(~t_JpriO+gmnkN@e#8N-8#(RG7jYoLXvlrCW zn*H8a!S9~}Z%+f>c;JWASQ%^Vt6J_IB^;tgc7F8GM|xrk?U&2KnUY4PA1pjd?vH zuJcoK^Um@~9P9L&b2ogatcj_f(*sX?|2nuU`kQ~Jfv0%z9!K?|`8tR-d(^euF0u8Tb85s3 z9JhlxeKmEJgE#c<1$=OS7Svx&)LsuccNpk=A8^nMyi*_E%0nM-Jgph#>;;U+>B%eS?Ui>Y zxZgC=BgYbOaMph7&O8t5MTa^pVZ1YM-FbQWL(f6L$Ge_%Up9mN=EVUT`Mw>7|8N!$ zcd_O08&|oP;EXjjTY|m%QB3~52pfT)dPrP8&UxHu;>XWHe!Ov%Yd`oV@=P3j4}){| z7}o4@P8_+-;o|=C(VTZ@?6nUk@#M7M+M6J+p3oy(VSKjd;~=-#;?m9oI>ix#4tv48 z^J*fNC%vb^yjZ)zoZL8W20b*q<#-$Lu~!Z~!Cx`?io-`iJiUpZ_fNxNuqFl%>2_|0 zdg3Z49xzh8$uW_Q@r_`{FkbKMVJQIrmjA zv3xWB666vCrzN-p&x88E3iN*)5_oeK&cj8x3V7q>3{K7% z&d@crv{zl|Fi*R8b8_N6K6~favtSR8zYaL@Rz7F&pb3Y^fiL!{72SMSW4!c%Ikn#m z4+D?PJqdbyyr7Ye6d$Py3F&Q7M|euRd^N5 zy$}4cM{lYPJ!0Xb#)p9qAhtaZZr*88V;pFpo0q)cFCJp>pB{O5Fn3v9X%rV9`{i@S zJHPFx^$$S~JUzv-XE?hH=A0Fe$HxI5HP{OL`!4W83|{hI9{TWCGw1j+e(v{fkN&~u zcEHEqDRKF*3gYtDz=dYK>=%FbtKXyG9Pd^EH+Ru_8l9V5e)CX&?*;zjufCvOd^-x} z|2Ly`!W`9=Y;8D@0UI#j9^Kvk+FcT80x{TNJha^|2?aE6feI95K2WPqH(Z}jwUM%~0IH=V-AHWVKN9(`s`56WXr4b(zTZ$22~@3;1KaNgUz{XFxwhlbs7e+7En5A{9>c-ybP zHiJIZ-@k-+;Ue(XJ{r_WK6~($hfZ_&(dNBajqMlTUOn{QZVp1zh-R%^r0T6JK0;;oj>3 zXYp8si(p>;KrCnE6;CW4%VnNU5aY|>Zvfx)gubPFdS}u2Ab9AfM{ha%Ch!zDv2l11 z)JR>$Q!AdU|03{=-?*EfJmz`m?um!jWx(5*7SHsbZ*}#awY!aZtY-Y@)3b0AaCP=W z5P$MILxWs*R%80jeG$Ymj~g!hr61q9<9>JK(OTFdyLtie#Q8*5#;Virj?}8rUCm;0B zB0LH1|M-jFwZI$o)iXR<2K@lz7eB^hp1H&L^430H8H=I5xauG0@DrC;ys_U|Z$984 z|F^*&@GYKL_L&<`@K!6|{N}a1{Nk}QJjMMWn0JO}e3r*PHRCHk`DQ=fV#)bkP}|$V z`Hdi#n&}sL4ujg#AQwIQR1AG)4e%uuijD;OEg}`bA87^o6{Oa6jlR ze9WH*Jo$>ZIQ$didKmtX8lME~xY=Xu?+U(jIAfoAcL8TU^Gco7LLH|jVr&Ncz6kEc zyKofvU|kJx#Gkk3tl=*fFTM|Wst3)UjW8O-m6J|fd4QMm;?TpBb)nei^QUzB~+gz7Aqfk1ZSHF2|n&{kSgzPxOge;pEK| zeDxLI-6b`#R~&hqr;V1$^R_WgcwYr>;*iZ1hGfU`rp5~lSi!|htt68 zci|$?a20qxe)=uMk+1v|mnVMze+=f9LEq4fJOAXs%{pE3=?`3a$tN}90biZJ7WAb2 zqZL1Orxy>kS1m{-EgTKAjZ?E3G z7=GsI;FEgLfWI^9YM+{41oIm~9dNi8c;qo}@AYs$;O3mZ8IN$Ih4(z+6<_2PXL^9= zxc?O7QjcYz^)#G?kL!Nf9FJ(>4Su^}y_cp>#lzDc-npaB%dbYj7e3K^GfYp5Espc_ z*(V1+e+abF?y>hl;DOk<$;Bsejn#+e?u|auSFZ!Fc*+s|iO z-2Fx1ft((C#W%MM_VATnoOvNWfB1YY90qsfLlE13=d2mi{ADob>@v_U7ryq%$2(l* zk#8%wJAMPLIWImP{KAE2;(NqV8{Wt}T-A(k`hjjSCZF#`;Lm=L?=T#PH-Q&tf!22c z2loXhx#cuRhkWwrS^74^^PuNwa);>mSl4H`>j!*sQA2lfH%za6XuJp>+;MS+9zHn3 zUo{aIuWy5Vdh{qbV-HSZ+k@}nYAhB{^zy{wIez@sm%NdG5quwn*FpYA0sm*=Nf67L zSk5~qmUG7Hthf2XFPeSx&wjt3#?$lW)I**4=Dvz`5qRhC$HO27-j~5X=kYPt7x>`y zAn;Cot_5fKXCF>;);`=kp9K4Oe<#q3m){;W))!j=4?Ow27xa}{;-Nord=nhO2BcAdJ4>eE|HBc*eoiF^=ulnF5yb0$)f2?bwnP~uOHOOfsOR*|R%1D@0uJg9xLBk0L2zynHiF-Z zZv!uQhm*4&KJpF+-0*rD?2+Sskk3OC?t07~@x-N@U%b&@YQSIdur~FPhi19dp9c>EPxr&ey}RDC)2~PEI}WGe<9@`AK05HF0atvczPxa^r-u09 z^?LQ@w|d#{tq#1VQ{Sq)c)Z{-F7x+BjrriQpDsM`R5M(}!;c>P@O9q4;bhPFss?X@ z*bi54ec`NH&F{kF#%d}KP58+vw!HSsYt6T}bvf0;*+*ftS##z%ybJQH&$r=K_z?8Y zZs3D+qmiHX>oYq3670oEKdHeY(6-(Kdwq+8-{(Q!h|eSakEa~sdH8S<-Uq#ir@iXS zb3ADF`|b{jxfS%9{YQbmXMuiR;i&F&H~IQ~5L>Wz=;P~Y zI1Bji1aXY@lQ*tE1pCy+{c@M+)(JdLOC~)4VvH8VMdU2Kuif->ZXpk@^k(E zk6(Cf1--Ew@DY2wf7+Oj-vo8w=c|C*zr)Xgr?knBpR+jeX+Jot2KMrs*WaTOZ+a7HC)meC09by}M*T-MpZ4#(410I(q9c^*5wgTP2?BrH1Ly8;-3WT zelyO4dHwRQK#Mtb{`U%Rd&`Ntn)1W9hu6N>^FQqyC$-|09(SMRq3KajYuY~vG}&*> zS?hGu=r@Nhe#yxv{WbpSAF;%6mdEqHYK(*L@5AJ#jZSgJ;=9`IuHN$SV`|3ZS<@40 z&re?RT0Hx4dluARKJn!j`+dNb7xv@HOMBFZH}8Vq6P~y~&eBJf&G|n)HUe?%x+dpE*OL zJ{D7*=vt32hk2e)PBFyC7dN$5n>T^yM*(m5osViY_2U&TApesfA09Ye1$%IpM|^vE zF6KtSQGKUZ4lh3k^3dyyoZ=bl31f9oFZyuDn-0F7g$IGo zZv(z~;GlQ-r!^oBm+?|I z_=`s~?&3cS{M!xu!NI&dw2MpY`+)DA)q8$_=B?j&E&lN1C2h3e<@?V;zS}{K@Z+KW zRU_Ij!sNZtSP$SUHari`oCW^!MQ%0FAL1+m9_H0`dP;6NXyLWJ^wP>75L+y@6kl!B zKyH5X#v6D0$H#Tw#xuS9d%p057d)YtMmh1p7eAbEltWJWTVa>+@fGsEF+V`se|25VyI2|2{lU)IdoC#HCdAm-%4XL6{`aj;*^S-WaHxi=cy zN2mL>8Qed)aMGjva=+9L5Apb8&vwwC#*czL*6_#UyFdfp?jbGK#Zg;%UkCI0OdNY( z2L7wHyg10~S=TO)Gxo|eKFE#RPAqBYYZo!%sTopIXsl zT|A!CB?qrx2Ha?T5%_4YxX$|x6N@Hi+zCBzPAq$z;jeSN6_*y@c#5S4V$k<_;DOv< z2mI;ftG((@hcRBWE;j971@}}QzgdSt&xp%!dBh)Hc#4ISIezGGdU8pKk|<8T^yi;p?F@T3V3{Y%f(+*p0>rA>Z!MNEEJ6VIAGo@+s0 z-Uv4X9&+3Yw*yXhgF4<1d{L7>2Jv_)$3KHwEW%dU3A)P+~>gOr3SR~>>D&!C zIxoNT-q*vNr+s_%Efw-4eQC3fo8L?O zX~7rdR;!nRKm5N8YQ+zG^^7}fj&@@jJann47;-!7j+l3rcjo1FUT@PbH@jVt`CkI^0+z9F+9!|WZNe_G%tc#^*ehT*BFAgos zK%;$fJP-Es*&cB(!tlZITEOWfs10B52QlQ=Q`du(S%Ne|l&C|@+sh4>6s)ahL zi+p;Xe|*t5`Wn=a7B!>KxA^Nd)4z1ly&p!mT;>kKc94rMd*pC#eXRGad+ZmJzs}tV zYGwT@+zRd^EqtKyZg9?C>$sXzd$H7%_nwX591i0h-aK<>c+CsH>&~ga-%7R8Q)>7B zf;+z%`12spyBqex<3PLmJqv-f;L`? zNiV+6(0dr@nQuCA700>pk0uW<#Gq4bdDVnodvO&{&D<0HLK|H?Q!6>_qX*8z$?7eq zT=Luhw}9ijfG>}DZI7J1+z#X0S!3~z15LP!scz==uvok^FD9<~#~QxQ*-r;e*2Fsv z*8UL8SyvBnc_EiKKIU+9PE2_o1$*2L9(cPy*5sSN7w!$6mqDHB7h@w_5B3||kH<-n zi!S`=(u+LOe`-&obLurZ`HL$J=AE}@pLk-6$8US(;|K3}FTS%p=Y_n^)9ha0g$JGH z*Dj$!#6zT(_>=s=vKHL_;WYlaX&cktX{@z{jRwOi?9{kgWa$fcSf+3NOQ(KpXAq$q&6v7ti=khj051R^K?f z8#M5d7qky=KH@1R{nl~!7DxSP!cYA3;5USiVmK?OJoeJ|B`?F6X@I#LfISfj|8C zDJ+9|G2~L0`P;-7F=(^ytbD#N!=2UJ8o$JmOI$hJ--B=w>^Te0>pQ*~t37V^iZ>d? z&>w#fVyPP~_N$4y+iwk*Sy%75S90)^w`xiYO*qmoh927r&e1>otS`fjATJH0?{Q=K z#IxTWGUvQ==H+-7oO2)5XS9ilkGFMxJ`e6Vy}X#c_~}(~<#dK_Z_oW8{+EGPziG~F zh6h34=`;Js7qzlSo!wQs_;@|I!}RKXJdDjh3OE4H&U)yU2Y-9T^3ZE+?lAC!SK@f+ zAKmJN6OMWTC(lOUiTu{wo!NW4F(2f(8}$8nAt(RjqgieF!&@GG{Ch~}BJgZ0(DN{; z>t4W_PG0gwo%IyYUWQkJFYfOzfnGk+r-$DKp6Pq%U^{#hJhc1X4~sCh_ggKe-#Y6! z+2byq2RZrjX%L(4(N3%VzInp~y!hcfZ^XD3oL5h=oUwkk`qo>^K*PkgpHFjMjX-^H z*MsWFH~#2f@lOIjoU_k<+{RBn9)%CVe!128dT?%XnA-^aQFB`9;Sui6`qnFQ$t#DR zl+#$QZ-RP_-|BZ2tUGrQY|c#gX_ zea@-pzk;*gJfRI|cTvtqLBI1`9{YUr!ox4ViuE+ehnsUW;bYII!Q+0@?!0^a@g3f5 z?m-YsE@#z?}P{@HD8zZ$w{ z*7-INgEsT_@J7FEg`2^=9QHU*$CJR{e+HWH63?E!@L3R-2ixIw5X&CV@YfIa@mUP} z9|ZSZPO;?T!K>gdsM%FeE5Os9UjrX_`XcC=WjGEvzYp#vU!0>+E?$X8Cp`y&PI35o zclGVe<3Qu5fo^%G-wzw}&e^9yEuG<$KH-Tuy<#1|%i!+eaulrV*@r8f_tqP+58meGFh@82>LI7OKL`Hfr%%N2*oy~_c=5>|bNpI{H^F(H@f}xV zyzsL=aqLrLd5vF&Z-Sbr4gKnh?~TB#J3(BYKMVHrTOZ&hhu;}7aeN(|7f+w@V&cm4 zA&6lP2eHMSSbUwc!`mBIwYC05a8^I63B3;k4&v$;amCbA_Tp?FS25Ic;yeFouur^y z1zdj$8{r^WQ!D&EYPc2reta1`*8>kc+ksAfI(hWvouG#Jc+^J@{VE^7JfDP{L2Umg znQt+9&LbSf!%NQ?d*j82$;-Fh@HnUqPk6zzoxm&ebo<7S7MjNcT&(ffSP$w)+?PR) zCxLDnoW;d?o?6!%&Ta-?sJmL=P3Mj9GKeWZPwbUv`plf)VYT-=bSt<2#aLVk0&@xkMqoOp|4&obbLkC^sc3-YNEE^0k@ zYiBh+>rIKU&)uZoKDzkIKXZH)!=A0c2mY!#uW;3ue4**n)i>|dg}3xO2V%&>12wf~yc^U` z-&^O&_#v(}Uh93lu2y5cYQI>iS>xA%1|HxgC+>?N$LMnpp02*dq?vxt-7x;? z8S^~C-Fdp~A3yoPo6E2l^x~gZW3@Cs4`Til@cJa+^>N>+^)k?`-n`+{hk&nsaMqf< zVmqTx^$1RffhP0v+AojZezW?f(eo_m5q{&Qmwe+!i~T&l9eBwLXU+mX?!2Cm*PR>w z&X^OEhOgG%%f|TWJwDQAkGy(IJZtJ@-}~@u7+ub&gWPy-2Hc!^9rQJ?#c^NMP#yFZ zpJ?QjI5f`O#21g}?*i|{5`$)O=)4|i5@Q+g^mxl}jea#UH{Z_5|2*iye}~Dj-*_i@ z`0dQZ)$4ki7h=G8)bO|GVgK4k&vBrG=Ffw3w0eul8?{x#JHcK(Ag|bB={4WeYwjXF z_Ai1vq1V;Ses$!Pn)8>oWjGDaxF3242Wx8O;q{F`KR-Qc&lgO)t^Tn z-=W>+XcZ5)$AM0{X>wQn#_&L&(&4uTZ@g&2=|ABp90%I@MTruh0WEaU;Lg%Hzi;Y8CqDR^m)rMYkV9@B@n7HI1AK6H5w5Mq_K2wlYPSfRffi@< zh*&gu^Tpg&co^j33l8=?2+s1y`gRc4opAncSOk3~w=?{wmlkKlcZbC`J_%}SOtUzA z`Fo(rBbN2^pw@gh$5+~Rg1(YVU3o4hZ|?>DtjFx#2(lr86vq!iK3wp%&VO~chrY9bn|st=I{u4YnTm);tPn<_%!{6?TeRAkc zHJDg*+6#|^-vNA1f;D{X{c-hejLWk?AAa)TtZvS^TMq-?Vu+`g#8?I%SQksLIBV=J zw%-ALH@zpmesZVqS5s@^*^j$1Po@WOke_D$=nvlU2WK8m9dKL(T%3{5oSs!j-!rGT z%&Rl4?iMb3#NE3W+*iF~&0d`8!_k`;IGCTiu097rZE%=1oaxi!d~sHuXTf9t-QeuY zVEr)Ik28PdHI|zXa{E?4bG(p~j-%j_S1jjgRa^ebw;i0r!yXeYI_SMG1K#qfwVL}DTRqLwWR8Ed^Ws6^o7nd7>3oHQ)!VwfTS0y7 zS?_BednUFx^xg~)1D;}7_vRaa%re8k1_`A;S zsBh=+I}d!oi8ni8Kj=%3*z)0^x7-mi7hyZ-!-?ap*m&uGwJ_(57~;}{r&yC0XSLBw zTfsiu^@LjZouh$Y{N@vnNo!;SDTs2l&}!F%|5F9Lr25Qhe1{M<+9{BF>E z6g+&zRs374x3N8=$M^hx@(*Vmy>Z_Pc&Uv$Du-BV?wNi3a85kF=ofqJ<+-~i4&HhP z2mK;9?lkH{zZ>+qN4(TKn*ompfloVOy@%9FA033Lr@nn2crd-CCVXA*v8nrdug!1C zvb~3apXz=bP6F?y57p{zco%rB7W!xUQJ?W`{XU4PXLy8<-^A?|_(w0__(2CxctO8? zxbP2`MR0}(KLmPsCBC;>TnoIA4@Z74f|&d_&l7t*8$m6dof@v!k0v#o`l%g`YPPPG z4jiU#yN&n4@6~F(PPovrUX#PV@ui&x{?Lm1BJjX(wEk3QF??SO9&f!T#!=8?#_rGP z<~LtQKVA0moS(kMrHOu=mw_L=ur8;5;~#F~dE~%B?>-IgvNg4sUXb4&XT|s=&}lsP z?)_>`U3kHFwV&SR$@H&vJ)&RrsWm+5cZM!nd57z8{=6|Ba^DVWkE^qDUq7z(qP6{S z5S|2mi?`p6?}K{&9Q68Oz~MNYhO_WCtly!_zWE^@UO3{<6MgA-VJkcgJk?kFWf`sl zJv8%IKIiE%XN^X+H;=b(kZTdt%bFg+Q(uoK>gC&-x~TbsupMX@A6Mt)=YfaU_{%|q z8jioyL-y+%U?6I!8ay zryul!KHwD}HUlnntF?XVDi&|~pf{(N8veGI4{9TZJ$hMf_$#(YO!4d$>t5g)?{@<~ z?{`5i-ts_9TqgeH)hoPv6ZGG9uqF;K?gYB+ao_ZcyZ1iuj7I%3_2Y@$ z&f(>epKti^6K|f+{pHVTI16%#r`9+Cf6U42j^p|+;Nm{x`%AFzPl0!8z$fQ%qwOL* zTz$)_9(-|LOn2!<5Q9(RIV+xAIN(3OJ-F~e4twe$WK+4M$$C`$4~0?(g-Wb~LLije449^YR$$BWrhp zSio-_{mk2&3a>?+4zg+1zQde9Oak-}Z?i zzuaPr=RUp+qkFjF^{?RUz2KbjcHkRc&eP}7li(b`#kIyeb*Jq#;4BV)eDp0Audjz& zK@5B5F00LU!2d;12XURXR~)>(>5}VZP&3@cvftT@z&l>aWu8aga(JHvarjKP*zWDC zV2%E>AeLHP3$&Z}e~DfzkndlC7Jl+pY!4mI(mmK+8YFBB;lA!G3Wr0$tX5 zwhU^`E4}+B@cT0GP<(Kn-*WMTcI)b+26CvoF^#;&$9`UVtE;>R!5%)VsX5y4a6frw z&73^)S>rjL_HP7pwBq+eP(OL-Qv>_t5mOJ_3wT)PrS~#8gQr}#gS<4Tf%+NSe-Y%f zuJ#8(Og_H}`a>=7KMZ)D1YY4QwjNe@^~THF`K@5DK5;kIKzuzYE-lW|`b+pS;A|hy za5@fmf;-{C6^E$-ogeo!UTP)B{V+YGw&Kt(wtVWVX4CgP*84nNgv~(5gFy3ca1Ydt zk52-vG}5Byz6-AcPk43|UI$uus`qeqN9d97da#Gr?jJw!#g7j0oE1~w(5O!0h%b&_ z!9yP7=`(ZV^~98eHk$Ak3-2ETzuyKi)bBi)vqyg4csWah8sM!jc_7Djph2B*be?zX zT-4neJaJGL`Sl1*;``>Yx4ib_#0z}f5%Xf$Z~a}67cc$Ezn!oj)Wg}+a2E7~d0dZ! zN3QF^Jm2ut)Al&W8#yk*`|u&Ci9Qiy>T9enYBgu=l^0(<#xD;o^6Evt>qpNtk;&uJg}cGUOWuW zsrz0S9_q>0Z-ba~SMbqKVJQS11 z!`p8KJ~snB`iTzw@fOG9{PSS#vw-KPt1+FMVPgEX@pvGoTCD4T*|)nVHqIZ{QZ731 z$B8zv?SBx|PfR_c#`fYSj(dJ9;AoAW?eJx=A5R+iVqGjXb{22v-RH&X{rBGZ%FQ!p z+y$D&Ft$fddF;VWee{I5ddIz)UgG)s_uIb3aF$Q{X~q-(T65y_lON*hL1)F4!@bkz zG&$#v@x~i)me-j#!G8Jlr2FGrd}HVMpqDNJ{}w@fz~Lm^4({WffR7x9!TDuy_GJ)z zc;m}Qdgu_(IxhCg%MY<|(VudO>8|j{9r3MZ^vVxn3^(_ikMzo8pWNnvZr{#`g_E&+ zDTdy3Ms6{9=NvEO7ymLiEBF3tESGy`O&s?`47qTZM{ak3cJs^N%&j1YdYh;3diX;y zXV2rnFFu*KN1yBY`8|+xCwRnEBOJuAX3kmn$=$&dZ@KjVelw>A&dT}sfHxiZ9R!~7 z>LM(IbMn|Lj~wotyM@30cXqVehc~{?@luYBfX{Z=58|o;o$6wqZulfPkJmDs2mah# zz2y-1>%a@O6Q6h1?Ddw1c4K#nCphs6Z#l)W-*3~&3iO$GR^9CtgT|LZuD#$sT?M~I ze5UhFI1KL3-1c)3bcB&)YhVV$s3tiJ|5< zg0nnVTW9REMju^znNRZTfv*CO)j>Y{CnoOrY=sYDJNR9_9<0f)F5djKmmk*TQ3Jlv zZ(Y6l_bT9N|1$7j41Ixvy4(t12Jwcgn%GC5c{*sMm3H~mS+386ytFv;I1D#lzYEUM zP4n!*8#jBLoqg8nH@_S12fafJZ{<}Rxy6%9A2^2>ukGUp-FnEGm%;k&K*N*Zy#2m; zf{*)xiyZqw9NO*S6)$MkBcAa@?SBe*9j@l}vwZm63HlUYJn&Tm9L3e+&x3q^hkQGa z2TsdCi*tGeA8&V!x8n;P&g)(Cys?j$&RF9q9b(afle~HccU*by?VLD^z#l!t7w7Pm z@0-96UeY5TKgQSf`r6N;rkgoLKnKd>*Xh=#7^c)&URmVteTOBIs+qNvq%5 zr$OEQ-fjjO#5oPm0`B}2PflKmKYF(t({&Q$bKV(aHFfXg{!8G2c`pfgS?|2^v zbrXvQdk(@yz!MjGJ_L{WxVTTl?`C7aqx9-4n#AYP^wDsV2OnPOEjdTGo*M0&jkkiH zdl>k(8}`C}cpUK5i+IxTO>k#lgjeB*Fu%u#jp?A*eVF@1m;5~R{1kAYMGn2g3p}iO z^Hff~;u(*v^Wt_`1ibN+Lp+?+i5EQLqj`@!U=3IE_TZ^b*Mc>DrRLVmdp-#_0$=HK zmUj7ZbnbeX8i^(Mc~Gw}!XoHvKFC4)%WyAjhJ)2N4S3SQN4b2<@4PebgY)LxE6;!H z8GhkG)9>|-p26w=(l_qPBH+BGHPU zdGq}Bs1+Yi!hXP8%tf%r@9_K%@JW2N{W>@U`a|9MH0$QXb{<#zX?4HV6+fQi&GW-> zGtlmSil;Z#NM3vR&SQRxMW=f$77h51$7&$X)ZlI7i@^I2K|Xsv4fKM0fsb0@EuOdd zbjoMV86NV|nw;|EV*h!-Q>|&b5uDiyJAn>!YT_;D?3uas`SG8pV%WnUd+?dJdB0O9 z!59Z?uL8a_;~^fd*3?LToPAF&^%7gJ*^BdN8?EXikF`5%>wIw*5Bc0bXYrpk=eC1> z(G$l34}Rj|%)}7KSbyQE&(&h$>PPp+d0Oq+3v!GeJe=p7x`?M253_ z&?mkZVf^>~_tm%l6q^)gfVzqyS-^D^ka`+>)Fi7S^k9^#H?+l~Jb@R&ZkT&?5CcX!_! z@96RS!2|KtK+MkqF1!L7PJ%UKv7DiuPv(FIHQ*)CVx1>AZw2e$1hM!-hqHQAz5WvD z5zBqTk0u_PAFlin`zkC}W4!Rii#B@YH6Hx2v2)^k^FqDV#W`HzgdeB!Y+ z^>J_PpBjsO5}dJzW*X=+7RQ>idQ^>^Jr17nXW5u{{JbA<$4`85)NNwXycHe>{1*Xl z`=-D6p+8Q7eE9JAQ8*8B@sp1{b)G-Gr`KLQ^txW9Vd^qlGu-K-5g#$^=gagEANVtTX?4c8eI9R|J^DjFzjt!j@7tqhzUAQe zaPaMM7mU5X4(@{7_R__3d1ehekeuJa~B? z1iZcs>VPMW-tsSlHT5*dKe@%j#a`>yo(4R{mY)aC>8HJ5??Ipk5ApTRKZE^r;IV${;E7vlJc@@r9==GZx8tIdZjyqvu z-)ro*fKK&MPyXYgmNelv`SrtS;x{h40T*@WiC*TP->7eb8omhb4i9ktIr!Z;415@G z_^Ix^r`uUr1iA6xuQhr2#xL4&8863!&zhszejf1eec+kjreA~n8{vQQiAU?+@MY@o zd)i0$|E8JebgKnV@K^?1p9g(Ht9dzRtPj*+?jDVNq#akk7mHwz`q7FLUwJiszz_X3 zHFZ{Qad6Xvi=ZYr^3h#!W_sgk<7+|f?Ryi%mGd;H1HaY!D0~QdL>}j8Gj^}!m_Af< zF+A4g#NU1#XnY;ya?Uz!a`8!B=}|Xz)hEYcH;f1F(0;(#UT=LM-rK-4`DSkV$h!We z$NFBtQ7rjS1Ko0}qw#}a|NSs^!-a0%$d8wPa8CSXa2I*TD|?*Z4EFMTVp+#QE^{~z z2Rfeyn#4W~)_8WX`u4_E4tES+F}DK0X}8C`8i^yHH($kn7u4uH$j=+zJ7eD5_hBzM z=Ua_u&D`|1yyDAyb2X*|C*T`z#N#(VXnGyQ-3V9VNf6Vwz7}s{+pkwH0{+%LV(F`O zZ*j(TGkg~Kw7vTNcWU{8zYby241VP9PfiW@?r(GpoeaI z=nzYOvH2o5O}w^ewDHn8xgLer!Ecv+>W2ruZv+3W`KF)0_&p5b+fOS$-UR#cejfN@ z{UFfitUhv%7Tok9Ue@(6-n_SdcQuw*FY)g4V16Tr^)z^VtAW_ltM0Y^`sFN$aV^MW z%rEj@q!DQ3rJlkAFIvw6FU{Mpr)a@nJo@O>*Y1~E%3&Q}G2MAN=+p|5YR=J3J8gNt19>a&M|PvYW>n|1o+ zrk!5-cEd8bhd8Orji4UhVu_>Hi+~GXtcmHl9(>cd9q=(;1o@AG8tH`x!DEbv`DGAa z4BE}V4vV0EZ-TXJLA^cp=qcZHxOe!dA3oxo2fFsdaj?d7k9$B1Z)oQ2R#3OS;I2Fl zPr}Qf9{NlFx(m)PgSaO_uFIfia(j3<`8FCmtB2IdJnedvKIfekgLl@%6>GlfQ`dQ` zm%jFmqu6|;$(%fT((ji$wFut@ad~G>etO&~cMdN#Y}jK#M{FVb+h8q;qNUie-Q4}*TZA0`exJe!`S z%RY6{qw+4pRnV_@f|xJD^S~2&#n4CQ=@3snW{iv8^c#S~*Fk^c@ApG5^6_zS*1EUY zYVu{U&cES~r~bxEd_K;a_qyMD8ejF{`Hg^w+PHuG5t~=^iK#|%>l++B)_G!&UJ^$= z`KU)|x*hN}J_zb8o-<${?)dsWurAkT5Ysn~-X44Su@(5I#;*gu&d`AW_(abSVK`em z3ZCttW@348)Whzt`pJbO?~Ki>gI;id>2Ow#y+DJS=|8o98N|ayZPbis%b?ckd=;in zyi*r_g0r5XM_jSYi#2g3#`LgS{5|MP`R%8hH&X{X_{J-Gt$EbQyc)d;*2HuEZ7@f( zxhFvm+MQDm=WrKeylVK{xAmE$3m4~ke;R1yA&+R0Lodn8pPONwj~?W=7`(%68PrCf z@L>Ei_mANBLXLOAIedBaeHcyhSkrfEPN!NrBL=;PVd}+?-S9ZrdmQk^TYUSE!Xk)6 z6F&OgeX}Ny`)Ce7v2d_&KZq#~u6SI82Z0CrP!Bri*$C>PX6AA5@KkN|jUJQpdQf|{ z`64*SBl+!B8}U8%sjE2d!)B1fJkP9&gNNTfJoK(LT-|3ePXn#$kK1>vF-_AS^PV1= zyM~{-&Yc^-^a0NNb=UOJG8_hVyb5ya3w%BcG~l%xc&v8M0?pGC(?9ea1$Dq*ud46V zLLDvx&3A(}oW$iZZ}FnXV~;t0Ed!pu^%P%u3|rw*z!yIn%=5uR%XW~(cPp2L< z@Z8tT#MAGCI?1o!)M^>$@yuJLm&JhOs+*^ zo{Rl7@E-TA;H-UmfM>u_T)9zP7c5^wVkYyeA${4+9-)A&0YiXBq6{gP6P*%ihm}SmLX# z8sP0WKpb_~Lwwi&i=emE*;()HK<`m_6!gKHV9kBNBijJkW3%%*(5% z#OJHH*6g=NAD-@|JIz0w?N@hua9su(<;PFn@j*TsK@Pm#3wzbgx;sUKymXw0r$KIf z{f*JANYd_9e)HMO&kZ}>Xvci?%DQ-1H^?wm6l!6UD++}7kD zKW;bXy|`+}Z@&TZ;VdTq^bybW9`EtepYq`D_lDj@*bH>ws200nFYw`UcoO)e7B9kh z`l_*-`7JsON8xoi4sQZ3eEB7uhIip2sEfP6AKbk0+YV}B4M+6{{?Q^1e!Rn3JpQQ5 zPvKhNft&BC*G=|{*d21UgC=LVKw$GKCWt`4r2PY=DiH!*=J78 zeXEJLyeEMVdqEuj@|Bk~;4pO;Lk-ozUiHUmH|z&K%-eZ=KH2 zTMzGf;y#FD55M_~r});lf|&YgJBYJhJGpr;o_XHLYs>?@<;6#T(>>hAuhFPK#vk_> zH+)aSS$G@Xg^Tb$sF#>($Sb}q!(p&bE;;C;0WaF*nfUreKiJP-YqtVV#MX~I(3|3! zOQm^=StbI$)7PQsgT9_YnM9OuLwjdvSQ9KB3~IrTBG zZgi`a-x6cm{uadJ2TVWlgEl(lWDp>QI$K%g~n12YvZ`s(IxAl#1Kk!PA z>0Rsmb7p=cHXHMafBJ{V#`MZZgWpDDJ`BI>jcGav;;NPJ{Xp~6KnI?_w^whqvRBSU z&|mn{i0>lkSNY`|uP+)8KmEzW;leBX#TlLU_!dhorq;Of%^BQ$tDn94fIoDY=iT*y zzkKvu2E1Sy+#O@}c@b_0p3zB@c;fq=(Jv1IkLdS|Kl0nhXPnKeg{hXoGUf+1~&wkwWgYT~cy}JQNx@qOH-TJJ#@^1tl z=mm4m;HXA;Onh;8Fn3EYi-kL_Vp&&9dBnw^S0~{tsGVBSdN1(n<67=D$LA-37w(zf z(KmSVS`2yV5?_C82WRdFXVh1%jCn1eUV9ttp~E@8;_Tu7^n}{+g5P*70}g!k&?mRN z;%)|Y`7T(KUp#Mh5eskLnx{ow#}nS+xCnBpubM8yVK@rMfsZtZM?X&H;?6E9h?LG1UTczTl#4GCE(y2pT_~`Pl7zg>VTv3^5_9M)kQpOe++80 z6&?mW#5xF10&dfj;_yIi=;kMX`Qt8$M}ucPw7%YZxYE81?uqZu!ZP42rr48mW8ugEVhudf1scx_(1?Qj@y#gCUW{?J%G zJbGy>ykEV=(?9C1$8prlYHP1NIPut+W`4;bK3(*wsWFap@nJ7K3b=`dgE76wfkye9 zzYKbUk2K*Tm-%ObC*J&2gZ~7v>@(+_doDJ8_g7qj;ZeXtP1H>P z@O~NW=e0W6Z=XKGSq?n##QR#f5pD%K<;0JF@|jl$TI}bIx$&mqZ{Kv9r^nm*Wx#>A zzXm=ykB9o|FMf}H-Z-b8&e3kaIdSzDT{lZ3H{O0HoHx%SbBmzI z`N9J+X_`Ia>03PXk390@#8Wj=6ML)~^G^(C?e$m_14q8lL$f*0@WFwH_8F^#n7px< zE_K3lbX((x9<=8{xE;*>JK&+t%OEBXtUJFM#K(yy>*k!Zz6{Q*m3?&Jhr4e&w!-A4 zL+|=F##P4ka-Ihq)Oah*8Fl_C;H@vI! za|d{ZgIZq%Jlz4lZ3O*kjvn<_7kp{L-|v9Eyb#~II?2m#Z|m|KJ8O)uZ!z%0-MqbY z4`&+XcmH{>&*XB>UgzkwkH5UYn?CW?L@qJ-b}xwIyk4-6uktOz@DLAwG4+ccmS-8% zT8((ZXLg?|FNRQyS8E`TuA71$2g2S8emw*GE zbl|WXz7J}tu6o$|XJOq3vF%;g%fn|uo)=-X({UWc$JZJBad;nij2ABZ;ZdMX4E6gp zoCNz%1OD%V2e0WJf9GhzOB{UE94GxI9&JAa_m9S9I12cQeHPvZ@#)8p2jdg2^2xOf zYM~DnK^~897_E!OJlqKK>P6?&N?diPlV5(H)Z)|NUg*2~0VjFY(pwBV?2${)$gvZ| z_BhMy&jatorr)gRpZg<2v8W!Oqd=kuCmtzt1ko@?_w+uW0=RXAZVHxPheJkKj zpS--M5l8&_W$$Si{&dQ39ba74LA-0>W)M%0%r}j90xss=h25|hrdN47eM-CBH2oZo z!teFy`{wDo3|B!uxtBp-9ft7}Kfl}VlZOUA(L6rT_B8NoyrOXt#KB`T=-tnQhklwp zetRaTe00k>byRoy#pi+jxa(&+ZUyz@!;SDL=!=k0ag-Z}4g*<;?>>%ltDrk2jqBp*(^pxfE;RX(-Y2~%&nc&&$?glB=~ z`R!DD_uJnAJf{!*X8k9q;WE&56pjO5-UPjR7S6-l@Ge{g-du*OfTudsMH3CU8E=L0 z#yY*rz&pO#>wdfr>Pxry`Tl;Q>!{4-9r_UOX za9RYh#xq`6_dE$W%Ws|Mp4l_{7+lfL~< zYy>fRXD^TC z$5(!RCXSju3-W&+Xy-Z2;^3n{#)HwKmSW+|2RsjhdumQTy(W*}6u(pS-w5fJaX{cep+$;Cg6gfM=rc+)Cc-P zuZXh<)*r0iy?)DUPJi(UM{jx5#Cx3^pK<>osE-_rK;LDM7auzH)^XswI(-Q0M>h}D z>LC0((6S7t0pInUzCGi$Ja@yrAntD9ncC=!sV{Hv)F(WnQD3U5-%}4BJbE4I#t$E! zn8V|Kkk6ek=UE0i=*CMt>$K_80c19dhhhldyQTi~AByI6q_ z;)*A>`Y!`d`EPzR(1RbIV)H|My0^mB>f1T{cq8sk;IDa5XF2o}ZTufj*BU#!2x6w*F3G(?6t1O{Gfa4>W&-)-r?{hsM}$99e7~g{c~^Lgiiy_ z^r)4*{FhIU`1UxbM!XpP;(Zow1)kptbjy#+{qRMQ4_AKuIjEuhYQR5tMNaj0XVhyE zHiMd|)5EYGb_31)Ru6jVd>VMN-t({e{yxy5&kw^<;00au)6Anet8cAc20ZY!A4j^a zc`sJ)iNhOwy{+?rc3SDwEuxxx7a5@hBIty8`8JoKw)W#Wm<gJB(ekV+Pob>{&)+YwN$3Z`c$9J_5i)SC#9zPoWE{Nlfd=>l#@NyAw z#(g{Ng!LZavHJ69z3%SQ)c${~`+6Vi;j2KG^QS={;v*Li@YS>YyBpjU_nHUp+h`q6 z@blP9n};{AgWjRpqZS*%d9nHRY4|-{yJ zete&%FP5D>40?k;y)eDVOJ3`px8YsTC+~y)x(vLZy6b=a!8da}KiZL`rP{@s2M+SQs?QRKR3pIC)hI@ zk=*v%=PvSDEj@D4O9NfFj4$%zhWE86{d%e8ov&D=}ou_dKK@>z<>PI0&hHVoNt=s z^Ng-#WAnJ^89MRiwX?o?=r>?IyWaR?&&DraO@BMj7v9rO_jVZVbmLDu-Ez=Qvwp$n z=kQB73~$4`AeXpw;e;Q~>c;19gLCSxHXe26y&BmouZIsbx}(3>GxmzL*&|qK(h;O?{@vc#50#sij!FmlJ1P@RQSC zeZ*5bd4q>qx?ec@cE8taz{j(|`}aXEXQv_C*fJ3S6_`c zywsyV1)lOrpDu&?9fjj?8pgkOjV}Tp`EeC!r2|(U>PvpPJ3Qc(b7bKar5v?t>~uXK@fWxJd>aQ;;(zFZesG0hj{OVkA37J z|HiwIefz)orDu5ddBE|}YK#-!bWE+)*&h7(NH2|i9zEjtE#VKnzU`O8vk}(!`o13K z?(zAvu{+%59{^+y}R>&=>dN6 zi8tzwx4Y?g?ME8PJaB&9hv~KX{rWvG z@c2DH^e#{Ih5n?&9ifF!bJyvbJEQg=`!>G=%hu?B9r$<>-URhL3+Lf&co!}LpFV`k zpr3f4PnWAVKX|Yga1d7=#o`bD#xJ@(nlz%ShY8hEXC`WM&J;Bi*`>GPe&e6XKS z;)wBUaF>pPJnF=EeyX{*eQKtE^=gvNFjIVuSc}p`-FM@Tk#8eM)^s_furhyl~7GHgMj*D~Z`zq))u@8cE9^VV%*^8rlzX^X8~{Z#P=jz1o?URMR51+69@Dnj_N6oxZ;UzjUIXB)a&lU zSc>B4&%)PyekoCW=h)5P+o z!~Dab7xdm}k=xpEa+W`OZ*w(|r@C1a!=0sPYOIbpxUcxqtFPqM)6U?}bC3R527N}i zT2IYrQV)OIeh$v*0X5JEe0dwhqMhfX;d*1aXg0@x+UVU5_|th9+*LJ`U#-REjs08U zalnI5?$chduBV;6B#1%3`iY67F)w}%bl?N>^2s_c^pf9RetaFi4A#Vxga6_i z^VIu1sFU2d;3hV$ufy};yl?yUotWOd9?x*)h4a=P1?zaLfqCb6fv4V-+xqBn-W)D+ zseyj+c2+z!#M2&r;N`qMJmtH+w95rNS_bFj{W&;`GvC$H89Di=C-sk*z;kQ**thzt zrCeguvk3av`f*SLF>u4d8$WSqcE;SRupQLWcq{NveBPV$Ry*@JJMUh&`?&vqM(c#R ziYw{%ejsDaWJ5na_;C;JBMBh{Vlsnik4Gb#7{33fx&I$=^1ce9yYE{OE2?T&?##7T z=01{l{?e?j9C+Lh4}!XMFN4{~lW)$S8C(Q&;SA`tJtx5|-3>h90Zw?RXWeB+_+T$i z=E_Aozr{TZJjEZ+nHe+kGU&ndMqiGCIna}3I1d*AH$A|QXZ*wKN|4t&&MvsUAJo(v-q6L9jj#+nz>{uq-qrL>10H6C zUwpAweR;129B|h+=h(UMO&(r*w#N+Zt)8vvH{IsbGmpgJf*TLib6*79d9V!p&|7<- z1RTc;F}UI(#y%H4*2UsBd(75az}?=_Y8Lpf*Y@I~*L3oHGn@sNI$s8U@lj3t7lE#8 zLH@@u~1Fw0- z4?fWHE<6l!PCo171+6@wSIxcEyK`*acRkW~x$srfJh*rvpEcZY#Aj;t_$kk#yWb;q zogI6`@kt#XsPQ52jYoROXL_8=Mc}bnwMVRTEazsB9}lyn1}%8Z`LV}$-dHocbhs~q zb-Dc(xMnt=_Kb%*>Z>{Wq@}QyZO=sTAu|qXrcvYzR``NI&)rVncojR<*~KltUitA!eyTeCu{P42+Ob)=)WDT zxq~=)^%G}vXl*yhJ$mI7qb3b_`tClzDYTg3(Lq0*a+oC=Uj_Tsa;IfK@cKBoW_F(T zY>(da;V4`LvG&QW1|NOL9qDp1%b%NHb5=!o@qD+rjSzz1DD|Ycr_N z8#U=!gyB4XngKrPlQd1d5h&3O4H6!ZS`%8Eq#tT~P9iDtR4LA*d+RT97 z`8Ma*Sy9uP9QJrt2dwc}%t4?<9bWREx6cFL-h_97$NJ{?k?&%*!%pCr@Al}!?4#+| zVBgzt63)VTphsSsKZL!&qZ>ir4g=mBLCx0zZ{Nk5iSGg(^3otqFU^N_-|@K{)U+RO zp7Eb=^7&4?b82qnF+(`(hj_mgdc|XxJ&Rz?Z0fDM9Ih{dJmS9z_XDqRKMpQl(<;_p z^I#2k*VjQ_wd9@I!3F>Ip#Esam51u;5pDAE1_w3xMh|azOZQgT4iCbkz?aKC{h@b0 z>VbLS(e##{=_xMi==n0>L-S#f9Du|WCMgN^oPye5v_D(*W%<1&tqUZO4-iJXR8h;N*!R+x+?{WXQ zdY0!r=#QRm1s=KNvOayV&S(4eaVN~o-Rs$YzT4w6zv8ck{|)%jrEhvPp78ui;GvoQ zLwFW0`y>aicuRvl^7F_X$^Rtq_`6`H#mRxMy81c4RdnM{qncuAn6sdk`JhF9IdNVO zxZ0=hYe8+#czfp)9%e|KXI#N|ci!ol84yDU?|4M3I2xaYgMf!Q!3%f@Ps5`y9Octz zoHm2~_%6b5v410|;rDJC?As0YS@%ruS#Smx!G0d&gQMRgv1V+}oOOEXF?Z$!Kl#j{ z^UHgE;lFrud@n2m9=C%So?3U&p@)yddEoJS&{KZu&&RM8e0vzw`^u-`jgxSR&w zs5yLiK7GF5vp($wnsCJp53}l0!{wV8++5;#!=vew-x~g^`6}oWPjR+}Gks=|Ua{su z4I1cN1hwU#{*D%TaOOFk_Kq%VeDPg=Tz(EblS{woQqvsroiF-*Er@ZJU2@_;pMFja zGf0!%Vx0%`brP)U)kXM6cpGrBUp+ba#|NI9Iq=*0Ca6WzS@<%@XAe#GsXILI)<-bw zvp#Vft+{x@b6k%Df7N~+^lUR&TMuIKQ14yvJ7S+cs$UL5#iO?|%d zkVop!=DRu8AH7i9{2T=S>xo)&U0*%Z{ilF0FL}#PJk{YbK629LEaJ{T{^I>(pv^q+ zn}%gL4Em&hd^g+rNB@WLF*qyc&R)-pFnnpn!wk5V0Uvj3Yk^)K;Li_UIiS&V*O4S3Ojr<(GLQ$wEVGp)GNP6PeJnP%7M z#$TM8=1T9qUkPei=Yd){SjW?Ef=gZR4}$ONh|#MZq1!uob?u8T;;-nhu+oHGdaxyKIX`N?|%vSKMP{;pmEOHqUYVf3*Q!j=e)pG z9lUk|o@ViRaCYY0I5Yd9u;a%TFgCY1b#`%?!=_{Ilmp&=0lsWIO1=Xyy-(&E)9+cfTKWIg5MY zKl$;#Zyy8x`0|AAgJ6!$p}snAIGeexAUFPI&YbgAJgxN6qbKx>qfu__bn$~1Jaeay zpLpu0UfJVX1UY$*r#Qa2>ld%D1U;1(f4<-)elOr6{xFE4=QLQqAAGaE6?RtddZXX? z@sUqv0T1=)pvRhbeHTBVo92zMAAI+$rkN2pz1L%BZW%rV^Y=ygck`uBW{D?ih&4BI z^M#(zXU4qzw^>nJPw89+p6v&Iy$vVfEL=WEW(QZk(4)3z{+LI5*MbYz^}qwykHH?- z=rjxV>H~eVm$#n9`L0)d(Qi5Q*xosN{4s}^Xn)ahwuf(bSWw1|wkVGG;=`vW!EE4u z6wI(3wB1}iFS_GoM&!lCcV|Rj<@L@dyws=J9=)bRO*-&5U%0#TZ*s2nOpCdi@9y}k ziQ~sWiyE|;MH=)LKYi6x{lf z#qZBSuJMI;>tQ2|KW2yjGb4CC4VQm+@H3N_e`EG~x93IhT|ON3`HL|A;io2@{PVkH zFHaVM*HfD|yv?dyE_1A(xLe2Jbam%}{Pgf>w5ad*SKUQ0TWaF8ANav%Ub*<@UA!1G z>)q_Q7C~)!t^|7R;qMQtXS{L!5Tp+A3;`Gj5aq8Lc+6-z=ujpR{{N%xF&NA-u>xpl2@rZ8v+*kLz}rGvhq`x>`41YU4pSE$X@O#)%g5 zD(;^Fzx#pCM}a5ym_0o(Gq@cE=fM2nAXc37!P5spPM*C9F8XlO4_f$#*OviTJQu;m z`(J`@yfm-4{1g@eN1oxYJ}>6^X3y^e@9@CkAjl(j=9hkT@s-~@{%XrX&ti4wmo=K@ z;F%evQJmiD8*Son@ys_G#PT06Txr1zN4l+vq02Y#fVVvAOdr>Kei)`FYG3ZlaqoE2 zVZZP055iWku3!5>Uioky9&+3NA(%6JaTyQflS}XIk&}1y9R@wsYyJHq%$Z&E%x|$~ z;wadwr>}x|_4M`2AkGZn;~7u!YHkJ^a01_5V%7AnrxPbHkMu}xJlw_5f%`J($6UVg zN?-V+@A}F!GbGmRd>7~yuUCt(6L!Pn@HChaXY7G@~Gk6Hy+aA z>@9-4YVb^6m-=!Y1+l)(Z>e)dJD&QfKk97-dB!iE;!6AE;V+$f&4=sZ>wt@y!$pty zL@RALo1O0iojCGsGu#XGYzO_~iC)ZEeBQIO_&Usa{HbTNsdvuhGVF)L@HXhdSvU_D zVb0~po_R^T`EXvw6B_Z9LkwNMyYq=ByMZP;aFD}X=?xuzOZS5PzVXGH`Lge_547`} zPJB*RceTJ8=z%;u9WLfxe!htHO@7z(=4#K=AN=(A^Zv}->BZ+gc-WdgmR1yeugjWu^Ng>W zIGzRE`K}Kxd3h?v+KbgQ&Hh%Ig^R#fXICDa`QmK4ywirS8Nl87=g(sG?E82^k2=;s zEzpzkR~@syxmpvaPtJ|AB~A>T&Vib8sQFFsyRZo6a3}D{-euSeG#`hzLBDx(9^_pQ z=HO;<(KWjHB){2rZnuNl`u097f{Q2kQpMKk)A;ybb(24ddZO z&+h_1<~;I~7H3?ZdB)vb^Iea}dvSU@v;0%f_RvK;UHWx3+zPzF(Vai?IV0kH=c`zk z-iiAm$f*zJ`PZ-&F8e#aj;Fl*+`r4d;W@sIPvg<&{){(xfqBvg+G(Um&-K`y27Kwl z$DHtM`mVn`vrfO5C&BL_-)Q!o|1Np;o9<=cryl5sT{;mCVh)NmK0HZJ-nhTrB^ZgtngM!*>daqogz@cs4*a?m}Vy8HGx;IS3P zH(c?ypC{_lWnC?`?RTber<*38C+<#cl?< zY1EJLw8#H@e(=%{@#a<>Kh2x?*FjF-pwX-BA9uwE&Cx0h5KjkzN_S5N^7VB#8 z7{6Pq`^@=8?_$K_gS#AjnzN(#bGGnW3w)zlEj)1dF2A|g19L<>A88t``k@AnJe1$Q ziLu5H{ot2a+`%5-tn&r`nMvzv9tQQr+iOPu5XP6So_Y4qfU|lob8P=!n0dE{Hk#$r ze|nceeYwSqW;uCj-R}cUzHNkm1lMkO6lmdvGorRwd*27~VrX$D)E2A$@Wq`@>%Oav z)04nE{ZpGi?z9b8IsAU`L9AyS%mw{sVLeREWzWtCZv4>8{UB~D+z6*ZUirrBnKL^0 zVTSRh-8uUy*k}K&@$PZJ|7sBDyV+BpN3@vL%X3LPF7}&uceVZy><4|N)t&G1h*87c zx;1;h4c7U>cQ^{XJ`EG=**w~RE4&E2QP+Oo<=YDO@=UMvMJ+SB7VP(pzv8T`J^QA{ zMQduy?YET={1vCRdC>>;&D?Hy63mLZ(mUV9PJi`Qmw-pzp6`@!t^#vgms8jXC!pKtQZL%V)H3p~K#Mwr=n+p`+t z<;3A4m>YYnyXbou?8O-uetQ37*jc^1p9cE?7roUF>*_mG*5py=YT!B8@Am^QTIuE) zEo;I4qag1xJP2}4t?izjgO@>W+VqWHJ=RY;<>9Y=V&AWxt?QkAeDhuY8v!5Bw5vNl z(f@tGb2IQwPxNOm$bWrx=OJD+@Wb6~JPv+?^yov7ljpqf{9TY&f9Uhg?8-rtcbue{#`eZ+-NbUp~XLG9PUZ`Q{Zwz>~L>t$6F@h}f+^G(mqlzpyc z&_g=#FvnMe%ey!|xgB`IOW$t>y;u)C!_{1UwSsTf-Fa`{Tx&h>U!Kt{-<|M30Z(TD z7y73+eB>P-JAuCOiGOB+KfI-TX2N{(g+|`--MsFH!=MNJ)5DW+7S01d@#Y^5`ag4n zFRpx13zstUdd*XQ zs?Bpf=Ar)Rq5g~y%bw-MO??{p=DrAiH)+BP2U_W~##0(-;|rhclZysk;-;20Je)i0 z_FBi87uI27)uSDMG5qlx&pS27``w;t6*u1X_{jq^=**f2aq7#DKVI_bJ8pU)H+|yh z{ypdqAAK7x{4+P+apr}7iIo>$G4}9wxO)F4nBASQ7xW9CMX=vD9P|KhwKoHv^2|Kk z?b*!Xj)SwoBfl5wZ-lKN-*(VjH6MlNL4I?jr*k$gpNVDbxF3eM;Ut`e^KcQI1)80M zMOX&E9p=GxEzof%m_2>q3IBQhKG4lOKAVZ-z=K6t3;OE&de{s4?mG|7J`VKLO%q-> zf*#l_{z~AX{rEXke-FIT=lMOw*F4A}UR`^w;peh$kKE>ihcw_iHN~jw`)-(bIm9f2 ze#!my>UsL6&-|HOywg`}^zxr?^7EMveZ>cOfgkSnjITVScleuIbD*v~wDZp_ybXG7 z_RPh8&~tiz4bGT4_riE}xz_Yht(_o`+%seH(V=%{7_Y;?LmK(H5%9BazWCuRuLs)o z5C@#>r&+%7<$lk&SsUIo=)d#f`6zq{qvyMx`EV;Z7d*t7-`3_?->wI}a$eNrg}Yw( zj?ZqOh4*sNzz4qU1=q|L4dQ6RO&;$!>LDLITcb%mmt4N{Mm=$2XxRz+rS9yLUmj~X z=oS4lCs%tGV@(cq)VLA&XjZmX&+^FYPOmdgyWHOSHhb+;!@4}?R!%kaY8mA5`}WV^ z8ZPF?`uv8x?%95`@;J~e)-0<(+|3QYar`<=Zaz7G;?1GH>67{MyMzyp`$0|5&LAyf z_=fAvV6S|%nI$!L0#0UzmoB{Q^_@TF%nb2B99_4pa5nfwk37T0?DLHVym zx#ry>tcQ)T8SaJcV5aDN6!`We@b*P`8D0h6nsakaCl3w+J!XZcw0O2g%lIu9zvZI` zc;-8g{uT7Xtn!a`?>ylLA822M`(Y>Gpw`=P5?pl4bv1}l({H$WFzeZa*#kMv;?;5&cyhnL%7FQ~s0e78=AJl4GHjau?P3;59GT|RNn^j0uC^xEg!jexHli$KS2 z&|CF5D(Bn6O zCg+v+dgJ+iphd6E1U~YqBiGIFFv!JgJa8Ib`ZM2tT79QcE}mQqdx5`Z26uV4!%om! zwefrrhU?6VJi~kX#vdB=oDTW*Vk_X~Sxy@4mluy;!n<0+kAn4o2L0d>KfLR&HC*_lpK5rfQ!P52Svl6idQcm$2Z7)GvIigiaeoqU z(nETd!MgLrAJ6LZfjAF9s*9@?dK0}_DzrU*6)*g*5ok@X8~Wayr1t6 zdZt+{UNq3AmRx?j^vCy!abFK=>w(_zb-cq*f5vOx9A0|I>3-M?c<>DLK@S&UC)^8q zay_VnpYQhIiQ|(%m){t?tl{xK*mrC7&R4o$1api#FI*G1)$?``_dJNz7e1?FZt&*W zw}DQ4)CXK?JqWm(6YKV>p)dN$PkP5MI`zjq;;E)NR%;pbK`k1s<8>Is`}R#3&bVF# zvHuEU%5=9P6Ccp<00@zG4{hxrB^X<7vHVa*;=8{yCZJ=)*F4tX}mW-ayZLogw~hAUcJYaSNQ%OmVxfm z@GE+lTw;Gza?0GjVE}2YnK+K7Bl2g#GZ3fGZzx;EVG>i+cLx+sz;+ zpZTK?UxzzEeLa(hR(^|KA8^FqjPR5m z>-O0DILK{H-*2v-#ari#S<&aUz+*hNg8uN>eBmwLefFwtjZT~=mmK$ldd|4J^I@MJ z;N%%6-n|X)jeD#6uiy()5cspCZT@UoQ_{>M&m*J~mW_T{fy6@KlpFR666Q)7KtB>EEpoaC^!MU4&&i!R)(cpZMvMe7rhdeZSwG4=%pp zH)n$<EdbL5zOOs~Y$nx*@J7HeRyJFoO!?#WM! z%bf9tA9&IK*C39!&N?nUanXkdeP0H(d5?=4>gk=l`ZKxlrUMss=vIsN&+Cm>i@vG7 z5!AmI^ja@?{3y`D_ov}Spx^Aj4m{oq`{5w)^EiAAIP=SeGrm6uxo97qdi=T0(MaF) zhbCHZ#d9kRS3J!vu6WLj;%H9Qb=J%)zGvY){19BY@{Nc83OM4&r}cm%eddq{lUL4@ zAm6*d!!Lr_!QD5PzUj64zU!$qdYqr>w{hFUfM$Ki= zYv=88Fb8JOZhhN+=h3C+`(RD~X~)@~?eHjE?g0;+f$>LO{`@(p z?;Eb(ee*j+gS+0*HJWf-2EEoZd)9(p;=)fCeb#6&XD+qw1nb`h`DxNe{+Wf%AYROV zpwWELH{h{6`k)?Na`E;<&|@>|8Grj0!G)_mKLt5(ra=xq@=%}Ul1H3%G0wC*u5#!D zeaoO0&DL-c<5_*)y$*88{VMRE*GJ(Zh@(sI^nx~X;yWIEFiYx+yB2)Y2YJp{&wTY= z4)xxKx$v=%pS0k$6ZV2yVtFN>x%2HX;5xHIKW^f9=WZrm1pN4551u@9spGQuO?Vfs z27A?b9;}-~b@6cK&jWAFx?1vn3>yI_yyehqoP5_Sm+#i?rPVyqWv@6~wu9QZtE>0s z(Ts@IlQ~=V1AqDVEL;id59ixG%WL0y5O2+%S3yo|I9Y!ZTzX<=XvCSHC*dE#I({Ip zet#Y0ai`7Yo7qx#&Wo9O6rKkD;JF{@=dE1o>6JQkyS@x^^MZc0T)tlo&JCURh{5yw zuo)f(Iq8-6r(iCA4+ml9Szf@OkG{*VmO1sCAm>)_9jC7X-{|^d&@Z0Sy$HMES&&y> z%+~0iKJ(#CFl*z*=ksPh^+J9ecuJcK7d&y~1z!56CO^k}Jf}bWq6=?%asN}`q1nKH zyqw=cb@g-+)ZYj=+zUK0yAJ}5Jg1ZX$H83bIn8Ft>`YJm4qf)ZOzZ_-90ap5zXfMK zo0E(1K70&n$)`3Ai@=8~L4RoEgBbmn+q2&QUg4{znW3GJoO!V>dT@8)ON;q&w$_7ua_~p(>)}eE-}#ou8r|xGJ#v|6@9tvc#Z}(n z+vBHiddRbxLmJHi4RWYOhrG1>5OC!!O}_;>kHTrd$M2LJJTwb5>7g0no6BsNZGHJ$ zSOi@0nR(!moc8%!%Z#yQV)m!R8Jo-*F5a~Kh>q(nY$X) z<>{OQdTG~lp7}=8uVEQ*)hpcGc{;QFxM#fel?HrXg+-vl989d)z*%2^3bgYPA6)3Q zmzQd&OS?5akOzNS#zQ`R7x1YcHNVbHK|X78Obq>I5*J=> zte(eT-V8tc@v?t%-s)M*9|B(Vc;<(@dEfLe`*22p`3mx{`CqKQj|9;O80{s(D z`{Te%T=fnY93O?}K_BIj|Jy*@_<+}Uf$!IX8saYNRu4z9dSbtR=qH}Ic;^3hP!A8> z{N(*Q;Bq_Q&to;Wf}A`sXX5`i$myF{ z>)!FEOAfy4pBni1E>0~yl81+K`=&oUJPNprF&ARYzi)>@PTxNU`o-*rwZL<|;fL7c zFf(M0Z#RM(w7y-zd7ggYO*>As$?Z20Uu%zo@3d?Mn(@O)j5^;2Gi3klUvk#8h%H?x9JgWIN>mR@WP)j>hQ`tu43uA2scIcFX!e1Z+NYjdx4K%1aspz(C@H(e8Zi`4+G!zf(K^dbx?y2T-E(@yb?t0=I9rBC4 z2-f&www6JEaGtvA$#px>fQS5F2YYxV7azp%MI23L=WW27k36<-d^Rug@mkNs;3q$h z{FaZuw3sou<-HrsrFq;6`f0CtnsB1)FJbzr#zD{*mzdF|cXZ=Nli9_O{>kH64{6gw z+;Q@*M`mSfHtRf>-ja5NV#z2NKAKk4~0kKxP*y473@Jj2gk^JPA)yVGs<9tHY+ z*DJMfq!~Z^z3V5B&7ixSzlZ&B6#TBg3NM0qHJll%w0>@jN~b^ym#8 za^f|d)S-#@o@rhWJh~EQR_vFX2D#0g__@C90Viih@5J(&uZy7G=$2Q0^Wcs<4rbIh z-txq|SvU=9nhkYnQb+8`>RldjF5lEwn^)^$BZybatlSH{kXJwTMcv_exwm?x4o=f2 zXO#bZky}6Q<+(h5qty{-{>%yfVsLfW2W#TR$}xNJ(R;Zs&kT+B;9?f^)Y@8*6YrS` zT-D+sAL#!s;LIETswd8!XL8Bw-8`Bdwdfdb?tI((Y)x6O^6FnyKFyr_fk zjo{4CuO<#SzYP4O7gzcD=$k(NA>gitJ#vVV+uoaj7y9e$SpPoIM87-#Tx#Q@zPS}Q zy2a>)y5@$b@;(l@&6${am;0xH`*ye;yDdGG$`uoZB2Ch0XNe5I8R z^`}R8t_St>MV)7XKjwBB_$$tZv%bg)YSCmbEzat(nf{Pr)yhk&QLJdl%C_3h)$VUX8eedVn+`S@$yZbft(%m8ob*7M8##phPQ zVf^m#lb`a~C*EFs@t|dV8h@t#Xr!NRadMAVabocI9sl9zn=>e0-=~J!m+Po+@6-~{ z3u~Uuyj-3qhiBK#z(ePjkKX5-8fNoFp!;f=ShFo&tll`U`hxG|c-ixO>+w?`JbUJWJ=0%az770!;XnH5pPtySH{xkC59;eT z-n2N&Vn)YLJgUkMnpbq^stIZoZ6r5~J>%E=AeS|{@RZk`4$o_W zS6e~+_rYA81iC%bD+g}8<%60>!CWlD_^+;Kd(`HMwe4VDXV-!{&%@|<$Av$5JLAsHelP>husw9ss3%{A;Xyx-`Q)2? zc)R0Ei*NSQ@+6pFIlc*e5^tSO`fmp{>ClH)!JIt{dqI!ow`cS&dKORf^#6L#dZ&(F z(K#MX{zt9xigq5`KiYZrA@I*Pwd@;@=DXR&Q7wL`tEO1Fr+0kCpO-Y!Bp$Ct;Gf(Z zt2v}Z-?CH;YxTL?uOq3UgiQH9L)3I0-fS7!dJm}eRJ9S$G~%%d8sxX zGoz4yZLgz%u)VZfezpJVa<%H>)qWqTJ#xk+YDEOe##-Q`n2MxPku}AGNhdyjkJL9K>hf!9k5+TG2`cEg19|rn%gR?>#KX^0e#Thlb&OJ}e-t=Uz=lwvZ`FR`8!t}|ryoZ6OG~?}e zK`s1TwBwE^{rr#%XLBOAb^YWY-|W8FE^yDIl z;Q_z+t4HuI$n`AXX>RU^yYofRo%W0*&T}ZZ+lo z81%zDs-ds;p9Jx0&`FD0>gXMfp3R+_`t)6Jo|fTyP}^NC{c=`ln7!*gn`LXxpdRv? z4{GBj@5~QwZv&n9(uf})pRMlpABC&IUODW?OMUura`D7|{>iDXyS?sW<>nX8lbdg+ zfp>EAgFpIa4_&nI%OxMKIP<|X9oFR*zZ)Keoxl^^#GHrs!93z;4{ydRz2Uo=^(?n{ z@%qTK9|9kIb2onn0Y9GbX)DmGm-?Z%Uk5eCx#Q{1OTLL+2K((%_n(1Ab?omiNyt0|UeX7^FRiEr1!^qbf6(P2$p?=;A3 z-}~?~@LIgQ;`#GBd<^Ds{MQ4%(=MJbd*Q3Va~xf?(d0W$*44*f-_?*qUOMHXM}B(E zpxK;R_(2J+a?L+?DIbtlNrS26h8 z=RBC{wIGh?VulkRd{al>>Cfff=!tzFje4v{;;sD<%!^vM*(0}@_u*}zmxtn)!R7nE^MYTO z{op%3muK~|KmV_JrHy}^;s50!-s2-(#JEh3w))Q{s+O|Kj+PC>EZq0 zJbe}P{ii@bPwDX+GM@MN=^M`C)bLGydHBc^wamd@kjp#oc&NVl5Ig?cD_(sy^;Ui7 zn%+gwOT96lYRK^+(CS@2zZ3S&`S6Y_@AM02`K{q)#?0u)@GLwDyfklW4L{uFb^lnUf#0;=4!GJknyld` zpB&=l&_O~CP9aPdbj9^fd(p3PvUc7xj9&7?Zkc&7(+%jcO+u^Zt*P=7t}OAS3x zk2d*sf*QQo4)V*p2(QBL!EE8i8~Gmv^~{r8`o9b>gWlnB68I#>Ea?Frc#AWSV)<-t zxPKk+mY2`^^=q)#Oq>TXzPnq;iMKAktOcC)UT(Gc z%&(i_R=6GP$6258HBY$eo!R86ew#gD6W zce?bM_tRfG`AO?upz9#ed>rWJu{v_oub<{g%!iXg4g56|XTkj635!6poNDWnvt}RdbkK~e zXHf62!TIN>?`D(-ql?$_yUc;#h&SOVO#Syg<83cpc=ARqTrTT>-8)`;fv@tJ8FS>~ z#Uk7e`eHB5c&rDr_b}l5Tkv~^|K!0HPral^PxQw3Mff`K27hzD5zMJ)>%7+=-}S(( z@Q_cg-@|cuxq3EZ!{uqu`cFSUaFrVudido&TW1f7Y^KCc@XW=5e3-1F@Y3D0X z#q+`Jc*hri&v?=5tk`4j`QTk|uY@@dn?3X1H#4g~pYhh~yFrb60bdY@+kWuP8Teap zK6%dv`)Q=@%ixUgL|<{C!?V1!;?Gk$c`2WsnkzNchr-~ zJWqptYT<)}?>LMf_`2wp!`!K%e|()8aDM3B4z%*%PB2gOJ`DGRT4sco;_c&)dgkCb z$gQ6JyFr}2eDYmCwt^aR11)OMB~~u}+zzz4s|n)ew#KWQf!FHzCVo7jacae<(ql3 zwiWQk?I`fk`rDx2Vn@&Go@egt;k7&7YPze*AAVm8_Tx^6Z>PbUclUpWWuTSEa?tNx zZZV^Ym$=DipPJ&V>8*GD0(*F<&)ea7n0OrQm)kQR&A;zQ!TN8(WxxD5nH_c1bRJ%X zTdVtSci-f*=P>9O9&*Ytn&syspTy7!c*>y{V)S}=PA(jL`(wZvFL}3ub#?6HmCL@h zpodq37=6K!kNB%?4^HOE`I5(ct0R{h{)Vcj?|yUqUj8HK+j=mkn}KG0=y?!!f^*EH z$KgqM8lDH|h3~I}-)X<={M`!&fsgug7R=+vzh~+;i&e05KAkM-T7dT?>rTA zw0d{vt9;_ch@amS{jjFDyy6f3V#Ud)Z)QeazKZwzb`stN`>X-2E_~&X7e_wx&zc;( z;}71}^n(BD;VMR6eS_i5AMs-Jef-ng`5PmL{;8+WdZ$0m%+$eEFXS~de7+iXg5F*3 z*YxLo>n@tro}SQ$?`fbDUp_8_Jbs(xHkUlaXDzG;KIpq1iN#01<;TlDXC6Ou?3)>- zOWqH`H?wN~XjH@8s4_K`^w_gFTqgfU&%EN1vqP7fbkNKXy!kJ$GkpUt!nEp9p-5#=*`U6cudn}J!ieY547u@ z3s*f=L(VV5wP5~vttQ?0@STS^II^x()79-IW;@sUUG0`4Dz+0kqDzY4gh=`u6E@zZtLzsocA@AUpR zt)J`MF864inbE(?TJ?X<#OQW5=>8>`9ldi-mSObInHe8W`}W_wFh6==9-I>%xSJby z{p5)_y*3YG{5J5?T&Tt8S7A8GcQ5de$9i%ccz!qV*xqk~-x#&%H0xJZ@J$`_s9*ZL z2sp@#kJz=qWBbJne_px7+zB(=*6;w|XuB0?opYua!&g4@Cyq{9)Z~jk?FQfI;-yQx zOHH#w%XZLbd-RWw)ARY=Yw7=LsfY6Y9PWo@I0$_38=!Cai(3z3{a(udA*e?OZ`9Kx zy#3Dc#hk0BJ@?1Vz#aQD#ZOdS7Gk7=Oc;hqL@bZp}n!J>68E`)h zzM1EZK(l$b|2RziqG#V}R#UuKHTA&nm${Z(k9kEmKb(0z@orA!_KuhHC7(FIt7-A`Y4tjT4yhvbm^%YGpFJ~yl>`|c5AfonpbM@UW_>~ zGy3b||9UWI_IY-B##Oxkad5Pzw!HZ41!w7g5G&4Q2E>~qx{kv#;I8)jAg^|Gko#6y}GLjYhgUr58u`1@znUP=SA2J_`V4C@>Hz4c&foG9^$EIAA=fxTXCVy zSx^U8JZ}cM{oeBSJg9BmK4(Onobvm266C;LJ{)kyi~qiv33BC-v9Ev!K?aAPzTu@SEh$-_u}b<O?A4l(M>#}7HK1vS<7 z&9nPf&;z+%1$|JP4l&-%&HN_fN-N*!e-fP0?V!F~VrX|JUk0%}Q^PFk-ASN_M!8=H zTs(_Y+rA$I-^AH-H_#;SQ7~)bc!?)Y_XC~2=^tL>0nOIsJq&#N5P0Si_fxnN{vPC0 z^Xsr5b^?vgvF}rxPuA6X5b)u@`o7u!L!g}=eZy0~KZfClkNpn=4-Nuf_=!7SYJ4BQ zT)q3QhIRhB@bWI7nZ6hJGd$JPH-7I1wQ%7LUgE6juQh(sXWe|*qfdCa#Na01%$+#j zc}S0*nX&0RKM#XG^5rz>1wYgRdGU63-SOTC_X3_zgFfh=8e({0MsVc~AN15Wa7M&l z3;KkYOPpsum^c1{Gt3K~x~z>KG@b-J)xa4aeG@0==ol@y$tACEdSuPr8jX4_hDT!5 z^N#o1AlEY3qlP`|@`1;`uZO+xCWw;*Cq1%957kr48jpGNAgCp$Jig(_gPq{FipRL& zavq!qJb5mj&*sbjp5o*(FJkVl?tB){Yjxz`4&uy`HJo1uefE2euY89=ZQh%i;f|Yl z_4Iev&wAzo{=bIVi#x6A4Bx-?JhSVZ{;>My9LdLHxgG{S+CN<7Ro^_*>AN%F*}TeS zKaZTPMX=wRyYB}9AHSdS@_s9*>25~#@z&~@HXiT-$3KO4!R1}7oO1CPr>_Dn=0i{X zKKVxbPr(|F%Yd8x>Z>82T)66obu)9>i$(8ZtkUb%Jyj(FjVBQAQVSMDzP zre9+0!BH-MQ#{k;8V~k*-U;|Qn{qjS`(ZuEF}l>WPM3bGCy)1|fU8*VG%SL=a^OX? zGia9iWFFMkA2aYG_*?xZ@a5N_|Mu|aU3eeN|DB+>yk7=-^+X*W(qT>C4+CvD$*DJZ zxm)9lz5Z6=bsF>?5AS%5cF&8|yP1&RyII-@%Ygeg!EDU$3;*#@&phBSw;VJ)4!+ao zGBhc7Xl%xJ5KKUKA!M>&I$c`Oo!i&^&t1l@G7Wh#`!Ei9j7Y{58vq!uLq9;&3fVt z@&b2j`t993T%L72oGJa{i~jKCU6}K07WC|TuwN~6puhTMcGklEF!?>>sg~ao{qRmJ z&gSe%u#Tg$o;N|?&w_mTFM~Yv;pq4vFh=U$GFMsnLghBJuCuE^4KG<{CZ(FwgaB_@fa^X$3x8S>RD~yrmk38%z!-f z@^C$shoCQN+xuOh%{xE#gM0^p2D2u{Zwmf&_-@wrf;w{IC=ZT{;9X4|>EesJH1W`H z7jN9v!hg<@yEA9s>{t6qpk4i~z(<#N^>Dft#E8EY^p02bsZBGTbn(S3sZFEvV8+dl z8Nk7ut%r?(3m>+_gFrh!9tYf>hB*g6_Ut_Ih`z(17iv5UzVizg9Mtnp3-0RBhwn+C z2@l|zSU%xOyF6yrMX%Z8k$#AmmrncTwC-*X{w^L_!-rP=$6bED+9NklF8gu6cYgd3 zX#N<)%7cgJS78y}htcw|XETYHeD>0W1AXI-UWt`QKCzzl*0l`J0uO!ndw7|1ub+6Y zdFWEZ>>UR2@~DlkInZ|;-UP9rCwTLNetYI@@ML0p{KTD?%U~wdk%RZ=fiB-&bi504 z(qKl+Jr2JG^D^J@$IPnFxp$fGKLzti4-e&%*Iu5`{kLFlp9j3G$>Tie`QxB3 zdS-^qi0|szD+d2LPyCk0jEI|c{@@~>ceL&V-}%K$obZ#^C2oF`^uRfh+idx!F5oEd z@Y(NK?Wxz}r{8CH{N-_(HD^W+`}LnU_R;KG1pVfR+H~_3ucv`;_&DqG{u0zb3~$3p z(C3TrA>jRU&|A-Vz7KTULnl}VynNpb(>FZyna8-@3TBWF{o^&hIBtY{LGS2#5csa= zym%Z&2My}!2adG(o3{JevICK^~`TP zuLQAb;bUDs^NGK_xVYrCF0ae`BFN9T@yHC)qd!jrPB_aezdp*X*Vbqy1j#j5)-;H5uwS>qKyoWm!h8Pu55eWTp3ub8U&Aus!50^f;?%Nt&b062@vWYD zM=M@(;>vd(@=1-q1pb)&zXn=nPW5;_Y=q5lFZey+r5-#8`eJ@}!{fkn{dU%QYM#f- z|MdHCSj=%a3;M4QxZ{m0p7i2LpPu9PD&UXP%mLqNrH>~5(6tD^1)O;^-^>FaU89Qz z9@wV_@KOD*R?o}syirSU^-3SiHV)o#=I3a@#XS11pZ4N0Txq}oC*RD*wbk=!cfR1I zj(K#y8T5rNGp)Zk$isKNbK#K+Gma5Tr><-$!ZJ<;>MV4h~C5-U|z%R2Uhq+LnmtTZyVSK}nc57zEKK1dm&Zmjx zli8Bz=P-TXi(aY68#(3rA&6fD{nr=V7lC%Qt^~2X5hKTT;6LqrRae}H!2iYSe%ReU ze0(=+$6++`Yw_Nh5l5WVr`aF4<5dW!Do2zi3j54^sW!Md<^#S|HYg2>H2LXq>!G8Ld!8h}%rkI&)x#gtQ9LX)O zz1I1x_E$mu;pi@AJnQk(H$LAAbgTzGciC@`->f%5T{D1}x-^L8wYu`y&qLQT;61v{ zp#IrM3y*mwpZ=IrdFE`~>simXgFJIS%)`_0|M#4zkJCvw3+KTcybJFGANl2?cM+Ti zcYfjOPN#jmrc++~$Jf1{@m+@L2VKq`@5Ix}Yy0VN9?cTn=8B*CpzreD4t(`@#Ei&c zU0(HgA~$~4@$qdDo&>)iYFRf|KL$1Q!Mx#n6ma4@pL|;c`!-kLx63@?p%z~1tTApbN$5e zL-6~l_IWUq*40%@ukc<5`{{LFJ>zHQ@bj#mnZi{)``yixocdzF8umLo>hR~6U=}xm ze9u<*H{E%{Tk-S%f_nQw{ewW08oZF(`jsFzt^AY|r_DgGSe)JQJa59?EOpc#4mGxQgScobzm5f6YE!&I8^&pqGz0(PmcF zSP#wxfA0mHTyo0wWl)z^`gvwAtzQTErboE&1y>#{g5KN@+u=dj2@iugrsZkiJMDPA z3eFqN`uKDBC73sQmtimL2Ob{>oKFI8%_C1g2L4(55S|74#qos>Yu@2~kas=!Mhoxp zr`ujW>Mt&8<3kTFyl}0BtKoCs#K9JahtGY)2@k#0qw$i5y8+M3UOFHA zyDE=eC3~YIrYgi&v|V&_{|U8t-I8dpN2)i**vO)kNJ8W^hYe;@e{KM);#kK z7o63=)hxNZTbJiw!6gUXH1NjXhWCMQwEO-%90dK--$k%4M!uQD9zWH--W_MZ-|CCy zgILew8XX=XKpX*Lo1lo?mlhw2D-cJM0lZ$SCSu=BbK_?!3vrqjGVG-0_3v?`k z*vtO@-aFm)@X&0CpIqwFdHH<&Pw#Us%oiSf+YS%H<+D-Jq^Kes9gKxzg|Tupefhc>doE_;|)eyt&&9YT;<^_QH5&uRJ{9g?D+> zwl0_b=HL1HJ(#QGV7A2ZOU(1tv%1!3unuPTi~mDwz5M8zrg>h^u+!GUDXd3fXCa5!v-15l5IgpjhbvZhN+ceT2E(DbkYi3K}^5JFT&KVZay?ogbF8wk;eD(Jvus2)2M{>Dij{?7~AQt}{ zYxoT~uv0f}*y}6a;>fK>uL6DKyc{h4YIn!@;Nbj1xERz$7ydLjeNX6VHl|*A%!O|t zPFq19vvoCG54f8%+TID~Ud{USFS}1i+1v~?z;Ws{8}i|YD=qOiPtU`XAP+5Y)*H6+ zvY`QP-gkm{>ZISYhMw>-Yk0DeV+qG$JJ9h~aGxx{X(+Eb-3@rt)N@(C(_?hRQ#|MV zXo`z{yRBg`-}nYcq(+XYSPN-cM`&LBH`e zbNZ+*f5&nDESMuY-w5Y}9K*#PJMs9Mo%=y;Y}ha1B;diu{lGyE`~g3;snvY(H+$^G zTLpRePF(iBCD#HC%_I#j2EDx*hN~L>FqjLr`mYaW2d}$<{ewUUGp2Ujjsm_kWvf>2 zc*;$KJHdBAPt@ugLDyG7AJpeJUe>!o95vvBA8u-8b1BTX_j2#*9NxI-5pC6b5Y7a( ziUaiXn~$@-fV(>&9zL{o?kOK1+Id%poa*#j4BF$2pSz|;J=_iZ;bkzla`E-O!<9eK zl%8Vqu_rfMxt-k&mb#o>3+@c+Pfccf z?yGr_&+M6(C2WSRKsPB4L`iiv6wWX&9z|fO<==UJbJ5DU#_g*`RT75bfzaQX{3hN z!I|DUGp}@#htBQ{E@B@A-vfHcf$#Ysp4cG2-+G~cdZZq4^;Tcy(yx=C7F_u9lh3(c z;Jp=g1AphZ>hX3EkEZ&d2V&5a9X)Wk9Mpn~{pp4IFdO32N9mG;<8VFjXXi<4%M*9{(0b+;2RiJBH(|ad?8Ukl^iMoG zsD-cJW|U@2csI~jk9{-5d>)R&=;}8+nu{eac*os38`!ANni}r*PA5I3pYM~I;7^BJ z!D7!&E#_bq)S_p50UtV_59-(nc;lcy^j4!q_htQ*Yct>}9xa!^*1Y3QOR-PKdev{7 z#2#(c@4n+`cAU#4#uCg5p7!*>lfRi%H!YSx=c%8sdTA&BlR)pW6>s8;XSV2e801ig zd1j{$T=#;yaTM3=j1SIqaL$*H+VR9+UT1Q$^R#gDPVA&|(SvJaO>u{6ZK%HOoJ<#E!pMKnFfcP?Ni-Uu@iygFstx@b$e|g75vyVE*J% zw|O!Hm&5Jgo^A#{?ghK~mVdbZz4R0x2Q~6NAN0xh#vR)Vcsu7$CtNMH$t|85>FJwf z-o?@%e>XRR{Pu1HbNM8E7WRU3Io%z3%$~S%-wLZBCT;LCAJ5iLxz(w+XM?#=zudl& zadg6B&=(_}5_ ze(Js(@TJvC*SRWFKh+4=-9C z26p(n_fr#&IH=qAl)sqs?GOj2Yk@yLxD1bz-j`r)1bsPOFa5-$F+Ve7$-~$GFNO;q z@~B^ZZ-;lo=*0H4J~YOM{-e#~-u0YDz9Hh!z|7DNbe;R>w|=Wry-U~&?x{HyUmoxL&II2Vv2F!Dm~WZd z<dF1`t0g!vB8 zM6X|j;pjIW({H&h1v6?MXh8S(f<3X>szD6?IH}EdNPTLdBR~Fp=(hyAsu8!HfcHUA zr~b2N>&#Q%`NI;X2HH4x$7zWN{XFqo1u@1R-@Wy3@A{^H{5Qgzz)v4NX+{foaS42# z*~95}pvMx#qM6+K?a9W|zIxBD-Zv}I~*FHb9Mr&uZ z(EDe>JlJFBoGyNwYr50!N}!E8=_J27wI`OI@Wn-M=x^TGo5k%Qo?K6Zy^~-r)gY#N zc7q!5x)bQb4=3kljF$2{1HCvC@E1p|mN?GcH~hrGgFoJ%1ZQgeBG@yN_tv=5Q!mW& zanMhinFDduCWcw|J+QC;?DXqykbg7aF8;+p7to(2Yy`iZu@^@?InD+7>1jSb3^=Pr zKiw(sGy8JULLb;$V$A(rdRM<(?v5VHXEyn&-TYh#IN>lg_4=(1d;9VaKU&bxnb_jV ze=W#kPh8yarHvkL1$^jboelI`f;0Xf1@?MR2YQRcmZoMLU%$yQLP|&>ts!<;2tPH^IER1GLBE zIEc?zUCwb*r+G8Wa_a$2hP#;T#s56WrFVFn88zT1k6xXg3-R%GPBZhThATl2u7?}p zX1Ei?xF6&)L-bUGyN%Co*bmQxIe!^m1sqNSUHRi@PTVVdtDq0G(LZ^`*$J0}+@PO) z>2BtR_ru<&KRD45C+kdLN0%Q2{(5XCXiO*ZXn_;`&C+QdMgv^M*IV)Fy$VzFVej9q z`%Qal*SqPJ`c7*}L;8tF2WMA-0<!={w%?;)RDf(QkhG$eyh}9feoHe6Xd@Vep+)2cGgfb2sp? zcPW_ByPG5N{O*Ev3$VHKR4N%7?2`@?W9 z(2Jj1*tl18(D$*E=S5)W+e9;Q_{@CR6O--DV9t($?~(d`D;xy95ZCqG~EyK+zM>X)_XxryzPmnzn)_Kc5qjI5^%M| z6?6E}!x;_u;VIwzmXAjI;%U~!<>y;-E_@K2KL~VnX0FE0o}A*_vvvY4-VSDhR`gbv zn#5#78yc^I+Gy!aU)%#vi=BINBk-4VGl+V}M?-TXp84QIC+E03$3qQtpvm1p3w^i~@Nh@@s!RQTJEODQ z><8)>Uu@j@;j2zue-`Ay4=;P}oSCKdXr$)r;iDk7nG~CixX#sPHs@W9?9^lZAgI@O zMh@SYnHlq_2EXyx3u=Em>;$@4X98{Q%cXv2^wTS~=*^{|=E+G<{Pb8~d<*cv5f`&M ze0&eg))F?uR=|%&G`JCN1zO-dJ)#-z=GHe5#+h8V18wMGR_xyl`fY~f(las4jU3jG|K{9jWVpap$# zad+{?;aX6S9?_3ZdV{}v$L27wSp_w+l}9{zai%|eTFWKRX>DjnuhH{a@3g1)`Cz6e z&Q|Z1cpE_uHPhHS8}J%^UiWT>XvS6@>JoDm^pOVgKMdx{Z~Vo;MNa3qtKWT)$C`N) z7mwk>=S`UT@AccB;BD5_d?u(%-?!IKTG7)JPqolMUNQBRA0Ir;!$~k7(_cB4pk8)j zsmHzGW6tdXzGm2&8qD_HV4j?*)BZuQ?gcr`tNv^Rnz?Jk3un2-H7oL{+bj?7;iWe7 zxE(C}>K0S)__=@b%Sk)4gRdUy3(j&}2{`k47I1TSAV%>75_#sxB6u!%;STA z?~~yB;Qp(B752mPa2Srl%V1XNh}TJ=0eergZg8d_Z1sLS&=2?HAkGrl>ZchH-*5GghIDqWUt)@fr>CCs zp|dmb{kHIWJ6sKOw)8H}g@7-6b`yg}`f@)!2#><_Wcsn&-t#~owa`T$)bu9MReb%| zciiN{!7Q3Ti;drB!tJmM?*%ofZDx$$Du`=V?*%n{5xxxgJPQY5V*AF@dwQi0V#`H4 zG0iHD^j7aD!FN(_da(7p6U5&OxGjOM`tQCh!F`p3MmX`aaFT0gL?6wTdGmgni(Z&T zJk0{`WD`TRGti0Vbj6=$^td1Nb!t7`Uz(o2ccbsB{hdJP{XmQ9 zKfRB_%kV0^4l@TQy}OT_0dE{=f!kJ4zx?X;PD|SBiFb3aj~3nOKm+#l^Tg?Dpq-wm zfsH)QUxb^%89nh=zkEAE9Jbc@$fJjNu+<~8q8H!QoZh1~o$0Ek(ekv0qa99f0)22a zBkGpdlG7gEX2l%hCa(VIJ%8(m;Y#33d%pVYIraE0CmZ{Cs9P=OmYta5++Y8e2frr) zm+|lQ+xe3pcl$W-XD8OPAijKT=iNT-tp9g)MSl-ROeZ1)`rdj)O zFn|2arJUlZ)sw9=I*Oy8@|ssM%$VMq2YSn4A5YqOE@Awy_D<_}!rg%T{UD#ZW;d?_QXgr~g*%wlAKVX*N3O9e#fh#Fk5JbMb!I3wm}Y zd>C-BoT=wVpoMqyfe+2pb}I}QbEl8u&v)Qi@A^t3IWMn&PaHM#!{H}^-$}rCxQH*0 zJ-UdeHhSo>S*L?s{I3Rm!`b|(hkot?8~TXlH$6N@Q}ac~u{-LWesakxzBB%-KsR#< zKMeTE>F&}=zs;z{hHmc#cC?@aUBp$tI%zn2YLkzq&dnV^+K8=Z?B!bp9PF`GGdp`? z%s#z+15N_pop3wIX@2-Rrv+_rQ2VPO9)GpX|5x~ke>J=v%n3f8dLZs-gabZiQ4huE z>z&OHf^!`BKL}#5p#g1acW(XLQ}6Z1yT0Se*Iid5oy6uxdv{wOJXe8@895I2abho) ze00QP3D<*te-F09{Xi$&)TwTIdE#>^h|PbVG|e;D|)msfmTeDmmrzvYbG*&sLjcY-;SF;&cH|{jQ5%l<@a68-yG@N^DUT0SHgI;>F6ZXP> zI0){uSvwBq!#W9S&@*+u2=1dfpM1N$tIxT*)IodoiA_i6YNG`XY;e|N{_4@2`vC_U znm5l!;o17#o_OrlL}Pi>V^2Ih?9oR~`*Mgey;cL=X)PujSc1L%pmsj~|3mXtFkfax zePZYhew#rY^@%gHDj#jx(d^5x9q^p|?CI(^UGyBpRExOknHixat(KrSa)_zlFnj#) z7Q>#|lLME-a4yKlPF-(@C7205W{|COH9MF4cEDdPdW8!u*wEbUoC$KOO%D0q53d7X z=VJLi`Ny6=P5I6o;v|m6&*Enm@NiBacZ?q5<7JlJhqK|GActA@Tc6eaBGAGa-{)Zo z>a|aMb&7A^^zm{qTk`1DD$H-Z)H=V#6r0_RKtCKtU%Jt6Gi(LCovY2e`|EuPxZv&^ zMIXB2WS=iB%qtG7^;7(rHGVXeYcx@#`1Z^Q8~o{Gj_HB3nO8giy})1p=!EZnI1|*W zN1uh=;Ou4)2VXvVDVP3itl#DL&R$>chMjOZ$V{ODlrX}1^fm>$mGUh&)u z{PbJT_=;!#^p2)mfhM#!%O3^b1M`FnZPoil_%f^lEuMuJ0jJ@MzrE28NB54N-u03u zIGRcQ9j)Y+)4rUOZ}P~EkGZ%U*ngkxhwV>(`lyr7D(D>@_~Gq|^Xd7en|Yv_cl^vL z4aB7tP4x{=>)oLL?+4#BI^*b0>;?Abd*)2et-uZsy5J(u#9$+q7<_R*2o_%EnXY`< z>Z>~CI0|}lBjC>uZ|8bzUfAn{-?)hXQIJz?IlP-a`*K+{#R+Jj2J?iscX8=4d*bSW zTxLMbnY$~!XTXHM8$4yS8YpZK`p=!~yg{Kkd;bo7nEUwvv)H{0Qk zpPtZ0E>D`$jb?JF17~rTAhvISy6Np)9De#lBlYNk*>*3_1i9}8b8}j&;Y(NV`oqs} zetIpoy780yc2I{sdYPS{1a^G4f;)5&em9sAeyiZz9(#Jx`c{~4g8R=;z3Ork{3H-cKso&KoBjEXANwzWagpY7v7jYJM2_ zufm66CtM0N(uXsFAMQX)y;Z|U0T;2&8e6^8Gf#Fjz`?hIZu&+aea1uV-LM2PJg)?{ zv{d`WAeMb`<+G;-al|wS;<$f&-w*20x68qjPYgQY{$9WphYNv^xb&05Z$4tv#yea2 z+{rV6Ci%E+6KMDNQD9_dP?;hXg*Nd&7 z_R)j=*&z4C#N9VYO=>rn>a;JXTAl^{vG{vej~sa32z(N7B*r`z+ z=1bi)*H0X2=v`0sWiMEG>8CyM*_lf*ozX$9{$4!|tH4(sbXo=eKu4NC4>jM;u($OAGqQPlLO`p5N@vwmjnFOb^#Ic@vjB`H2FB3=D*YL7vVVQ zxjNb5N?Z5vAUKy-{m#^H*`qVw(^oTfJ=_d;0^aw+Z~88-I@=BVffk41D7*|bc^%#a zwaI@b$g3tZqL+F_ceUd}KQZ`B?fASMhVO^H(*kdxDLvIH7Oll%M_>J?mt}tyoasOQ z*2SQH^J_V$BY!#EJ$=(!3T0cYGS_0aQL&<`4@ z4d3_Hzwy))XEac^y6ju(lZQVa@hp3^W4{DeZG*2bP3`pw^bi7Tfb`faI6 z4m@z=qp$89?Y4t+znvWgYqYQ@K0E%_nZU>W&~tsFlf1ao-+b%YxuE~L34h?NlocLvy`d!e*ca&8`GJ^bMw$+G&EXKAShSd>qV{dLIS%$~-*{pNE~W8}QJWr?uGc_sj)N4uknP3iNpuUI(*8D|4ZrW`VZ)qu=^IGct232Ce9=@8)FA z)FHR{Y}CX5`(|rqioP>HGdKE1&n3|GyE>W`b7DqL>vmc%b&O7{-oLBQ=rWqngAV4F z4m6iT9e9}|Ya^VVA+?}GAT{LHJo}AO2PVSYQ z?z{XigWk!r5on~R;%|ma!Myrrh^u6@#*=y*YBCTN4=|I=FE3#71T+K{qWtp_I=+U8qtl;V&UUn;UfPk zsOe&`H+4-d>ab6vF9TbPHguuA`0lBG@u!KrTj6?8>u;)4Khq^5qBe4XT$XIyWf@X>*;79pO~k6sy4bzKc`MyZUyz;38#Ci z*6;h4s2g9kyH9jph25af&%!}a&tZ5Gj>60EeLZ(K&IEek%*UdGeSM?D`M_4~wDvuv zA0N6--yinQm)#PM13%~UcZNo0L9hAh2mX2{C+vl-fdAQ`$GG6he&z-@zpsXC0Vlce zU~d-rvUe_@`0n$}2rjf#BirqOo4oXP=H0w5;dT(8#`3)izGt`Azn$^X8+S-PynRou z1YE?kH?zdfnK}44$cx{*;bxfGFc*s}@ z`1wwK_q#+Fb$%H5iA5v!G{M#U<0H1(#8C&m)irv1H#2(1pEi%e*7`}0$+6S>5^$8; z%$ud*LhrdF^5_Fi^=Bij0-oOSqz@i%2R+BZ@120N80H9fJbVMyb2-q{tS!NeurXVB z-w0|DW6tC}3C_<1T-nf)UUK5(|Le4;A3kP-F7nIE&TO&6+f1pM9`YW&#*8`2zVacOLb0HQx zdu;I-fBEHhu0MS4hrOUqnz|=Df!?FfVefeGq0fUL{`}3QiP(JP$BUgBth2%1D$pNK zOK;>J4fTya>cc}Gv&k1fGe{$xp9J?pY6k4Fcw^oEb~E8#}?D7*>c(n;K#;Z?xPd>jXQ(f)N%2YuMv6VJV1PmiOp1o_ON ze#=AWgCO_I_3zD|c&kZ$7LLxv=Z~9wSA#mmVb6}fr*+gLaqyn+=e6F|s(0?fN#G-f z9Nu@=PxZ@1*IU88q9xvXE}t{Ky&lG2-mNft%ZCS!YEUzO`rxn^obktxeyhL+FZ^eI zXyJ~F`A#qswAK^aJ`6PC18gn?d}ua(me;$S_?jEJ)NDre5@$1Lc5w0C$KO3syE@FG zzKPApzUS1#kI!y+H{h`f<_teE^Z*Y%vG`h#|K{ssK851#bJb0;i;owG+l z|HN6sUSP*wKD|(nz1_e^54OYR`svI*?eq;tb2qiA1=rg_o~h+|?{wz75oT6<{q_!; zCq8D8c1P>qa<2kkwOtNoUJWpkez7wrIN;aX>6?I?eYMhu-ulMIK21Lg_|Rt+EOF>2humfpFaFnqeD2$KM3^tEZ`vjkHQjg_gn4!Xr~6h>8Eaa<&u2ych! zDLb`_g{vHDq_bILuf`99Ty(~X7V?@O@!fm9kQ)aaF9$l&RsPZGX7B352k(2q-@6Ba zwzM<XybcFH+rwaZrBgc!a;Z*4g=kf!pra~m?b)%1ls6{Z`)`go?g&a zKH8Z{HS7evaSk-1#e0E2AN%UoOZi7nnqCcb7IX5k=YR3ko|zZ>R-mutoez!K>WzB6 zTiM#o}WQE?!+oQ4{Y%}7x2ULC&67)*W{W0$jL?wXZ&bw|7y@LGiIM1eeBcp zb`ZxsNKXa6c}hn>JqowU(g{ZX%T zee)Y1dpCnzqZ!?4Chp7dvmgdreBE1|eBbP=k&XG&1AUr&V(Jw>G+{U2sAs*K4?NXL z6Fl^3KhTQK>YQ(e{^D&;oYC@nppQCeucvB!5FUm3Ue0$?ZR(=e>2DwZKMZ$+zKaEx zy1mPzPBW|)b>ea+ydCDuJlNk1{KOoc_IekGhWL#>?t!>+sevY9oUVWAx0?CTL_WIF zh4$_`ee~t0!FNMV;+VI~;cED_etb!h?L!an*81(D=bZz_FnR&nG z4+3rQS0i0!Hb$TCn~&q{M&qS-_0q*msf!Nur>WR}UtB-wxP&hQAKI`rzciQsS>WS0 z4RDyc=y@g3$1Kr6{ZlL5%?>~Ft`74fpSUyQ^f7Pv;z)D)orKMRufIK>>d+&(?-v0VPs_P)gWo%0c$(**1o`1yp#6KnyqP1sR>7j9 zeLbb8o<0irTnZD9=4R1dpu?F!1F_sAxy-BDak0c;Cq5o(S0gU(2Rg8q$2^!Vz0gB> z^$jnv>8h@|7r5dxcVp>&Gw2Ns_4|6b5vEsayBo{_?dh&3>NOX1r{!+g3wnDH^mlaN zYu|5kLVp_Svw73&S3zCwq5F*g)qu0wmtfZD!w$cRZ(rP_;EZm3)UMXC6W5u3(v()T z0kz|0KHLL#^4|<%-44#hpdp)A!5JSt^G+N6Wsj@A=$n2&3+keeS#(Z=*~38{_T<_O z^3aFwIO&Bae{kRQ-@6>@*ME7%$IFb+3lF->x2)gov$6kC@LLVynXkk3Q;qnDIeOwI zrXKxaz@M&W6AyOB0cSJCesbvnU;Q5apY;A&P>-3RuUf>Jdmxv69L2+n-|avb^T>WX zsDU3o?97Au+%387n|pTnZLObvPyP6d$4_ngup8*W1|RdM_jJWed}n%~Ht+5cE^;^* z59ira%lNtD@|ZLJd|wBA#j>{wbd>`yan$s)FtwW{zWUF{w?@oIffnreit8@W{4n4l zzZ%WNjj$iy1T#R(t3iIX)8kn<3VMSp-NY74KgBu|CZBnsnb_i~YwEw&yL(0}{Ovyo zzBhP059+YQrK8&A!EZ0{wQqjJr;qvYWFsc66sTX>w=MTfonmzY}uI7pz4fR#b%fbEJ3makTG7Ij8JsRRKmL7grucbX2 zm>cz|fv>tJuQ+1xS2w+I)$`pzOZK>%9r5gmWzm3cxZ-19A3W)DE8t9Hapf0l6%GP! zYE!rT;(QtKu*8@1B#8OJ`bjH%?BObgS+a)@4cX``|BK;Su*`#8r+e=Gd0_i0jJ`X) z>(geyNo;kpW8+M1Pl7uouHWO2gPFP>+*S8tz9W3hG~RL_hNJK@;P5&e2l33jeLCqC zE@n=C?`Bk-RbYpQZ#eDE?5#j++T-Lt;7GrZgSh5LZQ|qXoG%}DK@9wd_m$qi2nRtt zc5Uz2iXJGhsi7t0w%+ zf%@qp$6lDbXD-xA*WV5J(U5j}Jv}(;-JBc*y~2Ur^xC}Yi&*N_Tbyx`8~D5*#MTq> z^kz7Z=YTgy-R> z;oV?4!$TbQp3XPJVHoZCza3UV>Wj?$wL40?Gow>C(g53Di%q)t@hAoaWU-X>&ZBD!c{l%e;eVWtv zVc^eCZ=QvN@FKhnqZ5vof?1Vo=HOQE^rewl*8^SDu1@+m(+j!vl@54Y=9|Cz>5Y@~ zn}Lr#+8qV)>7$<4!R)fZOWfT+ceAM$vHcdqZ*_q<`bR@`>Ye;}kB)r2>pN}KqG#$h z+kDInnAauXFmr*Eg$s^qqNTgw%sxHnV}9s`|L80CtMH?shRNwS9(cQF{KT?wF%$IB zQ}xMBUwu*|O=y2({eGt>O~e%2-INPQv2es&jEU#BGaB$QS91ShVDF5U`aZv%u>tkc z>}KFgV>;k;H!K0q$tNBk9O>g*a4ncMKD1Q_{rT;N{XoAr;UutipFR(>|FHLo&Hq8r zA2vJTVxaM6*a~JsT$=K`5QdlY(|m6CdpPOO#HI<3&jU@*2aA?8a?j+|2incMx@pDU z84WH6z0g-W(8oJ1aiz8UvI?~E+zW39+Pgo_&BIYR4)V(@w%lezFXa|bf7B_C+RY~a z-w*8dbrtA_4?i)@qnvo$4(gyIjokUyf&I*Zob3Fj9Uf|x?=a8+e|>O2F9rUzrGc2> z-s62Xm{sK zD$tJJa|e3;)`!j?2WK?f4Yl1%Rz5ffhIWXojCN-7qMw6 zuleGKFAeP73N+ zZL#Mot~_&Y9;Od;5PRaN;bLIp?A?I7IC7{@&vwIpV5i>}8<0!i<+u3RQ?ERpW`Guc z(-5~G1h&rcqa*J2R^c${-EqLvxwuPEw_ehcZv5|ttza(jrITKo9Z!4g#c?m#@;ejk z`7I}VIq^6NOE6cTh50?_`0!=B8HV5W-p!M5j=VJ1TefPHODwr*NCRhd`8d!?je9{I z>Q^6~>9z#9Cchc`Aea%{#KFgJJSMN-V(NuH;^sT%{#*(7!coANty#c_e)7w+9eke- zf?9BW81zF;;)_RXclT*vBcFLvkGMGEPZx1${KMe?6Z-Qi$So&Zzs2GIZlDRiYS<5W zd&)1Kd#ev>m6vWG2E61t2=Y7&_Soo`*y^W0AKKB@-$rribuKvnJUI88t$1Sdr-7LG zvZb$YAf4!Esb5aK#bqa!m}1b~Z`!eax&FFcpWV9*j-*f zabl-VzMk^YTV1rp!<^ILc3>y|+d+@bG#$-=J-x?Y|D3-GG#q{J^?n@W)kCp>{oNo3 ze;SIXhWCTNQ=ZP)(RuQ0_Kp{hPuBbV)s8#+RgmMO@Ihd2(SGLdPVe?*Cn6>Ex?&_rj8}azjn?|D# zo6&<7G~8V8@xe*YX@bjMkPpW*VQfaf)p}3N(VW&|^Aqp%KF!UNeLUr2OLslcON%e8 zf;z4SapmV@ww>c44*luvF8S?yI%j4^K7HK`*1N&G{Z~OOOW%jns`n3rxzvCDxahq( z@cw?lNuLjbnWgdVU|wcVUFyV-pBZ+~@UZxaHM2l#y5XW0b~xf^#?9p2V1C6p3})3# zs7ov}qVM+igW02-++w*0dW?@4{J?L0$4Pz=>snBk*!;bFIupmt;LTpW`XC?v_T-?+ z`12Y6dD5D%-)i=IH}Jg`R)OAfI(rh_CGo}P<2QSDe8qAHJn8E>eehc>oE`-ld1rt6 zTOybIY{h;a*qaYM_S6sDuLgGNmrD;;K@7SM_Z#c?(MDeO_@4>*(`X5Pd-ufcVc_T9 zd&kLgzr>J(z55U9WXlKdtsviKP^Y-=$*V9N#S~{Juv-FKGiHW-JJjY|Wj3w{Glhe@ z>`t4#CqZvkfo?B@Jg>uXpgFDleb*OU=xL9yIJ8@Wn#96Yel?g=T>0t08r9&*mY!zy zoiHPr=>p9kS<}^+AoevU4x_h>fc~wfgN$ zF8S3(3x4Xe=%ROYRM+gY|Lx#=elM8a$HDyX2Hc;A!=Q#&;Z2b9ARGs|Xv~+rT%*n0 zFTJ3@vn8kzhgCQVw8GE(cYXcFlOASi_|VxJ9r&9cT9{{??uO69=JNshEE7$1q;d)ODHM6-D zEbr>ZU45&dH+bM6x18+cqla349L$t6nuX5I<>X>u;m z^h&r9rdHZa@Abp`R#1=mQR9;!rkr9O26<>G$7rW6`Do|41ZQf-hi>?bJ2P~?cf4r0 z1iog7j(YJptin#%4f}yVes~-OJYI*BU=L?H(u<~k!`}KSmOOe1>Y|-oZ1ljPUnXv9Z6oUF}YZsq1f z4>`o5^ogIhdwK8$IL?_XB;+y}R&P(C6*o3|Dsen5`w?Y=+DS9rc@5c=&s~5$vgz ztzOe#jrhyMA2)SbeCYX3@GTS9J-|yXX3K2c3UcAb|6$k(>em; zH?Fr#|rNprEp z*W;~#3ms{+8`Pvub*n)yym;4E5hzLvD+$GjYY!UwibV1z5)cCpz%; zE{A>gKp%XTfS+^qnmP8Df*D^0dDQ7{s?(DW^fX88XpOu6`28}>-zam1i$2hdHgd`1 zH!X0tcP8+A8fbhz+z-yxyoAlL6U4+FS3SbfyS|DmmU_f8FTP_pf;(V2R~zp7#Q#E| z3ytuV^Ir)#iJ|V(vqXEdiPKiV^GYyZa?wKlcf#GEr|QAWeB<;a;OM@X9krP|waJf@ zTIq$eCmX!Pp_4kS>DAJ^p2}$k)Tb`o)vFHt=zBKcB!}N80cTtegL>3xkKHP;HOuOA zck~@sdBoF8XJ3YcfcMOlcQeFZuhhw&zO<0je0u5wjnwr#sMj7}^@(W?Y3H1e`1&Fq zzUrBJ{g#_AJ8@5cD`+G>ZKvln+X|NhEpCLh!J=B*ahw!dF0UDoTMXZ_ z-GDn@+u_~dTwgscduq@Fnu%}b<&y*7C9r=L^k5^%xdeIaITwr0axTF>{n+BPxBhKr z**kX^Xf|h_m)F1LFejE6`lgQg7Mm-!a`Dq2+&&9<&`Z6m!1h@XV>95z zZ|lb^cG?DWZZbrtO6g9}@n@tt1KA15|?^)T3@o7vDqd%NK< z=#d;|(VWP`&o>A^^GiE1@tfa!y|c$h?9=_lO%0y7Enz2!XAVab=Q#1l(UObLPXjwV z5s$GrZ;Ng2>*2U9TwN4yQKH_+?5r5*+_im6|4dUTQ@8LL{ zJ>^=0wFEV@yA+n7XBMtYkZ<u?-!I|*Xo<6JKB#S()r zd-|J0oYd>i@bRR(Z|$vMR?Y=A;Cnu(*-Wt4J37ic+PiD)cLF`tyad|l{b;o6opyMO zXa3}-qnPZ#Z+>(ak0x}rkE5CSG;9TV)vO0%>JfWfp4JofY%U-i=P{h%gw%8iG= zPdCDR>-k$^m~Xc3p0yq1#TB2kK}_-eb_eKpH*AKBfvuk7sYm*wrt!CD-tbZv``-=f z*AsQgug7KuFL~*1U(NbWL;hlmp+3Cukn2joPpxJKPchXaE*_tURoD;D!(liI=8)F7 z&m8Xcj+>Y`dD8Gsu+M(xk49<|YZcg=JvGwbZ@@**=&qI}un|*D@2r2Tn+<)IKtD6; zT)%Dyebfhe#ilRt(?{?4IonwOmYZ(&)WGg+_$0VbwDtSF^*as4nfQ3^1)963YG5Oe ze&NPfY|jsa<+r}v3V5qk@6=Aaov<6skb1;dn|htgdoE0T9G&52&r@&IM|ZyR&|Yog z;Ub?|a^E(BJ#+PLn3#B73kN}L+V2N8=H22em)R75^ik(dFn=_$=prYrEVF3F=pnv! zA&7ID!&bkq1hGzndhl8WTrAqq3oo^bYsTfKKRfUE(&S#CF+I(ky2QrAnI60d__)*N zRZJSleLd(mTX$;}_`D46!)B1jIuq2RZ|?+La1t9|y}<1UK_BR;hcunybEfh6AnLZTiySB%BL)O}+Xe*ULadoa7PP zT-wtcJ~YAedY}#8`#~@DK_2rw-*!67E4F!j9mIbV#G!*2c$=A3P_tU)vk$lcjp@Dw zy`!c2%&~K|UkSb^>eNrX?y81^3qfsIpx=*CE&Ul*omhPz*miOEq_cYg&+R~4^-nIm z@mI6D#H6Wx8nAOGeP8IUF5Djmy0Menk_$hvEOT-;@R!rK6*oTb1oe5!iH)1NrIGoe1>SV9M+3d5mvb|PySb$`4)*j#Oz(i_-C$1~-0iFRdJxNKvL2vkpPg}9~f-}#}unNx9g}eClJP5}DN9Xz^ zwp?PWO!q(HhOfQP=c$)%&%8=SC3wVlXG}mp%4sriuFHq?;uk z?t0G`2f5UDA@J1)wbFx5xXt&Hrs@z^uYD`TSI1Vk9L$PZ@H8hkg1T;n(U)d=Oe1=^ zA0G#I_VRhJyuW&6u8uu^Gg-Pe04P9DLQTCwjog(;1!Y zn>V@jj23?5CEm=seq0WGXk(oTcuueNncwtx?!i`j)34Kgy4U`9d!#q|a=IV%FpG3j z>(oBA9Ji-lGsQ;DW`iwV@Y5%J?fGpV7`P~V3=pfKbUcc##k8^j@?C25gXoAa5z<(9k(nOE+?DG1Z?fKv?sl&d0m^;1V zjoHz0^mT?G&T{dY{Nm%_8>~jL z&IWPVsNeiw3D<)Dy4U(gtI31UaF&BM>YbUGcxtrAMsD%Urg&=SBc}VIM|y(4`15yy z-ztcK7e3bYKzlL12vein55qx_pC3)lfu%LG~ z2>)OHKInlO_QSKFw=_QtF9PjfhF5{!$APY9b|cW#yiLD+Tj?`%puhA`w|925_Y}*X z*>Kl#_1an+bZnTEF8izx(lipo@Nb(jDhB!I@qx zf!=aC{~+*pu2#0}{FYBI%%Xhn1e|9s)$H9I(T9({tuXg*w|BMpez1|#xp(pAH@#?g zH;6+k8jJUXKnr|m$?h6fuy}KIZk@G0L4r-E14eC4-#H8zKKG*s!&hvl^-PFd1 z-MhiOm`~hrv`=^6T(gF+y^Dc{U^y2bKYlo<9rgkr=l1oJ&6O~-g0I@m!B!YsJk1de zozq1QddrC`TZ^4~#1P+8e!T6|U2Zkfia(7mtlxWlJD2*3@6^m@6~we~a;KcrZ2aAC z{H$kT_|lukuhvif#YN4urK$PCjUMjlt@ZDNo-e}8kTcr*cCh_2$gf7c`I#B}=0YFO z1@*|`9e?rYz)yS}Xo){g^i{8X=2GA6(UFgL_e4BA*epR^a?zbXZXX0%>BsaIS3GgS z>*Me!$hQhR0WUS21o>u$d>iBw!#=*^ny0H_>i0f8oUwm5Sl*reG;D^`oN0R))Jzxm zPR;tLe%x?Y*X7{u^7k&cT4`x*1Uk{07W0ksRF`u#d>-7Tmw}#ikxQM|1Fm9QW{*zx zZU%YHj^FnKK0vzAY2N1 zA(p&+X{bJT$ot(uD_qUP67)um;?hlQdb|p6!n*+{_nVG5I^PN6IHxTeT+}x4)TCZK z?*+9?40-kg&VUat_|p_0oM}wgCqd17;V2vjwW7Dv%vk}e* z=a&6jfsOn7EQtLgh^rRdpNE%0y<+?{Yz8q`!P1MnLCw4CcX{;G?6IZM@THF$#n*$W zmCiJh^DwAeZoN^T{^$$tdLi$%pzc2ia^j}`%V8DR;K2U9;C_g$7Qb(VRq*XHn|zm` zesSLp>YAMV`B-fE_)Tvbsc|Rhk>B3w&JXa?Gx0V9Up^Z_JT}Kcol7t$YQ#qkwDJ2) zkb4Pw_*oEx{}Sk}7iwVZOfB-On_l+hbZ6PBQ-AUG&B4(b8##RA_~EQ4dgXq|ML+#^ z7xB9l_{?0J4*0Y8 zo84x3vi^;uJeK%)(T~s0`kl|6K##-qo?Oo5UV`r?-RPx98>ilJ5SJ#-ozX-ca$1|= zRiNv9%fzM;4Gz|@M+02MR0nSEko@9!XT#QScDKX5^*gTbhj)XXn+b93sZ}pE(XluO9w5 z@ZAdXnZv_?4<2lmfXC#czu#)~{wjzqHy?4t6k`dOgS@!wxBBUEH>iW3I&h*dPIOxW zU$$x&Tg+aALazcDT`9pPvRA;Kk=npfAk4-szn- zuLHjGTTH(8+^fps}24@C}s9eGwZE+^>Y|;YPs8?9<_HxEJX4ahU)2 zJn8*u_$-*Co!}goCEz8ex!^0FoSu(^b9q2LbkSG5&jq#Mjx&B@$e}(w`H5k9-U?#V zMUR(o_B+qf(LRkf!puOg-)b(Gy7f>@eAwH=&pZC|h+)R~$Sp6e_1pa6x);>PZ*r++ zdO3aJFNe7LgB$Jm;-J^g{KieZnNKm6U@bv>TrHevUe%P_O=E?C=v|LEtgZH2jucYD_-T=9@k9y~qo1iI_NDh!|L zf!}OAm!M}r>oebCURw~u}& zVHM=s2;#g57QeH>-J2PagTJ`A@cTi)kIwULnQz+Z?+Xp}O|3M>e=Df#^sL?Lx4LPj zJ~Lwf{6>t#8nmY82e)5Wa9Mq@xYS<2H z*a&jcj2`M2kM8*6=^n5X+qu5d21oO$c6D9}_`L}<0(pHuX-o(Gdl_)xuNPw8U%%@U z?dT+@n)ML;HsecBizVl?a1u7da1vk6CD7?w&}%%^%YO;1W}@&eA(};!kY8;?vZV{mi7?8^L|R**kvfS_K@~@>R$6fScIjtb&-w!Eajr zUeE(e+~I@o64Ysljqi>ht<$q*w&kN4|C_-YJ~;BlOAdWf%jBYyc;;iiRc2P)t)RZ? zi<)i(IrQG#;iy-4!@aN_%!pZf5X{HJpx(L>LiZ3gw~D-GvKTlYZ> zwd3X<(~FPz^t~L;2YQ+_I_rl#xH;$V%==!jrqtAj6I`mhRm`cbey zxy0BBVyIOv=lZte>&_0 zI(QxiGqHr}!Elt5mU^m>dM+mWC&9Tn!`sVI$BMKlQ3ZKfrw8^D5}`5@?B+ zc>LHbK@PQBbmuR(b9wRRvkLtA;^EA9WeI%fwhD5ZCo$}+(L3w}nyXhXXY%N$HN4yb z?`k&3`1$*@ggZfg@w~qd`h}ytCCF?3#9hM2K|Z-`Xxc<+bpWV{-Y8 z`wMi3uIbIIQO%#hx@Q`_r(e(u`kKoeT9ms8DZps925 z**JIa-7D)t&}X)*fP?wBuO>6M3d41?cl%R|8t;YuASdnE+zl@SzG}BT+2A1G?Z8iL z_d#y!bbK`pH=3%MHuhKHbvO=|`Yip`Q+{H(hkWh^+WEWjFyKRP{%r8Uf#1xteBRY` zHn=PFJqYK6`1A$eC3`bB{48AL=l4aBOYR#1fBjv8HQaH*bqO@%^DNMm4)*xrdns&% zxvzNGQzK10Ej=|4&x5^l!7R#yzq@JiQJ3e_@T1^6g45_K4*t&Rg2xinNdv#-!_hfT zOV|lho7tl8X25+f(0&PWs{uc`)ohQZOVEEyYnm8Lx=wzwMni}xMe`aWS4wuup`1n0Jdi~otOYh=1 z|73UnkN%rW|Mpw|TJK+ftHfJxedV&X{SI;lla1-ujd9Ps6_x;eQma^s)JO!#@julraBGxZNQA zN8x`De{}J!xBhW>QZfHo__)IV!*Dj@|3Uaa!{4a^|0wMCA^vZ}Pa4pFFZ}cH_xe!% zPvKsd<-Z93SNJy*@t=e*8k)ZjKkHKc=i#^dL;9=mKZjRc-2Xn@`Mv51A2k^NV|da9 z`A@?;HTO@${}%pE7xsS$U-n`9ufhk*TW|fx;h%?pP=o%D@bxc0-u~soZ+`Lfk3apz zlP8~iy#4Ug2Ty+ac>C*5zIpKJ_E(R7@$i>zee>zly+8k}Kl{sH{l#B?^*3+7{VR$zyGIy@n_$B{i|R8>YHD@_4T8N_rHGd^PfL_`0&%OzW(}~uReKL zwyz&N{^g^OA3l2g$ybl=Kl;U!U;gsx>u>(@tAF_Gubv)%^Cw^Z<*$EHia-AIU;oKh zzxef^e{(v;|0BVJ`;Q*ofBe<<<8Pj9|NQa&uRi(ZFCIMj>g)TTeERjzAAI$TCy(#n zfB4|xS6@B-*Z=re|M06n|5bs%`SY)TE!Dq}__IHH^l1CjhaW#Z`1^nQcYpPC_p877 z)h~Ya>p%IcuYX<1JAb!GU;FjVUw-o!>z`l!z5koFx9O2I&C;}hL5K~4zrc=7x6Itk z-OY(rbW~O3RCZ?PbVfB@2&rUlZk`$H$nYQ{a_F4JAm9gpV2Ah+LK+PcNQe)KB}>>K zu|n+F41aSnv&Ufi4=FOOH}+uh+}f3>>2y1YLAp_=v=+x^*Yb#r!hja%EBo15Fi)%JLHxI7-O zH;3c()nUKco$v0O@$~iUSho zZ`X&*?Zw%CyIG&~K7H4IaCiLj+qZY`+bivklXoXMA8g@^&H3)IzP-IVJHNhNUG9&! zSC_lpez(3{ANRMn`}O(tVSU^_$bGSYd-L+ExBK7Mm|y?p)#dhbcX?{btNpw8cZXN| zhmHIv-=B)_qq(;9@ISGCvwyej*t6~CY;!hdU0ki%JJ;LWHSZhOH&^?!+wIxTeR_Gk z+#gnF7l*@+JAPsE$gGn)5AFWIpWX4u<~l!IZ?CS;ZqE1I4TsJ4=4^LdU2&W5FLwLQ z>f-Qd+rE9zo5vg9MV_*+zJBc{c>3=B%ddEEdDzS!6xwso9nP)}+x6=FxIJfdeZD&1 zKzzHt*sKp%7dPAO`R44&({JvM*S&|l`1bnSS1(`l9`*F?;rNQ-%bV|A{6x85{rSb! z=7McJZ!ynb-FfZpY*5`g(tUbvVC%r0Fks`?`CFiqqhC?3s7(e)sbA;rRCb{`fmK zz{__}zC7@bdiRE*KjQe81a5=<=3rf4n|i9M-$T z?couLhkfDb14Q1ecXZY`T22wdHSvJj9-AfrQH8sc)&h2<%0^F z%Zu~lYQMX>S?yOh-1V!o&2hcHTHoCA+xKwXtgd(K_3q;Sz3u74W`1_}=Gd)xk_rJk@aQ(Zxw>QVPKWzBZw_knt`t`g||LO6~ z%bVU&+4TD-Z}zXgeTNnA?swz;`^4G#6`v?p^FDFB*sV5)o85MMaed7@+u`Q&c(J)Y zzq-EN9Jk!gyov0tZ#IAEZ?@}8db8#8#qnZwzFyy6Z#I0u*zZI<+#D{>HoRA@SAX-5 z8JE{=2Hp_Pudn&svcKZD&+7Voe{;6Ky4jwct#`+pi~Y^b*K3$UcP?+H6JBjyghvs`t0uY-5Wl4eSOELkMCY_XFmV#E$?mzF8YP` zaLH#1t3A9IthX2I)q32&;f^~yKfArXJZ?7Yv(5hMf}OZMT&<3q?TU9e?$5(<^+4&L zV(($o{iw#Lg4GqDI39le+*|f|vwiTMbA8~$;sv+Z^*PX6-q3C@c!$G-m$CGz(``CW5%zBzsN_^Hp&U);U= z?hT&+{Q!L$_v`H!`{NC>cW>VD&cfTv;qKMl+xxeeWs^Vty7Bb4tIcM6dG)b(&(HRJ z0!Ig*2=-52>|gKUPn9RUmGP;F8_(Zi$48g@CpX_-^M&Ej?X)@Lt@`p}ZYSQqZ_Y2a zx7*d(VT+|VXX~@;{eHFQbauG)m($JFdUN&gX7+w zllS)*f5dyxlJwzk-`mxSoxeJ;Qx8YC)#`S)yE$z4x7;4Ld_iHmZMJ-4eArNrH|R%x zpYxu&<#+k!dPmxpPjnZX-Sx%U?&kLLY<;-B+kf@y_`2KT zVfZ_~2*3LFo8SH2?|(r0l+T6l`+)SGZT4Wm2lwY^pRW1Hx%yeXJl!AND_`$l^NI2B z^`0-!HS>!{GmpPQJ^FocwdOPJvVpJoU3tat13pXhUb8*Fy5;lE?e*n$d%51+9_R0Ch1#hHJ{^F&#HlK-K^Evs+Q+|;zeP|d`OEv}?lnui z8NGS=)z`B_fBCI9CvP?N@c!xvC@=r*Z|~l+V;{W-`KN-*^W{D0=9*tVmm9v6UR~Yn z`89iSxY(W__B3+O2IkitUsR9#k9GSrZ?~Pv^{{i1V`m)5|TR#2`syVTYJx!i2dbiwRyPh-}qbn z5BfUB-sVoZTJv|x4c}BYdp;N6@EdotyWx8YZ~J^cLf*xPVov0L;{N&UlS{tu@crv& zzb!o5zx~eb#BXEYpq_rS=l9tAujh^7DQ`(#va;t-tZ)6L^6-XqvEK2wo_S~g^o!jk z-*~Qmyt3aZ9={=daaTo7ZRKZQ^GS;jU59TE-$JIpnmqgxIp6X3$E6hC=vOy%cdtb-be&{?Ac+h^fS@HXE zZ00-R`I^6|Z1@8?9~k(wdV9F#&tC`rxWda8;2xj+;>ho@yX%+y9>IoJNSja8kAJD~9hPqZBkSTw(>8n;-(8&d zXT0@#&0k!(uhDYGUsaB~^Ud-0{NaP`&-x8@{tV+VZP& zzRl0x`1P-@_}IfYte^d!_MER5FZsIVt@o#QU+rHnpL6(~`W=6py?giMcF$YPUw^+O ze>2)GciFE$U$3_3KkpyKJnQZ9tzT!X_x;P?-R{46`RcnL{$1-!{2KHU+|}u8$fNds$v@rtdf%UK+n(RMj>`u#KSJlX`?qiJ{vDd}-Ttq> zefRZCK6iZdoro{Ge8XNof&TJ0JO0GQpKj)F$&ZNr;B&)VMu@kIGswB?{hkt@vs%v7kr}J%vQMKQz(B#zU5DheC*tu9XC5}DgN4WyXFrU?D|W-T%2wB zbNT(>LY}>R^^)IjeAlPj4?iD##_%7$tv_;;+<)rJp&bAj9{ITU{-$1|QJIJE)4Zk!i^~K${{?n}ell?3HiR` zmp|_54?pfa{#EIxEIt67ep~W)*sosRz2ci3mD#HAzS+Ni{qiLw9vA0Z?CBlx^Gkl0 zv2i}Q^?oSy@$Zpmerdn^W_d4kyWC5BbF+WH{6k^Bj&GK`?X!P_f0)MKKt3$#DmwEkjy#5#dB^+P> zGq3-V*MH3GKU`k_Cg+VlD|URS)%dHpNA{v}@j0;L6n4EiU${_o}We{udldHwIa{x@F# zE3f~F*Z;ulAM^T0%j>`5{9p3=FL?duy#7;O{|T@EfY-mz>)+$`?=G)@$oY=fHLrhb zdHp5l-|+g9*B87#=k*D%=e(Zr`qz0~@cLt3|0=J4p4UI&UuOD$cs+aZ?AeQ_&z?Ph z`r^g&r%zuzf6fF4#!p{7d+~y?E1X|&VxGhEXHTC#d%^JqgQriQlk7==7cUrr_`)?Z zo<4sHfG>}tut~r_3Ns zj1=Y}Pq;jX-1bg#j(X9H#7fDRdJd)-0^20DM1N0#Os!^SxkmHy9sjubE z#GPbABqv`RkRn0#`)OC&EC-#jZ?3TlWx^@RHPcRLFt$!6EVP&_yQ)~3@{$*bhE4`R z021koI^fw4X02++0En?MOnL+*1V*xpzy2)z{b7mOLlvTK-BZb_ z)KSstvn*vQEf-ds7<0Xp&@@CFb%-a1+Z58=1c2Bu?bSf}Qyeg-nDBn&E@& zF_GHfv>|L65!36;et!7sJrdui^WbrYxWGFD>RVa0H&PFRfTZMOaB&SQrfU~d4i%<%N89ZaZw%qbV$ z6?RQElSYfDz;JvCAv(`jHdlYk=G6*639xn*D{ha#wQt5G(b63auYFv z4~ZhVK&5mTI##jRNDPf9mP(1xWf_2!0GuE%Jr!fEVAoL#4g>irC;4&~PUV)jY^|43 zw*$-Ov5-*WI1>&OS(O&m3sa^B*IH>78llA!jBQ0%tT(B64BhKzT?S2k4NXC(EpSynrCH z?k7XKFq;v_Vk2b}rJjS%m{A9<<3jrYI7X6CzY(q&L2<@|Rf;>NUYH>B(~p&_KERr+ z=B6#nQ?|-S6U3ab1E6)S6Xm!UnD<4rB$KPU>5i>}Ou7xEikw+CwG{?bvt!ET)ds46 zdTU*%v1$WkV3q9AOD^n26e3j(322ThB+QldyU5INT+}!giXGbdHSF|4Dj7S;3$ks{ zCkWXrc|j8#TQI~7k(lMDvOW(ZWGV?{%fmw9n5%k7+h#@E!^Xl>pirHXuum!(9!Ccl zYXz4o+D@AOOgIU$te;?$p8vMbM9MNNh^7N9KUnctmLwN0&bBr zs+Xp<86)^~?f!Zn$xN!tbZ}h)8;AA0cLfs*DVN6@< z+53zq6#z3yk(YpSwV*yJPghPhO!p*4(O@ORQ-8D6-9j!Er3DlQHsM%sp2~_b8H`Y4 zJ1S-ZEEZR6oY*F6!9Y1>W0*&)KxQwd>MG_q9k!|ch-tKfTZg}jfl+mPjDR*o97&Jt zP_QVMBTgNTZ1Pwk2dF#m*}gIoo`EJ58Dvw0vnU@;cK~H8$9V5GWL=t~&@qUc*gXUX(Xqow5uH z_`1a*0~Bdlq7Ch3rexebRBMA3>>Wobxf4FX9tE$ERlV!I&j>RZ=Lm^2NwV)@J|!wZ zmI`?C@+aaak2v$ABJ^g%C36&AVWEsqUuTs}vMlOv0~zbDep;wI zwO{~}2Z}#38!L$s;XX*OrKd(7Q`G?Zqk|u??Vrru%`C~^AB?JfdsHQUDR7=}gd0}tT*_BXCnU&ErpPq`vq9V}remRmS?l(5+c{TJlXVpG9PAX&T(z{*FtS7z zw25S1NmI19I0Rf5s}xGAiADu+G65{> z+jps0WAt0wX6dCPW;rlcylY((OqyU`Q;(z6D-JZ-Z~)W>!bC&`aZz*9JAJ~fvWS=o z&Q(^0sz8Y>$LZcePe0ODN2|9WX+A}*G%pqfi@i(-Q#CUk0jL>}BQ}dC*8y0j%?AS) zbmjT8`I8oZ$I_`&95rhK*WNbnL2=+XMMQv}fQBl9E9w^AF1xyb&+kTjdr?hYW8HY z#t1zol`XTjKxxC&@+c%tPl#6^a;Fq&V$~Z}%g%jlWrWDi3={a}g0qCR0EqL@?oGK|=BLmJ~Mt z1@VkZLuGO01TU7QWX#lu_b>t)G^>qT*v*7A1@K1@rD0PP^-H}bjlU;{;R-UvomdEh zH<#pkgGELI)J1V#pjlow$5^IamTpUghvqcFB56i0b74NP5_OH&Je^L4;2J4yY8W{O zfUOMIq?*R&sz+fMD2oL=15u_5(}HDl`@H<0?=Mhiz!0^P+FoJS?C6F08x zwohz_TgzBQ{KzoOmdGMJi%3I6Zzq;Ww*+U6AZ+F}mAOdRKzA1t&Jm>Fg1GVW4;?JD z5@&Diu&Jycm^V$c_%PzEulWSgYS;>uFwIfV;LDsY_6UF+p;B!&aCmiBKgMDtv7wVUiWk0D-8?91IDRm0-HE zL@>)TPjlztfLs*v;w*%xAi}k13_?+G!R-ax%7l%gicr}`ip`DQOh~{9;W?Wub23vW z-HBd&V+Fqo20(?WGFaZ0b$8h{LggH^0u9A}nJjH#DJU0nRXrvM1938#ua)jv)|5J1 zNvebHwJh9-2{A^N=2nY?q__~+Jf{U9v}+GisP#r>q<<`# zn{E#pl4RIenq(;%rGxaIpziP!sHSNrv-}K8&v6h~h_P3P#zswI2(%)!p(6%S*JOSa zqW9#-b4bg}cyKt$ZaK(1R zD?x@Q{zhg$0P2^un`E9Z814c=*Y}=G252ejGb+euhGc7$}Cy2g(T0|lHhordT61DPzQ0oz_xVd zDkQ$rRN*Zw+YUA=j;UctTX0=&%SH=d&^l&AmQ2zpt6koB%dCR+3ILmyKhy%S7DJIW zsx$a~xEK>E*3wI-QE21b;MBIJf|WY$G$Jr}2($8* zi*-H4$nsp77ZuM|VuApU2w|!^m0=F4zUR9i+_P1dJAv&UHBinNg=?u75*$iQil{Qk zOHz~^^peR%I~4&%f0hUVH{cbSg`rCvL>Qb94F^uSrpwaGb97{PdxV!*8Mq5{ghJ5P zpn#YFx$2AOFqpQ2W8etjLLa)Kegx_#CB4oS>QIP$!ZdwMQDI0#a+bI8_R@j4=wLu4 zJ>zl+%F#Y>h@onEESt$m(h#Kq;R2{e4FJgp(?$f!tXPbBL5@`16dN?h{VPclrB z{M(}PMg>Z=+~uWVqA@EVSh=DQV}P=;jwaVyEKauFWl;m*MYiD-#mgv0m}FwcT4oL9 zf;mIw*3Tl#i~_g@5x*%=JqIf0*qjxnXh@l&bs9TM)@j=;PO3Kg*4A7^oCbCeJZ%8f zmX3KT$p~ZZ07)9{y~5K3=X%vmGZa~eJ!qFoB_(-$GGw?k$}y!5C!Jh&f*X9=;?Ygp z=Cs$8re4j0trdg@148jarFVyBB|D}RCR!x7jE$Y7xSYDn)B56o9ayG{=-o!K0vocwlJ>DbHp#Bqw)jiY?ZB z2$U6vP`Zf|sWCE9s!D(b*B!)5XjMlB2)GcV3XO}pvl1D}UPvwwHy0xz5+hRL+ht`~ zv6uE&lvorHK^-@WHQQP4FRLM^%B0B2_bk`F=ot><>nn% zrOd-<8L3wR%p_Q6NU)G=L4j73wi+e01+?q54{eqGXwD^V71qbHA6-O>JB)H*O;yQT zi-SyLG@J~Tu5E(8EI3H5%mJ#9tc6{Wh&e%}a`b70r9D*6He)rx-YGUyY-J?aeFya> zbks(SHDpU)+kq0m1%VCXnoCZ>=b&ur67kBTOkl#YMIc4dQdApf_eo|EON@qPQd)GH zssQB;%j2a*bio#L^g?M>XnPW9$pL|T9d%ju^R$%GIEn-m>*8>=j3}l9!Vt&5Z9R|b zT5Lp3o79tn^f@PGBXUhxu9;H}f%e-rFyU;wkn!kSHiO$leI*X~k~*w&ftVy~qFvFW zF@)rp$3LSmNeTSR^N&vN4T`GUKZ~?$c*4~mpS*O2U6ic)rU`OnmGya;IjUWXk5VD& zUf}^vHT%m=yKTCUtm?Yj8kP{;F^I{m6u}e@_uoD#V|Fx`eb7BH-5YdjQB)l~=&8}s zQ4v!RlC9e~qG} z=x%2uBh|Ss2vNGA7liR@(A$O=7CG7LwP+TDbaEDX(~I`VsLYq(p%ey1^(arA(PQZZ zNReSiu-lXxtW;XU-TOr|j;y?DLV68{Dv1H1))rR5cvdh36Q&-El&0^eS;h$PDsyw3 zV6F)hTSE4U6ryw!XH1&99%08aBTZ77Bi28BFC-5Mme)?nI3S0C3@j`oH<2B%SD7cE zwNqgW!|(Q>5Cl&PrGmpm0=$+i7c-aw5~)Q-@hk(3=U?&P@9<9Z75}Xce@|fGtAXd` z$-9vO91pX;2pCfj{AWI2`Sbnkkb(H-j)r=rIr0BRsL4MCI90pfBKJ(8Zz&!r_-;G zxu&l((dt18SweZro%&|0rg0ATUmd2Or{hc(W87DJ9g+U*wGpJ(Jccl_|3Exmk1b;ZNz10_FCP+#JRP?A# zZU?pf^xF${qsKw+?w^Xe9j$D|*@{z7A6QZeB?*EHt?in2C@HAmi#2c2cFtUPrJ zupk3xvX;ColCp$3xoSPi7?}blodb%4ZShUWC8Jr>K>|h&sZO?IO)A;Owx0ZUs414^ zC{5Dckf3v%*YsU{h1fGojm`Q{mS!j-rl#^#UUU;DrfpHV+{II@Dwl3p*xh9=%gNK; zTmZS9$3*&-o!cZ=);$uk)>zwRPSB2W)GD8|PQz#D+dFk2CkNe0Ovo+?ur-7$+v^)+ zf|DVnL?jUg6GmpJqx7&MDbtu)RF!75;p)^k6J(O1O7hxShx2ls8L#A-h^QsWp2VFj z$m+iMDQHbbOt35$w-Q#80yXDA0vhaPT%Uw+K>*;}&r)LEmo~G+3yyVaK$r!WFgeM^ zhf^%{_9AMnaF!8~I^-rF23WrY0#R;9(Y$*4qTdXUeibx)>TmoWXvr=M%YzFJXkCVi zksPabOv4=eVBl>ra)^);kA9zdaOz%%E2h&8&~SFyGaA!FcU#L(VtAx}PGBoKvV`4# zN1?$)!KV0SEaB%5-(x0NBlr(}`Hy?ycAvlByGd-q$vpVjX7t+@9$eEoc~ah9+DJ0=4W7!GeKoM8nMwpnjB~3Rl2DW~c|hdU25{U8_Rh(r zaazT&MuK;*m&jA=-DMIogV#WSsv1`qjpwW{K;g5%L*qRnpce`EogW1np+zZ`a@J7C zS&FMj3mB@6Yhy&cXS?4QYQ0e{CHbb-Y0HC~GZ{Abn75r_9yzJ82Fm>rIyUoAvor{n zvgb+UdQ5Uw#?+~raia$2^P>nS?I$rF>}m4mjpQVsO5DP1Vz)b$91IKJDMYa;Qqv;v zMqFf0MLao06Qc;OW!Qt0mU^q@OmHF&2OzvfmphR( zl9ma|D5J_sZjDfjK;Nknheg}oio`3~wT_V`SQ0*CZ?;BipDiIsVJ^d5OYKiEGPPSA znKDL(Gcl>;Pu;L4tb#f`(PV)LjwTkIYnrss1Qu#8`mE{1NyAcBc0CeiX)(*X&BeeN z$aTW4#WI-%j4*~)vw~~I*h%I<_rPM^Hj#;J5<T61=A*a>y4}0Cp30Z7P_Bj>vJQ!!9kZ7AdrR&FNvf| z{!`g&;&sT-X6NQ5#De%i2nE>lpqR%C3uLcq35}&Xi)La4(>n=zHC4&Te*G`esE3}` zBqt!P9|R>)(?qtak_rIcb_f6k1?zSa%u7jJq$gL24sp}qNfEAlaZ(CLy|+Kw!HFqw zSu=-KV(#DlnH5^3SyQJ!Oe&jPkS0l0!n@?oa$9lKTKfV9%QgzA({Djx2i!9&b`nH& zgEiG1>t^irTbL%rHNi^`a`fcpv8R;db$6abA|} z7fY~6G7)YStBcJAfja5hL=bCHuhYksKxME4C$Plwi19IMpY|Wwoc^PmgRIE26JlAW z$3V+YPOv!H_rBXsy=3z8tOP}H<=Li_=gr`La#&**z6NvM!-B$SqepA8>OH{$JP%5n zN~fKt*+owCt!fP<8+cFE!C|h<&_yW{qOf4==r|)5-QhT7bY*nd-!_vXyXQIT3zzVh ziDF6CZ`&`DDi~OpbTOz-LcW!3sWKrocBq6YuPjC8(v{}GQK)I6@FD;Ri|DBJZ5U3- z!!xV-IFbNp8;EY88BDntV>#N!Iz_}1tUxsq#S|P+2bfYFj11FOr#|-70sHh1a*0NFICnoqDH91)rDt4ORd)w4MRy8fMsH!*xC%dHSL9J zxzt&&^h12DFh+Sq*cWw8|1?PL%QZNr#ZgauMNV(vKS)S*gP5Z%UlaB{URQWlDV8!TW$$q)%hD*gHZcWr_T;EHUNQQu2fw0}Evh z7qUENC?%IBvpNUEY$ZfwO}<-zDC6U1S+CrLBPKyip`eu-F{=Y1VOD!;H38Idq_B1t zmkwq!n>2*WoUu(rT3XS%2m*_q^~HhWI*UHZOc@rm%8a&VT?K~=f(OS~ZSWx!)s#T-CVG~LMC*|$c3*iDnjTEt6Qr>Caf5T9jYGAq^1 z$Wf2McacKE=?xd;g=$D(t7S0VmMY$fz;6%k9L*=r9o*#o`Klt!6{yp;=)M| zghs^ju5plWlZi7_I2#896lDaHJZ0dH@mA((?+vY6?ErZiVx|?L-kE9ZaGe-v6$smA?bF*jNV=}J?T9Ht-mE&$Kr%554yxV&#QP{JYQ(T^xaPEZ2jN8B zP#^9e<{(9&$V)Y8=Uf_UG@%AkiX~yp2+w7r;214| zC_;qGaZ%%GP2pW;54eU5@riAtv^PX0SS#WvhXqs&{Y$4oUa8(&T&DxB*0L*DnpT0} z6vc?q3vw+00GD3LyfNr=EfM#P`WN}$c$i>6$WVoe!*6j(PV%8tg%}VlDNa)(8-+lm z1mi7;FXChwrv`QaC+(%gbkJ5yS>^)@56CV}_5xh4Y)*|;c4*bIG5BI>7JwGeAI0(r z+!DG*jM64#`t7)#PzWWUA-w5BAFh$(3J8LsBx#-$wvZ&bu!3}5wKQjY#O9D6A@-MW z=IWS&t76O~Oj&}(Cg1(S9LKqN)Gj}PDgq>tc8RAB0Og`*i#eE7@mQN=Eh965)EZZE zz(NYl6pNTHmg+e%rPzg;{A4gttWd;A9w+BB%QQ9TnnqA&fg#%}9N0#cC5+J7QUfPj zJ8aoR8IUdYR4Hb*e{|HP+Q5ZrBugm%%(FPEm})KeQ3?%6FsI^3<)C&zJ&+QOa+x5{ zrf22&%>>o8$vig!!jcAerztO(u--aUbedQ>gJ%Q*gup%KoS~o}hI%R)|6lKO$)kK&jSpLcgO&`N5 zOycslstQ!5U;MN(mKlLP0>n)1-5N}4iPa^`mF*!=>joJCJhdkTImp~2N$Q;+YJg>Z zO*2Ci;p+OSGF7>jDb)--wF4grS{&@0eCEXRWN1iQ5_^lxoP>?%(z-#3y+H8=fJKU~UDc znXr;oY(BtU14AGoE?H16d1PtBGlPw9ysO%82=gfjNtR|wk9l^kjdN=;2^%uFCb=1^ zx24<$Qqe`3V5+Xe?y->2|vLtsS#xzUmUk!2?4wzZ35-M4D@WbMu$ZF`8SJEoVTVT>zr+*d4 z3p`{=aRdtJob)glavq3b&aGbK3C?5FD5rOZ^QeKTqco4ajzst(Xyxox?RR7mmf65BiQu~bambH4M8K447$ zFj*}31_PgRw`YV!-kPinvHVMV{BQF8;BlJKo|8-=6)7zk^b|Ux@MGESGuAjRRceZ6 zdgNEOL&Tv;oUsy=Z0uAhBG0rMz?wnCoc5R4lTw(ilLEY%2K?{0kSyn1wta`9THsvEd*0uWp`-VUGH0u5hRkM9soH&${{b6 z24#p<2_ms1ZIZe*r4wLxiD1a0jysC>k{}fe3T`cX!hvPOWX_^sw$zdwX_UAW;1WA6 zZy4_gE{VaFK0CnMkPBxF3b`tR8N4$&E-Z*Fiq9&zNwSvtYD66p@+?0vmwZ=<_C!x1H>Bu7XM;*uw2fAQfwmh>@;J`exmF0(r(i@uglMN_#00IoLs{YK z&lrFV7JLa)N^XilT|JjK)sPa5EkTQl#gqs$1V*!lP(Uh1m12e-=_rp;LyXBMTg2A` z=YWe+;1ti&CM}3$@l_#tcLL>A6ytjU(5fC=HXK=4 zUMPF?jb1LT*y;e52pzR1HS2SEJT-$9yE+(_lNwjB%x43j+QK-l7Q;caf<{VBAgv7_ z5N`sEFqH8WjBsE;v1YS4+5k6Xev=0VG?)TicMR67SaMQ0u!it}gSi5UGIyMl)@RCk zK4ojiJmspx+(kpKsx&)DHIxAAFgx5xbarCoRRo8Epv*EH3bO{UJeqDR2X0Pc2RS|= zhGVWc*_1j7tdvlKD7kJ+-YG6L-ky&MZYMA5p3Zcwe0>sLRNF=ECQCFq*I)s2dq*n0 zBuQ(ip)Lz$u?m`Ei@V{Qfgj$WM=dHxSi1YArtbgorOX`*@{kti11B@1INj6pnV?W2o4?xQ3q ztvo>ce01)EbN|LgI)T&9DTynM9+Dweh1BKY3X@in z8$azrJ!6s>M?`XE2h2?@saBF`>=$L`=$?d2<)BD)`l6J8+UE_BBTQ(yB{=jn)y7pz z#;)cTF;dVWN{3@>RN@6p4WG82z90Fo&Yc=fAp&h>RvWAoo3C8f7)wp3RTFc?5Kkpt zBAZgI*lQ6IqNmv#)hN{iXle+c z3S?nu%)hITq^m5HK}{I>^9ir|J$f)R`IU8==01^wJb*J8Ng;pm9Ub#;8Cooo0EW zI9dfTE0hyQ4jNjkoSn{t>Mcf+q{x&FHXEjQR(F38P^wEqnF$>rD^hM0?gp;4Y|y<8 zZLl>U!x9GB4(SU!c4d$9)k=ixGLijH-C;?OYfz-N4W|-z)gP zGV$!|SO6vhvs?u@Ea+n_eW*r+HAP!yyMzD>`3nw)jWV65@WxWqnR;z& zRq3t~#3WZN&)kpSV8NYkFw!VjDOGrXP!I&lYam04WHD=iT8c}QQJ-Z`hHzrZwI$+b z-)iFu%(dN{4DXoJft+yTP^Og;r^WTbvs+kaWI~1wHW6;*>6(a|H@3P5Oi*0~G|(s6 zJcM-~Fs%qG9O1kx;)UA+h;7{h(u-Y8T1jtsib)J3JyxP*Jk4Yc#4$<9iwr#mB+4f} zEDr2U=#{Z*X2E0ybZbaVv4m(%7PT8$w~!KM^+ZE9;~vE)1^|B01wSz~0(1p))~#ey z8xqiURZ|_?K!e;NTI+;hJcQEr3~T3(ZatEg>|E7R3Xm6MHi}BB4dNiL!c3KbEtGGB zOhlE}X0en<%J8yM8s{vRq?hO_JY>x&IoR>|b^?GT;;d&`)^$qlVYFgPKwygVf~9hT z&8A{1ZLw5fYr97g0W4cyD3rQ3Ph&&YAzfv?+GJ?CBuXr*%7!aPNE0AKu7Q!UX^Qcu ztsjm+q!13A@<=c(p#<6_O7g>e1nVd(6KSqdW2Ix;jF8fL0C<_=nQFKeytScz#)ZX9V5 zodI4LFJhSO7Z%MRCkx!jI>5Ec7E=gtr~ropTHn|fNp#%}F>y7g>yZGGxU(ZHs0&t3 z3Jdb`DO*E(vu~LHFU!lCtP^p8cc-(QL8GXrivz{5GIwuw+Fy0eGh! z4DtsXIN51SwKNbwW;C=eIV9St5EhngqjBE8SKCHGQB5Y2t=~?7>A^|PK4oYj1%a(t zuI;PSv&XwbRZE7eLMm7;oOQDd9h^N#g~ifqd{B-Vv9~&RrqjS6rWQQlPX>g znXD~j!r1|45uHm*+oMi00KkZ11Vb~e&q{C*6-5XKV4=?=Ge+o9hI>aaXqtpE#taP! zV7O$>gi)r4+YLx7Lz$=dEAX&l3<6T_k>zQq3kKL)OH`4Wwk`4$Vv}^KI1*h{1=of% zq_p}S8>O!u#|848rlP2f(;~mZ=H8x)EUZO*0{K?Z_y!ex+pIN4!BnAXa7&dZng-gv}NO=Od0PE5?uJzR@#}+jkQy9xv zE)%CsVR0=R2v3-~%Yp>W9Mi>$lSpx{VF_9V=kcW9CC}bich-W^+;EJvge5i_LC<-4R z=>#qfc?Yk=NkKi}02l;Q(Q;~pwIeLxLo1k)KfB+hx4@J6*jMC^#AeShw zv{=Ww;NXKWYJ@N3vNw=DGUKlpekN?j;MZ)E;UkiSWHEux$Z&*k){w5O=!Ki0Rhuv~ zZ7nIXpzqX>2zi1tS&mhRv8CRagaJdGTDWj45lNO|Mo^HWDYlGNGlgw7W=%^&%UQ2x zMwUl?(mCpD)__$ta4Segv<$Rm5U?@1C;|Ap8Ot(~+WmlPAoXjoaX>jGyEc_(0AU<+ zjZLkDR}vsEaFF$5>MkDQ7s2W^B}c{6W+e?@Xk?GZySNY~KzogIWRn>%a#mf|cZw7Q zcvgo4U`8`jT$2(JPOVjfkyaDWD-8`f3vtp27I$3(3LvS|A1p*mnb`-mC+6WUF zMxZ&tx~@vSuNz2_qz;*>6l?ZV0=yK&9D%%Q5=N^`gWzXy_Xeq%>72om$W{gfasU9h z$(!4SqRd*u#<;B2#st;Gq>88a6@|Q+b{Sz1H(dL&$@>5>qa`f&XevX?%W2&`ZY>hy zssz0zH#?XHiWqW0)wn<)K~_`5q1JRxav@ZqjDgN1sbi9?q%;I5ZAaiUrWHP=0mHv) z5(K9@U~$)32@Fu-Yz1|OmXt&_31qj5(&sK9zYS-+vUN;2y=Kmqab;m#IXR(Y+E%Bn zSV)k;#$PK~DVI<|+%-1URPEGuN78|m8^pj8v$95n!kngPOQw1$l-cPKM6kB0krbmQ zqk2_sb9F_KB*YMy%rxIc3wY)sfHPjXlLCdkT6i%6M^Hv$`s2`ePDoLXsA%&*p=I8D zcWWlf)o9i#kr<||BO9e;3qe{hv-W)bchAR_5=2?&WCYVA10v)|eUWFNBFK=G0Fow% z3$K<&m=ta48qpm#EXr6wtT-%Bc(^ROwxNMyf({$iq(?Fu=4o`bt>xenVer8=EAtT1 zS&ES)v)KepT7Jk8P%+PfDwzW!l%zVRrND)T`_z5s797ojth`DJogo+9+BTm#O_ZH; zl%a+=RwUE>91<$0mk6sWK|%6oWMw#6%wQnm#+Ck@{_}0AQy5JRjQsxpqKp4q&+V3^ z$1DNh%Ed%$P|m`lh*w{A&!>CI0F}5mxo`qN!gPwFxy~#XZdcWiu5O?4Clmvk%_acF zb2$!LAs4Lz!7_}=p{Jhhv;!^f3_fR4)UQ*Lw}!NpVRNJ#e}e%~v;(4rVJXGUlhql% zJh*_Oc-9aldK1EshY(r_4a*RX1oNm7Fga7B!h)cM`b;MenJErbnhjWU)*u0N*O}yr zi>Jd=_VUo=kp{NSNtdLEv>{BmqqRuO+(`(V-6kJRgIhNlBONwHU<+%EY9~jv-E@NK zwVf?l%UvU27*dOo3;>e{kaE)pjutY%ZdKMybKu%=8EcFT{ZYQIVHL$W7gLHk_leO` zuqKh9wHhamFq+`jk%$HHLwjF;(&qQMQrlf<#>L#sRu>$121y zwuK~G(Q6=S&Oya@tMUUUdHrp#4ZZG1V8sRpy6pZjo&Li4Y^0 zabrAG3fI)JY<(xlz}Yl#Di9pqvYJC4%F_`|BHo>&u?C2TQ|=}LAmp$FQwh&Y#q%?) zao@l;t3Y0YVfQ?VEjVMKIcGif*jR4KH2~$pjiqAU=D@t*2%sB82=V(b*Ak4s`lj9Vt z5hWl-!xm;hn4o9^Y?3yKDDT;A0s|)~&;~Wu99Ed-MXW8BsUC6}m6Hl{b<^mjjDS{) zfZIT3Txp^~Q(ipMT7nqkh!P_|6XfEoQ+9#*xdE9>mo;5Xowhil8oXsB&xtrmu#cIV zrD&0RRZ1NBF4=>#^~(-<;8wk@@-_H zHAYg3Os!X0ysJR`{2Ocjzc`4t7lQxK45s-Xm3EE)3x@m060Xx4rB}l)Px8vGz?^Db zrWTjl@9cQh2DB>@vD-scYmAku`6k2IFooMIFWvoP3@Z=ao|ae%6rYQ|_IDCVV(gk7 z7u!eGD92N{Wr}2yVvLnM%A9DS-3*JtWblb{abD-N&*NCh|4T*o>8}4f-8g%A_*X(_ zwqt}*fl^Jt6iYzihxgFOHnwDD3q!S}#3Di?wQQONoFX;V_^Fv&%A_K;{L}2~>0h-$ zx5iX*&%t0i0aaQ`t=^*T7fbCXsJc1ok(+}{udv(pdqLN@@<-dXNPqmXMFh~nHu}=N z0044bk2n%w%v7|sGkUa>#cKCa#Xw_+hRN0jVX4I0aP(XW1`2RNN=x!0rCWGxa3=$k zhGi!cJRBDxlIBN$bW<^Qr-9T6cAleYFvBR-ljJ;(EmoGVRazG>q6`^1B@hl}BQ;gW zgeQcPLRya?S5mT6dLC$K93se;7NZ#eBbA&{Z>-ywMjOhSygmBHX&!+jrHBGdwGnK3 z1?)Qp`p!00vPsa0kNaH4T=IayLsZ74hGf02y>BX_N38R-Scni)nI^kmJ*dNx3Wj>D zbuj&AXk+A}8~_Mp)C4=I-G2+4%U+yV%VsHpK)S7T%feHwEJjO1D6e)Gy1g(Oq;Z&; z!sB-`DRFHHQvjeUIRF5vMIz*fCgpiM36)c%N~Hm*SJv01C}W)jCb2eavyBm~T7?Sc zHYE`ZF$-cCA~CZ~)ZPwe20eC)h&U9J1^8jw^d-tF^X6m$c-J9e1@O4C_hRI1k=je9 zM45-b-ZAN-{4L-*laycfFGaj+J_}@@c#=%W3{*zW;x?&g7;Qy+rw<(M5oB4TPTh7G z>4eMA0Mwu&XDZ}1Yc9YqI+T==o&%H`8o;uSt6_QX{*o9TlUp3=aUsGu=FkE#YV3&) z63xMijED$A-O!RiNp-i&X4PmiwOm%O=mHU`oXXU)sUve^&|zwtbJYS11<8VBPg8D> zrD||SgsNwfg08d^Xt#0AdNR^y!sNvunNlK18W}*Iy4}a1wn#cT)*|HTZ^Kx)<(A6^ z4<1)?gNk`Bh09z56EakZa1u2&uwb^KYE4$#vI4l2DT0kg)R+2MwQe!fTr&i%QdhP3 zlIOY=XEmCq5@FC$qq1A8b&)*nY-D+CVSzLtrOKQGLV(xgAHRc?Vtk-vm|(PyI>}RT zxMWq@?ZHyjUKXL3Up zOoq`QlZ$mjD!0Km(hg{o%Z4!!f>|bcDN;r*#*A49lPVr-lWci1-Nu^brA_nE7SWBm zX$*^wb5!k0QU~ZBZK8(-z+Kc%f|11Y&VD2-eN$HjBH19lOd|=CwMs6iT4TBQl(sVXP&1rSJ_G4!FFBbNcpi}J zrfCgYMOZe5^*}44BizueJF4g9E{s9-5;$e4i&iqF$D`&jW=<2SXjx)gk6j{1hgmkv zqU8IdgUaURES0?q-u+9wvx!v_L*}f_v~fBvUPUF(0xQHasx@FSKJ~C@wUMpHe1wDR z67$DAA}FS3-b6EsO>p?z%`-@`Prw z34)#RR&txVVl9qAan>W|EgA6|k9G(k#Z{vd2blXv?mU=tKnTltn^idx`2qvB(~#)l zx?0AqdMz)P6Un^2DZ{eThyX!42m#Rtjw#ug>%EwTW-26%xy7fRGj0SDq%J~KNMr8B z{ogPg_~qP(l-w;msX|gzgrOeS%mu@;cY_L{%PbS14o|e(TFb{xI(AU8NN>v05d97mOBFULPJ z&_vD{9>u7Z*{S<+^11M;PpFLuHIsX&VXK$#v`|kT>zN^MW5uD_Bd2aW@DEJNT3Yt$DXH zM49eOYZ;C{X2L+c=8w(7z|aFv23gY{^BvACiz#n1I)@8oA}Hm8qBY!1`Q?kqOoXNc zBE5+go1&%fm2tL*Qo%wMIBuzSf!k7sp?cC>QuPUj2$P(}lXkgcE^T5lhe0BO5Ix9Z^X!2#$|9FRO@pmHvqKDZ&#k3W zRq^7X`6gr?0sN?Zp*aZ}FJxQ6z;-AD$GOgwF=L1Wxmi^BC0R4cRFe&<#mM%-k}?Q4 z35CVVj;GRX4VAQpQ>sKzCOLM^IH8!E@ia>d2rAB@gbY-RlwHu0pd}EdW*clKz@`h& zQy*+RAX1raUQ5jf~E7fUIEoFOgFC-##> z{Uy|s)vZ82%S76V_<5_DuH6)sz?@_XMF|?nAR}0fPk|DUrm*_tr2yb{vY3{+cdDFN z>2l>v{<&o!MX-*_BL>z1UCo{;Z?=obum)x2h%!6GGO5Hm-!^ns0X`Jwi*?aeB!JI! zah)lGyC#Of;tPREjFn}nN;rjLixdUnTbgO{eMLp)y9moseTOg-18s_dm(x}{i9 zH+&6nnmM_XDLwF)1giq&wet$gYY$#YR%6El`9pwugJ=x`bXgZU^JC(8(=O1I1=~0w zeja*pfTH55P%*q0ZVi6dG#1#MxVV*cy z5J`g7pjKefH;&`(&f~d3?Un%3(IC^s&>n#V(kj+z;a2J?MTTeFDs&8F}ctIEzCUK8q?qd z4#=9IN&?9eOvIc4&b(MUkxnftAg>K1$W=Irq6DzIxix5L2n$e&gU=9ni{&l+%`uUO zWSklQ><%wGL$Cx{{}VueePvq&IbOqfNuW=p_Cb1Cc`wM|LEz$AT{O@~}9 zegad9CP$Y1D&`iW`mX-!dwjG>Yt9chslg@6RU={yo))Q2&N`xa`>3(Xs*U!?M#)D~ z&d#Rd=(U%Uun19PGH0Ec2x=egr~AU8s*z1K6C&a9ilI zv*XE>Aw?F`O=GlR&gz+Drp#1>Y}gOdr>G5KWnt4>-CsdFDPU(oK zOcM^3QOtA$mZZF3k>HbS5*P))I1gmxgtDq9u@}Bv!pB_F90$WhcZoZ^G!=J$B=JZ0 zh`eoq+Pu%|BvZE36>V_~vOV$+VdED^?LZwokdYCv5v81oGu64Vb7WV`8e!TQ38+#t zPyjG&EG2;+TI9B=1+~o$n}rFCtxm`m6~MU#+9EOsM7yT%Eah4OD$qruEKW(Q>ZZKn zqN&!hJTu~Zma-F+GS$kgnJjn1^pput^>jqR4#!y5K7#P3@>Z0xz$(yHf%pNPhZaPU ziIyqc!1^Jz?3B!BLlj^#B18a=YDTL>Y)>jVu*J4#@(g1ZIxNJFP?5FL5K3_k7q1Nno6Ks0uEbd`tT#>qN)nqe)O$wIo*RK1`Qk4dmlr8(=dg{)2;P3Gyrh5(_0 zkL|-SLVCZVXw0~*Cs2{SVGxzQC+!-^oCtxb(~428p_YTA zEICkeso^d`q17D@5V=`n>6K-?_$s7S|4HC81hUJngS<@jg5M!uMTm#}}Q(0{M+^sJBxF!otx5i~*R(D>mtO(hiOPSLPzvl}dp zA_tSIe!X$a4I2hRQ-pF9v_^ZgIA&su3l``R1r9tp^x!-l;VRFhoZXVw!iRsc4?HfwT`*bD<)-Y5G^#b?vZv8 zCIH33%J%)yPK~A|q;0}a?i24kH=OlVp}9d)jC6N~`p|@^1la?gisq&xjTbZCfhHqe zFv`l9YXJcwUIW6D)G~Cz5_2sFQ7oi5mXt?nc!GqYJZT<$HAISBhx--+qJzzeWL;3CQ1|&4%0dE{<7i{%^@t>5 zOdndz+B}ZgXOrb8oMCWgou8}`oJHcbeX){QJ(Ik1!At$b4nXIpH(Ct3K1Y?JL8~NC zafrYFmzUqauN8K8sYxUdZX8WQE6~&eUmZ-rnt7K9$Si7wYi;7Dq!!h4u}1QewM-Xg z7lNcO%FqrwER<`um^lafm=j!q7pYpBWNC|Jrh7zJMdS!miJFs&@l$Io$Wx+3>iuBV z2moBkLRo|uBrxMhfL)+lb@7ciOL8aQflvLUOx#7#Of6KW%V{Bnf;yl0k8DpC(>iCA zF3D+>xUV`jMU*Ed6`_JY`yh0*vbBYY%{Fk;$ywrK8rVCpzW0W7QQ z+3QjEN~1rEyR52)eLD}XXjIipn<3XA77-*7RChMSI;c(l#9fM3QOYnPBC-2cmS$}8S2xUV+ zN_nS|nQf6Q9WpZ0UUlcXNAI(tDjC^CZF(}iV{Gwjx4FzPRL|S&-jYiC==zXpU#ShI8#xS_pE4p96K9KtWRTXmOXI*em#(^ z%b1UU_D37VuUAaT+Zdsu%Gp%$R8q3M$c)~?#eT@1Xy+p=fa#)Bg&~SubhTLk+e=It zTeFt26)cV4iQsA|BM|8YLqnFbp*HJ)u`r69=gLKcdCaz$bZttMn@KkM6c-nStJj`5ZL@H*ilBOV_aDZ+bBc|BmBn+47xLQbCtSDE@LIq2}AdhL)IUs~W1aW0a z*?_O&ge8gz;$7omY`~Gj+?;UFKt#P(%$Q*nTV`q@EWEZ?tV)C4jb#ES(}VQ7h^|f& z%ZmcTK^*fWN+`kkr9WIVr%;zEH|_oD9chd%&Dp+9vijSR`a^jx;ci(AC4?F5aYtyf zoU}ld9%oxhhzA;#5*4DA@A|Y0baDi)8^Jp=93Y~#m>;);aBl2LaVkqCOOS1{M*`au zu1ISikOsA|>`yv1^~3E)ij<~ZrKdb;&N@{Gtkw1-+$y!nbw<@?1moYg{e;wYg@B@6 zB{IWR*}4g7>N@9zGWAfMOaXvNhUi4aC8CX!Ik{X~z^zxlM4@V$Se=a!axk@$&@>UL zoWITOr*OqqFEzm1N*l;Py|$Xm)VWLAehMWi#w?B^q_(xpAirH}Mo*2ncOi??rdGED zrsx=F*x0nJthRfka-J<}tZwVOBw5r+3P1`D-UQTd#vmfwebXdcV!usdsW@k5=aMTl z!1@9sVrrK&CoR)7ldTQB`$2sR4mX=r`2$1wn>pSlvqO&*id{vP@dut;SgVpin!RTHfT$FePEdxu{L1 z%E!83+jPUE#883;b5=aduCT6vgfv0NqA5%7opd8}^onDc=>!Po1L4+(ZUyTqtE!NV zElP56RF{SX+&5tl1u7F}UGOGBQ?1n%2OZ%>s;)?WC{eyZEfv%_Pl|Qm(HMA`O?mq% zb$PO^obhyteg35`T5=wyo2O$Qeb__WO)hhcjGl6obV8NQa}_I%=qI1;V;Dx8!mNWD z#ux$sa)ictW(jsW?Q z-8Tw;YWJZ^N$wBM_q&gpNm9BZsg^k$82Ess)1K{Hfd>Eoz1mHZ9AylI;jh z{%7HRQdORTFb)<#Pj}b%Bc(D^YOl=ub&&u`+C`a+tU6VEMq0ohNUvMi$!cv{OLk5VuoKXE_na?QB^YGSp zFeXdjkUhspgF5$H4Y>_Y9yZbuY&A24?QZpt(e|5#p%?ePS(vZk1!opA?ZEFS%3T#R z59x6*j=_`Iy3DmnUqowzAL}|CQAxq}fYP(*3TbI_@FKap^`u3c^`Ll%nB^F+A9#d0*i3i z=1754vzscgbq>F)nFOi~BxYs~v?yd{s@lC;G=?LPz{@SwRfOh^9<}3sbtzPK)!R&- zJN%U%jZ>re2C51GGHCh(RF$8 z#Q=d&*cgrOlU9X0-AItyR*L?VL){NLInpMNClssMXVfa%lN=@`Okgnuu@^E$8&^T+ zb>bbmqNwz5F>fTJ0N08fV7*T+LDraU72yo~(#1qIdY$9s>0`ki9RC>*>QD+Sk060g zr7Gc?1an6A9pqg>imSZjwVtA0N?i2GgE{JwCKjYaK$>RMP}`sR(hF~B+#OI7TJX7~ z8ZpWN%CEF=B8$H|L=QJuA;ko?9@JD7990n6+2dspxA*L<%!te>0r2&#`<(}Gwf-p;cXtzN z9f(WyZFR3VH+wy)(r9+YQ1!&EpW|@T6a>Eb4J9@s*%#}E*GUV*k?9i?J7Eh#yp6VQ z@DS{3GgU;F18TvULUAE}-%_!!{)}n2leR}xr+opH=u};LdhORmAqp#1MZY)(DW%OfKt%Fo&W&ps(Bk!lwP*6OwnU=}h4916!>XK} ztfpHo0k2?{yFbp0rIwXy1rDK`r+0DpR&ra180$~)-IR!YB^M?0WtnvOGIHT2>$}s%4LmD0Q=SWh= z71rb|uAE+a+aE#U{8#ObdKzZVoR6Q9Z+q8Zt{(5vnnk?Ftk9Z;e2KF9dd2a_U_F71 z+cFaZOmeLN27ti!!)Te}KB{l6TJM6oW9~!AL1CLq%Z;{7JTL(m{7t+IObq)Qbt2FZw4%$|mbsbzZu6!faK!;$pr zNf%1?Ds|be+#-(cN+8_32i9h7-xNc@CX6H&Hoht_C|V)*3Jd_v?Z>I=w70{fmjke@=~9WtX2~7N992r%CJcRrD38*)8_Yus^(yr^ z@x+zlTOXzftd6{Grez~X=pyE5rK-L*#bKy(bWs9(DF?M;t1{-CxZ933n{gnDW~Dn$ zU^TC=_w7T8%5{njg%TsD`qs~|bTu2asRUsptW4-Uodv2Rz>v*J7m?Gut5aw?S2CEp zEB4dr+V~Oh#0oG>bF@e6-dI%__6Sw@{okfIu46S3>-J$F?7Pp|R7|dR+;HVd;`pceqK#nE(dunmPjIB^vG(jMCK(fEYUv@e$gI$>uH|;h0sHFt1YOW< z^{ve8RvU-5n$DGy*|UDnF$H`;7Ad=&W4$Wp_SlSatk%DCoWUh6XWS}0oXU8iOsIA@ zaXqHpgf)Lo-?x~$G&xl()$Dm72PFR67gWB#PPz2+PrrjJx2pajg3F(M~WM=%b|bL6PFkyNWY4k~|Y;*Ziw z^&GEp87IoZzxqlDFWs-$W+j-=nJ;7lp5(I1EgEUxJ&D;1hycP}_O^}IY*c6I&Regj zs2ikTzJM~G4^=U}c`hEPbi39Wpf+qF#H=#O{0BNxpNr`F{ z#4fpQbe8P&JowA71T{51rNZO!S`ldWa2)0} zVH3cuAJ0vopMlZJs`E9rYjN4;^LX++br+3D)2ai$b* zMH$8{@DO@7o3Oh4K1;Wz?-Mz8#TUsl) zRkBjA&pb3aO@MJF$$VESRH5rf*#OMoVNmyZgo0dQa4o`zpg%eoOzB8_2gJB-dQOzk zA|9>Kr*oG%GF}8OaU(P(ttyOi+;$S-k-p~zDc}458=)9y+aqoLpBuU)&JWZ#2z6$AIabQr)0FqW@FDn^J!f>U%EalTgQ{j?KAhq{I0i2|vj$!+a%XG^rfkB&R z&;8?!)%QmQQsCK}4Pn#WHc?A;s!N0E2<64h1P4U#B6oXAW|(fQL2|T|FoLE(o(dMU zd}Ch58rjnMoDf!8{9Jb|sKq%&6wGsv>f5Zl&#;sPZUhLaM&O-ot-am7XC;Cp#PX^a zpuu=LNe#=`qcZhf9iykoGmQ3)iP~yvA&MaZED7?=m9`J*O5o(F+{G%1k^MLnz{*7!L@Vl*iBERKm;aBFmNg(;n2O&NP| z>4ZBk$}SfCnUc5b2r3#MY%9J{UaoST+u^hu9hyhFaK)-xexduf=RkNGe!+qL0mX{> AsQ>@~ diff --git a/src/pipecraft-core/service_scripts/NextITS/test_data/Test_S2/Test/07_SeqTable/Seqs.parquet b/src/pipecraft-core/service_scripts/NextITS/test_data/Test_S2/Test/07_SeqTable/Seqs.parquet deleted file mode 100644 index caad092596b4c14dbadc6c80019b6589958b0ce4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21217 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 diff --git a/src/pipecraft-core/service_scripts/NextITS/test_data/Test_Step1.fastq.gz b/src/pipecraft-core/service_scripts/NextITS/test_data/Test_Step1.fastq.gz deleted file mode 100644 index 5238b0eeeb506954ff156452f07468bdfe2dde2a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53323 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 diff --git a/src/pipecraft-core/service_scripts/NextITS/test_data/Test_Step1_barcodes.fasta b/src/pipecraft-core/service_scripts/NextITS/test_data/Test_Step1_barcodes.fasta deleted file mode 100644 index dcc3270d..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/test_data/Test_Step1_barcodes.fasta +++ /dev/null @@ -1,6 +0,0 @@ ->Test__A1 -ACAACACTCCGA ->Test__B1 -ACACCGCACAAT ->Test__B3 -ACAGTGCGTCCT diff --git a/src/pipecraft-core/service_scripts/NextITS/tests/test_step2_vsearch.yml b/src/pipecraft-core/service_scripts/NextITS/tests/test_step2_vsearch.yml deleted file mode 100644 index 4420b5df..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/tests/test_step2_vsearch.yml +++ /dev/null @@ -1,18 +0,0 @@ ---- -- name: step2 vsearch - command: > - nextflow run . -profile test2,docker - --outdir out/step2_vsearch - -work-dir out/step2_vsearch_wd - --preclustering none - --clustering vsearch - --otu_id 0.98 - files: - - path: "out/step2_vsearch/01.Dereplicated/Dereplicated.fa.gz" - - path: "out/step2_vsearch/01.Dereplicated/Dereplicated.uc.gz" - - path: "out/step2_vsearch/03.Clustered_VSEARCH/Clustered.fa.gz" - - path: "out/step2_vsearch/03.Clustered_VSEARCH/Clustered.uc.gz" - - path: "out/step2_vsearch/04.PooledResults/UC_Pooled.parquet" - - path: "out/step2_vsearch/04.PooledResults/OTU_table_wide.txt.gz" - - path: "out/step2_vsearch/04.PooledResults/OTU_table_long.txt.gz" - - path: "out/step2_vsearch/04.PooledResults/OTUs.fa.gz" diff --git a/src/pipecraft-core/service_scripts/NextITS/workflows/STEP1.nf b/src/pipecraft-core/service_scripts/NextITS/workflows/STEP1.nf deleted file mode 100644 index f17e2fa8..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/workflows/STEP1.nf +++ /dev/null @@ -1,2646 +0,0 @@ -/* -============================================================================ - NextITS: Pipeline to process eukaryotic ITS amplicons -============================================================================ - License: Apache-2.0 - Github : https://github.com/vmikk/NextITS - Website: https://Next-ITS.github.io/ ----------------------------------------------------------------------------- -*/ - -// ---- Step-1 workflow ---- - - -// Include functions -include { software_versions_to_yaml } from '../modules/version_parser.nf' -include { dumpParamsTsv } from '../modules/dump_parameters.nf' -include { CHIMERA_REMOVAL } from '../subworkflows/chimera_removal_subworkflow.nf' - -if ( params.seqplatform == "Illumina" ){ - include { qc_pe; demux_illumina_notmerged; trim_primers_pe; join_pe } from '../modules/Illumina_pe.nf' -} - - -// Define output paths for different steps -out_0_bam = params.outdir + "/00_BAM2FASTQ" -out_1_demux = params.outdir + "/01_Demux" -out_1_joinPE = params.outdir + "/01_JoinedPE" -out_2_primer = params.outdir + "/02_PrimerCheck" -out_3_itsx = params.outdir + "/03_ITSx" -out_3_itsxp = params.outdir + "/03_ITSx_PooledParts" -out_3_trim = params.outdir + "/03_PrimerTrim" -out_3_trimPE = params.outdir + "/03_PrimerTrim_NotMerged" -out_4_homop = params.outdir + "/04_Homopolymer" -// out_5_chim = params.outdir + "/05_Chimera" -out_6_tj = params.outdir + "/06_TagJumpFiltration" -out_7_seq = params.outdir + "/07_SeqTable" -out_8_smr = params.outdir + "/08_RunSummary" -out_9_db = params.outdir + "/09_DB" -out_tracedir = params.tracedir - -// Sub-workflow-specific outputs -out_3_quickstats = params.outdir + "/03_Stats" - - -// Convert BAM to FASTQ -process bam2fastq { - - label "main_container" - publishDir "${out_0_bam}", mode: "${params.storagemode}" - - // cpus 2 - - input: - path input - path bam_index - - output: - path "*.fastq.gz", emit: fastq, optional: false - tuple val("${task.process}"), val('bam2fastq'), eval('bam2fastq --version | head -n 1 | sed "s/bam2fastq //"'), topic: versions - - script: - """ - echo -e "Converting BAM to FASTQ\\n" - echo -e "Input file: " ${input} - echo -e "BAM index: " ${bam_index} - - bam2fastq \ - -c ${params.gzip_compression} \ - --num-threads ${task.cpus} \ - ${input} - - echo -e "\\nConvertion finished" - """ -} - - -// Quality filtering for single-end reads -process qc_se { - - label "main_container" - - // cpus 10 - - // Add file ID to the log file - tag "${input.getSimpleName()}" - - input: - path input - - output: - path "${input.getSimpleName()}.fq.gz", emit: filtered, optional: true - tuple val("${task.process}"), val('vsearch'), eval('vsearch --version 2>&1 | head -n 1 | sed "s/vsearch //g" | sed "s/,.*//g" | sed "s/^v//" | sed "s/_.*//"'), topic: versions - tuple val("${task.process}"), val('seqkit'), eval('seqkit version | sed "s/seqkit v//"'), topic: versions - - script: - filter_maxee = params.qc_maxee ? "--fastq_maxee ${params.qc_maxee}" : "" - filter_maxeerate = params.qc_maxeerate ? "--fastq_maxee_rate ${params.qc_maxeerate}" : "" - """ - echo -e "QC\\n" - echo -e "Input file: " ${input} - - ## We do not need to change the file name (output name should be the same as input) - ## Therefore, temporary rename input - mv ${input} inp.fq.gz - - vsearch \ - --fastq_filter inp.fq.gz \ - --fastq_qmax 93 \ - ${filter_maxee} \ - ${filter_maxeerate} \ - --fastq_maxns ${params.qc_maxn} \ - --threads ${task.cpus} \ - --fastqout - \ - | seqkit grep \ - --by-seq --ignore-case --invert-match --only-positive-strand --use-regexp -w 0 \ - --pattern '"(A{${params.qc_maxhomopolymerlen},}|C{${params.qc_maxhomopolymerlen},}|T{${params.qc_maxhomopolymerlen},}|G{${params.qc_maxhomopolymerlen},})"' \ - | gzip -${params.gzip_compression} \ - > "${input.getSimpleName()}.fq.gz" - - ## qc_maxhomopolymerlen - # e.g. "(A{26,}|C{26,}|T{26,}|G{26,})" - - echo -e "\\nQC finished" - """ -} - - -// Validate tags for demultiplexing -process tag_validation { - - label "main_container" - // cpus 1 - - publishDir "${out_1_demux}", pattern: "tag_names_renamed.tsv", mode: "${params.storagemode}" - - input: - path barcodes - - output: - path "barcodes_validated.fasta", emit: fasta - path "biosamples_asym.csv", emit: biosamples_asym, optional: true - path "biosamples_sym.csv", emit: biosamples_sym, optional: true - path "file_renaming.tsv", emit: file_renaming, optional: true - path "unknown_combinations.tsv", emit: unknown_combinations, optional: true - path "tag_names_renamed.tsv", emit: tag_names_renamed, optional: true - - script: - """ - echo -e "Valdidating demultiplexing tags\\n" - echo -e "Input file: " ${barcodes} - - ## Convert Windows-style line endings (CRLF) to Unix-style (LF) - LC_ALL=C sed -i 's/\r\$//g' ${barcodes} - - ## Perform tag validation - validate_tags.R \ - --tags ${barcodes} \ - --output barcodes_validated.fasta - - echo -e "\\nTag validation finished" - """ -} - - - -// Demultiplexing with LIMA - for PacBio reads -process demux { - - label "main_container" - - publishDir "${out_1_demux}", mode: "${params.storagemode}" // , saveAs: { filename -> "foo_$filename" } - // cpus 10 - - input: - path input_fastq - path barcodes - path biosamples_sym // for dual or asymmetric barcodes - path biosamples_asym // for dual or asymmetric barcodes - path file_renaming // for dual or asymmetric barcodes - path unknown_combinations // for dual or asymmetric barcodes - - output: - path "LIMA/*.fq.gz", emit: samples_demux - path "LIMA/lima.lima.report.gz", emit: lima_report - path "LIMA/lima.lima.counts", emit: lima_counts - path "LIMA/lima.lima.summary", emit: lima_summary - tuple val("${task.process}"), val('lima'), eval('lima --version | head -n 1 | sed "s/lima //"'), topic: versions - tuple val("${task.process}"), val('brename'), eval('brename --help | head -n 4 | tail -1 | sed "s/Version: //"'), topic: versions - - script: - """ - echo -e "Input file: " ${input_fastq} - echo -e "Barcodes: " ${barcodes} - - ## Directory for the results - mkdir -p LIMA - - echo -e "Validating data\n" - - ## Check if symmetric barcodes were provided in the `...` format - ## (if `biosamples_sym` does not exists, it means that it is a dummy file) - ## (if exists, it means that tags were split into sym and asym at the tag validation step) - if [[ ${params.lima_barcodetype} = "dual_symmetric" ]] && [ -e ${biosamples_sym} ] ; then - echo -e "\\nERROR: Symmetric tags are provided in '...' format.\\n" - echo -e "In the FASTA file, please include only one tag per sample, since these tags are identical.\\n" - exit 1 - fi - - ## Count the number of samples in Biosample files - only for `dual` and `dual_asymmetric` barcodes - if [[ ${params.lima_barcodetype} == "dual_asymmetric" ]] || [[ ${params.lima_barcodetype} == "dual" ]]; then - - if [ ! -e ${biosamples_asym} ]; then - - echo -e "\\nERROR: Tags are specified in wrong format" - echo -e "Use the '...' format in FASTA file.\\n" - exit 1 - - else - line_count_sym=\$(wc -l < ${biosamples_sym}) - line_count_asym=\$(wc -l < ${biosamples_asym}) - - echo -e "..Number of lines in symmetric file: " \$line_count_sym - echo -e "..Number of lines in asymmetric file: " \$line_count_asym - - ## Check the presence of dual barcode combinations - ## If line count is less than 2, it means there are no samples specified - if [[ ${params.lima_barcodetype} == "dual_asymmetric" ]] && [[ \$line_count_asym -lt 2 ]]; then - echo -e "\\nERROR: No asymmetric barcodes detected for demultiplexing.\\n" - return 1 - fi - - if [[ ${params.lima_barcodetype} == "dual" ]] && [[ \$line_count_asym -lt 2 ]]; then - echo -e "\\nWARNING: No asymmetric barcodes detected, consider using '--lima_barcodetype dual_symmetric'.\\n" - fi - - fi # end of missing asym biosamples - - fi # end of dual/asym validation - - - - ## Combine shared arguments into a single variable - ## Note the array syntax - that's because of LIMA parser error messages - ## (note also that it works in bash, but not in zsh) - common_args=("--ccs \ - --window-size ${params.lima_windowsize} \ - --min-length ${params.lima_minlen} \ - --min-score ${params.lima_minscore} \ - --min-ref-span ${params.lima_minrefspan} \ - --split-named \ - --num-threads ${task.cpus} \ - --log-level INFO \ - ${input_fastq} \ - ${barcodes}") - - - ## Demultiplex, depending on the barcode type selected - case ${params.lima_barcodetype} in - - "single") - echo -e "\\nDemultiplexing with LIMA (single barcode)" - lima --same --single-side \ - --log-file LIMA/_log.txt \ - \$common_args \ - "LIMA/lima.fq.gz" - ;; - - "dual_symmetric") - echo -e "\\nDemultiplexing with LIMA (dual symmetric barcodes)" - lima --same \ - --min-end-score ${params.lima_minendscore} \ - --min-scoring-regions ${params.lima_minscoringregions} \ - --log-file LIMA/_log.txt \ - \$common_args \ - "LIMA/lima.fq.gz" - ;; - - "dual_asymmetric") - echo -e "\\nDemultiplexing with LIMA (dual asymmetric barcodes)" - lima --different \ - --min-end-score ${params.lima_minendscore} \ - --min-scoring-regions ${params.lima_minscoringregions} \ - --biosample-csv ${biosamples_asym} \ - --log-file LIMA/_log.txt \ - \$common_args \ - "LIMA/lima.fq.gz" - ;; - - "dual") - mkdir -p LIMAs LIMAd - - if [[ \$line_count_sym -ge 2 ]]; then - echo -e "\\nDemultiplexing with LIMA (dual symmetric barcodes)" - lima --same \ - --min-end-score ${params.lima_minendscore} \ - --min-scoring-regions ${params.lima_minscoringregions} \ - --biosample-csv ${biosamples_sym} \ - --log-file LIMAs/_log.txt \ - \$common_args \ - "LIMAs/lima.fq.gz" - fi - - if [[ \$line_count_asym -ge 2 ]]; then - echo -e "\\nDemultiplexing with LIMA (dual asymmetric barcodes)" - lima --different \ - --min-end-score ${params.lima_minendscore} \ - --min-scoring-regions ${params.lima_minscoringregions} \ - --biosample-csv ${biosamples_asym} \ - --log-file LIMAd/_log.txt \ - \$common_args \ - "LIMAd/lima.fq.gz" - fi - ;; - esac - - - ## Combining symmetric and asymmetric files - if [ ${params.lima_barcodetype} = "dual" ]; then - - echo -e "\\nPooling of symmetric and asymmetric barcodes" - cd LIMA - find ../LIMAd -name "*.fq.gz" | parallel -j1 "ln -s {} ." - find ../LIMAs -name "*.fq.gz" | parallel -j1 "ln -s {} ." - cd .. - - fi - - - ## Rename barcode combinations into sample names - ## Only user-provided combinations whould be kept (based on `lima --biosample-csv`) - if [[ ${params.lima_barcodetype} == "dual_asymmetric" ]] || [[ ${params.lima_barcodetype} == "dual" ]]; then - - echo -e "\\n..Renaming files from tag IDs to sample names" - brename -p "(.+)" -r "{kv}" -k ${file_renaming} LIMA/ - - echo -e "\\n..Checking for unknown tag combinations" - echo -e "\\n...Number of unknowns detected:" - find LIMA -name "lima.*.fq.gz" | wc -l - - if [[ ${params.lima_remove_unknown} == "false" ]]; then - - if [ -s ${unknown_combinations} ]; then - echo -e "\\n...Renaming unknown combinations" - brename -p "(.+)" -r "{kv}" -k ${unknown_combinations} LIMA/ - else - echo -e "\\n...No unknown combinations require renaming" - fi - - echo -e "\\n...Number of unknowns remained:" - find LIMA -name "lima.*.fq.gz" | wc -l - - fi - - echo -e "\\n...Removing unknowns:" - find LIMA -name "lima.*.fq.gz" | parallel -j1 "echo {} && rm {}" - - fi # end of dual/asym renaming - - if [[ ${params.lima_barcodetype} == "dual_symmetric" ]] || [[ ${params.lima_barcodetype} == "single" ]]; then - - echo -e "\\n..Renaming demultiplexed files" - rename --filename \ - 's/^lima.//g; s/--.*\$/.fq.gz/' \ - \$(find LIMA -name "*.fq.gz") - - fi - - - ## Combine summary stats for dual barcodes (two LIMA runs) - if [[ ${params.lima_barcodetype} == "dual" ]]; then - - echo -e "\\n..Combining dual-barcode log files" - - if [ -f "LIMAd/lima.lima.summary" ]; then - echo -e "Asymmetric barcodes summary\\n\\n" >> LIMA/lima.lima.summary - cat LIMAd/lima.lima.summary >> LIMA/lima.lima.summary - - echo -e "Asymmetric barcodes counts\\n\\n" >> LIMA/lima.lima.counts - cat LIMAd/lima.lima.counts >> LIMA/lima.lima.counts - - echo -e "Asymmetric barcodes report\\n\\n" >> LIMA/lima.lima.report - cat LIMAd/lima.lima.report >> LIMA/lima.lima.report - fi - - if [ -f "LIMAs/lima.lima.summary" ]; then - echo -e "\\n\\nSymmetric barcodes summary\\n\\n" >> LIMA/lima.lima.summary - cat LIMAs/lima.lima.summary >> LIMA/lima.lima.summary - - echo -e "\\n\\nSymmetric barcodes counts\\n\\n" >> LIMA/lima.lima.counts - cat LIMAs/lima.lima.counts >> LIMA/lima.lima.counts - - ## Reports should be identical for symmetric and asymmetric barcodes, so no need to combine them - # echo -e "\\n\\nSymmetric barcodes report\\n\\n" >> LIMA/lima.lima.report - # cat LIMAs/lima.lima.report >> LIMA/lima.lima.report - fi - - fi # end of dual logs pooling - - - ## Compress logs - echo -e "..Compressing log file" - gzip -${params.gzip_compression} LIMA/lima.lima.report - - - ## LIMA defaults: - # SYMMETRIC : --ccs --min-score 0 --min-end-score 80 --min-ref-span 0.75 --same --single-end - # ASYMMETRIC : --ccs --min-score 80 --min-end-score 50 --min-ref-span 0.75 --different --min-scoring-regions 2 - - echo -e "\\nDemultiplexing finished" - """ -} - - -// Merge Illumina PE reads -process merge_pe { - - label "main_container" - - // publishDir "${out_1_demux}", mode: "${params.storagemode}" - // cpus 10 - - input: - path input_R1 - path input_R2 - - output: - path "Merged.fq.gz", emit: r12 - tuple path("NotMerged_R1.fq.gz"), path("NotMerged_R2.fq.gz"), emit: nm, optional: true - - script: - """ - echo -e "Merging Illumina pair-end reads\\n" - - ## By default, fastp modifies sequences header - ## e.g., `merged_150_15` means that 150bp are from read1, and 15bp are from read2 - ## But we'll preserve only sequence ID - - fastp \ - --in1 ${input_R1} \ - --in2 ${input_R2} \ - --merge --correction \ - --overlap_len_require ${params.pe_minoverlap} \ - --overlap_diff_limit ${params.pe_difflimit} \ - --overlap_diff_percent_limit ${params.pe_diffperclimit} \ - --length_required ${params.pe_minlen} \ - --disable_quality_filtering \ - --disable_adapter_trimming \ - --dont_eval_duplication \ - --compression 6 \ - --thread ${task.cpus} \ - --out1 NotMerged_R1.fq.gz \ - --out2 NotMerged_R2.fq.gz \ - --json log.json \ - --html log.html \ - --stdout \ - | seqkit seq --only-id \ - | gzip -${params.gzip_compression} \ - > Merged.fq.gz - - ## --merged_out Merged.fq.gz \ - ## --n_base_limit ${params.pe_nlimit} \ - - # --overlap_len_require the minimum length to detect overlapped region of PE reads - # --overlap_diff_limit the maximum number of mismatched bases to detect overlapped region of PE reads - # --overlap_diff_percent_limit the maximum percentage of mismatched bases to detect overlapped region of PE reads - ## NB: reads should meet these three conditions simultaneously! - - echo -e "..done" - """ -} - - -// Modify barcodes for cutadapt (restrict the search window) -process prep_barcodes { - - label "main_container" - - // publishDir "${out_1_demux}", mode: "${params.storagemode}" - // cpus 1 - - input: - path barcodes - - output: - path "barcodes_modified.fa", emit: barcodesm - - script: - """ - echo -e "Restricting the search window for barcode lookup" - echo -e "Provided barcodes: " ${barcodes} - - ## Add `XN{30}` to the barcodes - - sed -e '/^>/! s/^/XN{${params.barcode_window}}/' \ - ${barcodes} \ - > barcodes_modified.fa - - echo -e "..Done" - """ -} - - -// Demultiplexing with cutadapt - for Illumina SE reads -process demux_illumina { - - label "main_container" - - publishDir "${out_1_demux}", mode: "${params.storagemode}" - // cpus 10 - - input: - path input_fastq - path barcodes - - output: - path "*.fq.gz", emit: samples_demux - - script: - """ - echo -e "Input file: " ${input_fastq} - echo -e "Barcodes: " ${barcodes} - - echo -e "\nDemultiplexing with cutadapt:" - - ## Demultiplex with cutadapt - cutadapt \ - -g file:${barcodes} \ - --revcomp --rename "{header}" \ - --errors ${params.barcode_errors} \ - --overlap ${params.barcode_overlap} \ - --no-indels \ - --cores ${task.cpus} \ - --discard-untrimmed \ - --action none \ - -o "{name}.fq.gz" \ - ${input_fastq} \ - > cutadapt.log - - echo -e "\\n..done" - - ## Remove empty files (no sequences) - echo -e "\\nRemoving empty files" - find . -type f -name "*.fq.gz" -size -29c -print -delete - echo -e "..Done" - - echo -e "\\nDemultiplexing finished" - """ -} - - - -// Primer disambiguation -process disambiguate { - - label "main_container" - - // publishDir "${out_2_primer}", mode: "${params.storagemode}" - // cpus 1 - - output: - path "primer_F.fasta", emit: F - path "primer_R.fasta", emit: R - path "primer_Fr.fasta", emit: Fr - path "primer_Rr.fasta", emit: Rr - - script: - - """ - - ## Disambiguate forward primer - echo -e "Disambiguating forward primer" - disambiguate_primers.R \ - ${params.primer_forward} \ - primer_F.fasta - - ## Disambiguate reverse primer - echo -e "\\nDisambiguating reverse primer" - disambiguate_primers.R \ - ${params.primer_reverse} \ - primer_R.fasta - - ## Reverse-complement primers - echo -e "\\nReverse-complementing primers" - seqkit seq -r -p --seq-type dna primer_F.fasta > primer_Fr.fasta - seqkit seq -r -p --seq-type dna primer_R.fasta > primer_Rr.fasta - - """ -} - - -// Check primers + QC + Reorient sequences -// Count number of primer occurrences withnin a read, -// discard reads with > 1 primer occurrence -// NB. read names should not contain spaces! (because of bedtools) -process primer_check { - - label "main_container" - - publishDir "${out_2_primer}", mode: "${params.storagemode}" - - // cpus 1 - - // Add sample ID to the log file - tag "${input.getSimpleName()}" - - input: - path input - path primer_F - path primer_R - path primer_Fr - path primer_Rr - - output: - path "${input.getSimpleName()}_PrimerChecked.fq.gz", emit: fq_primer_checked, optional: true - path "${input.getSimpleName()}_PrimerArtefacts.fq.gz", emit: primerartefacts, optional: true - tuple val("${task.process}"), val('seqkit'), eval('seqkit version | sed "s/seqkit v//"'), topic: versions - tuple val("${task.process}"), val('runiq'), eval('runiq --version | sed "s/runiq //"'), topic: versions - tuple val("${task.process}"), val('mlr'), eval('mlr --version | sed "s/mlr //"'), topic: versions - tuple val("${task.process}"), val('bedtools'), eval('bedtools --version | sed "s/bedtools v//"'), topic: versions - tuple val("${task.process}"), val('csvtk'), eval('csvtk version | sed "s/csvtk v//"'), topic: versions - tuple val("${task.process}"), val('cutadapt'), eval('cutadapt --version'), topic: versions - - script: - """ - echo -e "Input file: " ${input} - echo -e "Forward primer: " ${params.primer_forward} - echo -e "Reverse primer: " ${params.primer_reverse} - - ### Count number of pattern occurrences for each sequence - count_primers (){ - # \$1 = file with primers - - seqkit replace -p "\\s.+" ${input} \ - | seqkit locate \ - --max-mismatch ${params.primer_mismatches} \ - --only-positive-strand \ - --pattern-file "\$1" \ - --threads ${task.cpus} \ - | awk -vOFS='\\t' 'NR > 1 { print \$1 , \$5 , \$6 }' \ - | runiq - \ - | mlr --tsv \ - --implicit-tsv-header \ - --headerless-tsv-output \ - sort -f 1 -n 2 \ - | bedtools merge -i stdin - } - - echo -e "\\nCounting primers" - echo -e "..forward primer" - count_primers ${primer_F} > PF.txt - - echo -e "..rc-forward primer" - count_primers ${primer_Fr} >> PF.txt - - echo -e "..reverse primer" - count_primers ${primer_R} > PR.txt - - echo -e "..rc-reverse primer" - count_primers ${primer_Rr} >> PR.txt - - ## Sort by seqID and start position, remove overlapping regions, - ## Find duplicated records - echo -e "\\nLooking for multiple primer occurrences" - - echo -e "..Processing forward primers" - if [ -s PF.txt ]; then - - csvtk sort \ - -t -T -H -k 1:N -k 2:n \ - --num-cpus ${task.cpus} \ - PF.txt \ - | bedtools merge -i stdin \ - | awk '{ print \$1 }' \ - | runiq -i - \ - > multiprimer.txt - - else - echo -e "...No forward primer matches found (in both orientations)" - fi - - echo -e "..Processing reverse primers" - if [ -s PR.txt ]; then - - csvtk sort \ - -t -T -H -k 1:N -k 2:n \ - --num-cpus ${task.cpus} \ - PR.txt \ - | bedtools merge -i stdin \ - | awk '{ print \$1 }' \ - | runiq -i - \ - >> multiprimer.txt - - else - echo -e "...No reverse primer matches found (in both orientations)" - fi - - - ## If some artefacts are found - if [ -s multiprimer.txt ]; then - - ## Keep only uinque seqIDs - runiq multiprimer.txt > multiprimers.txt - rm multiprimer.txt - - echo -e "\\nNumber of artefacts found: " \$(wc -l < multiprimers.txt) - - echo -e "..Removing artefacts" - ## Remove primer artefacts - seqkit grep --invert-match \ - --threads ${task.cpus} \ - --pattern-file multiprimers.txt \ - --out-file no_multiprimers.fq.gz \ - ${input} - - ## Extract primer artefacts - echo -e "..Extracting artefacts" - seqkit grep \ - --threads ${task.cpus} \ - --pattern-file multiprimers.txt \ - --out-file "${input.getSimpleName()}_PrimerArtefacts.fq.gz" \ - ${input} - - echo -e "..done" - - else - - echo -e "\\nNo primer artefacts found" - ln -s ${input} no_multiprimers.fq.gz - - fi - echo -e "..Done" - - echo -e "\\nReorienting sequences" - - ## Reverse-complement rev primer - RR=\$(rc.sh ${params.primer_reverse}) - - ## Reorient sequences, discard sequences without both primers - cutadapt \ - -a ${params.primer_forward}";required;min_overlap=${params.primer_foverlap}"..."\$RR"";required;min_overlap=${params.primer_roverlap}" \ - --errors ${params.primer_mismatches} \ - --revcomp --rename "{header}" \ - --discard-untrimmed \ - --cores ${task.cpus} \ - --action none \ - --output ${input.getSimpleName()}_PrimerChecked.fq.gz \ - no_multiprimers.fq.gz - - echo -e "\\nAll done" - - ## Clean up - if [ -f no_multiprimers.fq.gz ]; then rm no_multiprimers.fq.gz; fi - - ## Remove empty file (no valid sequences) - echo -e "\\nRemoving empty files" - find . -type f -name ${input.getSimpleName()}_PrimerChecked.fq.gz -size -29c -print -delete - echo -e "..Done" - - """ -} - - -// Extract ITS region with ITSx -// NB. sequence header should not contain spaces! -process itsx { - - label "main_container" - - publishDir "${out_3_itsx}", mode: "${params.storagemode}" - // cpus 2 - - // Add sample ID to the log file - tag "${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}" - - input: - path input - - output: - path "${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}_hash_table.txt.gz", emit: hashes, optional: true - path "${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}_uc.uc.gz", emit: uc, optional: true - path "${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}.full.fasta.gz", emit: itsx_full, optional: true - path "${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}.SSU.fasta.gz", emit: itsx_ssu, optional: true - path "${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}.ITS1.fasta.gz", emit: itsx_its1, optional: true - path "${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}.5_8S.fasta.gz", emit: itsx_58s, optional: true - path "${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}.ITS2.fasta.gz", emit: itsx_its2, optional: true - path "${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}.LSU.fasta.gz", emit: itsx_lsu, optional: true - path "${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}.positions.txt", optional: true - path "${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}.problematic.txt", optional: true - path "${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}_no_detections.fasta.gz", emit: itsx_nondetects, optional: true - path "${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}.summary.txt", emit: itsx_summary, optional: true - path "${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}.extraction.results.gz", emit: itsx_details, optional: true - path "${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}.SSU.full_and_partial.fasta.gz", emit: itsx_ssu_part, optional: true - path "${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}.ITS1.full_and_partial.fasta.gz", emit: itsx_its1_part, optional: true - path "${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}.5_8S.full_and_partial.fasta.gz", emit: itsx_58s_part, optional: true - path "${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}.ITS2.full_and_partial.fasta.gz", emit: itsx_its2_part, optional: true - path "${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}.LSU.full_and_partial.fasta.gz", emit: itsx_lsu_part, optional: true - path "${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}_primertrimmed_sorted.fq.gz", emit: trimmed_seqs, optional: true - path "parquet/*.parquet", emit: parquet, optional: true - tuple val("${task.process}"), val('ITSx'), eval('ITSx --help 2>&1 | head -n 3 | tail -n 1 | sed "s/Version: //"'), topic: versions - tuple val("${task.process}"), val('cutadapt'), eval('cutadapt --version'), topic: versions - tuple val("${task.process}"), val('vsearch'), eval('vsearch --version 2>&1 | head -n 1 | sed "s/vsearch //g" | sed "s/,.*//g" | sed "s/^v//" | sed "s/_.*//"'), topic: versions - tuple val("${task.process}"), val('seqkit'), eval('seqkit version | sed "s/seqkit v//"'), topic: versions - tuple val("${task.process}"), val('phredsort'), eval('phredsort -v | sed "s/phredsort //"'), topic: versions - tuple val("${task.process}"), val('seqhasher'), eval('seqhasher -v | sed "s/SeqHasher //"'), topic: versions - tuple val("${task.process}"), val('parallel'), eval('parallel --version | head -n 1 | sed "s/GNU parallel //"'), topic: versions - tuple val("${task.process}"), val('brename'), eval('brename --help | head -n 4 | tail -1 | sed "s/Version: //"'), topic: versions - tuple val("${task.process}"), val('duckdb'), eval('duckdb --version | cut -d" " -f1 | sed "s/^v//"'), topic: versions - - script: - - sampID="${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}" - - // Allow inclusion of sequences that only find a single domain, given that they meet the given E-value and score thresholds, on with parameters 1e-9,0 by default - // singledomain = params.ITSx_singledomain ? "--allow_single_domain 1e-9,0" : "" - - """ - echo -e "Extraction of rRNA regions using ITSx\\n" - - ## Trim primers - echo -e "Trimming primers\\n" - - ## Reverse-complement rev primer - RR=\$(rc.sh ${params.primer_reverse}) - - cutadapt \ - -a ${params.primer_forward}";required;min_overlap=${params.primer_foverlap}"..."\$RR"";required;min_overlap=${params.primer_roverlap}" \ - --errors ${params.primer_mismatches} \ - --revcomp --rename "{id}" \ - --discard-untrimmed \ - --minimum-length ${params.trim_minlen} \ - --cores ${task.cpus} \ - --action trim \ - --output ${sampID}_primertrimmed.fq.gz \ - ${input} - - echo -e "..Done\\n" - - ## Check if there are sequences in the output - NUMSEQS=\$( seqkit stat --tabular --quiet ${sampID}_primertrimmed.fq.gz | awk -F'\t' 'NR==2 {print \$4}' ) - echo -e "Number of sequences after primer trimming: " \$NUMSEQS - if [ \$NUMSEQS -lt 1 ]; then - echo -e "\\nIt looks like no reads remained after trimming the primers\\n" - exit 0 - fi - - ## Estimate sequence quality and sort sequences by quality - echo -e "\\nSorting by sequence quality" - seqkit replace -p "\\s.+" ${sampID}_primertrimmed.fq.gz \ - | phredsort -i - -o - --metric meep --header avgphred,maxee,meep \ - | gzip -1 > ${sampID}_primertrimmed_sorted.fq.gz - echo -e "..Done" - - ## Hash sequences, add sample ID to the header - ## columns: Sample ID - Hash - PacBioID - AvgPhredScore - MaxEE - MEEP - Sequence - Quality - Length - ## Convert to Parquet format - echo -e "\\nCreating hash table" - seqhasher --hash sha1 --name ${sampID} ${sampID}_primertrimmed_sorted.fq.gz - \ - | seqkit fx2tab --length \ - | sed 's/;/\t/ ; s/;/\t/ ; s/ avgphred=/\t/ ; s/ maxee=/\t/ ; s/ meep=/\t/' \ - > ${sampID}_hash_table.txt - echo -e "..Done" - - ## Check the number of fields per record (should be 9!) - # awk '{print NF}' ${sampID}_hash_table.txt | sort | uniq -c - # awk 'NF > 9 {print \$0 }' ${sampID}_hash_table.txt - - ## Dereplicate at sample level (use quality-sorted sequences to make sure that the representative sequence is with the highest quality) - echo -e "\\nDereplicating at sample level" - seqkit fq2fa -w 0 ${sampID}_primertrimmed_sorted.fq.gz \ - | vsearch \ - --derep_fulllength - \ - --output - \ - --strand both \ - --fasta_width 0 \ - --threads 1 \ - --relabel_sha1 \ - --sizein --sizeout \ - --minseqlength ${params.trim_minlen} \ - --uc ${sampID}_uc.uc \ - --quiet \ - > derep.fasta - echo -e "..Done" - - ## ITSx extraction - echo -e "\\nITSx extraction" - ITSx \ - -i derep.fasta \ - --complement ${params.ITSx_complement} \ - --save_regions all \ - --graphical F \ - --detailed_results T \ - --positions T \ - --not_found T \ - -E ${params.ITSx_evalue} \ - -t ${params.ITSx_tax} \ - --partial ${params.ITSx_partial} \ - --cpu ${task.cpus} \ - --preserve T \ - -o ${sampID} - - echo -e "..Done" - - # ITSx.full.fasta - # ITSx.SSU.fasta - # ITSx.ITS1.fasta - # ITSx.5_8S.fasta - # ITSx.ITS2.fasta - # ITSx.LSU.fasta - # ITSx.positions.txt - # ITSx.problematic.txt - # ITSx_no_detections.fasta - # ITSx_no_detections.txt - # ITSx.summary.txt - # ITSx.extraction.results - # ITSx.SSU.full_and_partial.fasta - # ITSx.ITS1.full_and_partial.fasta - # ITSx.5_8S.full_and_partial.fasta - # ITSx.ITS2.full_and_partial.fasta - # ITSx.LSU.full_and_partial.fasta - - - ## If partial sequences were required, remove empty sequences - if [ \$(find . -type f -name "*.full_and_partial.fasta" | wc -l) -gt 0 ]; then - echo -e "Partial files found, removing empty sequences\\n." - - find . -name "*.full_and_partial.fasta" \ - | parallel -j${task.cpus} "seqkit seq -m 1 -w 0 {} > {.}_tmp.fasta" - - rm *.full_and_partial.fasta - brename -p "_tmp" -r "" -f "_tmp.fasta\$" - - fi - - - ## Remove empty files (no sequences) - echo -e "\\nRemoving empty files" - find . -type f -name "*.fasta" -empty -print -delete - echo -e "..Done" - - ## Remove temporary file - rm derep.fasta - rm ${sampID}_primertrimmed.fq.gz - - ## Compress results - echo -e "\\nCompressing files" - - parallel -j${task.cpus} "gzip -${params.gzip_compression} {}" ::: \ - ${sampID}_hash_table.txt \ - ${sampID}_uc.uc \ - *.fasta \ - ${sampID}.extraction.results - - ## Convert ITSx output to Parquet - if [ ${params.ITSx_to_parquet} == true ]; then - - echo -e "\\nConverting ITSx output to Parquet" - mkdir -p parquet - - if [ -f ${sampID}.full.fasta.gz ]; then - ITSx_to_DuckDB.sh -i ${sampID}.full.fasta.gz -o parquet/${sampID}.full.parquet - fi - - if [ -f ${sampID}.SSU.fasta.gz ]; then - ITSx_to_DuckDB.sh -i ${sampID}.SSU.fasta.gz -o parquet/${sampID}.SSU.parquet - fi - - if [ -f ${sampID}.ITS1.fasta.gz ]; then - ITSx_to_DuckDB.sh -i ${sampID}.ITS1.fasta.gz -o parquet/${sampID}.ITS1.parquet - fi - - if [ -f ${sampID}.5_8S.fasta.gz ]; then - ITSx_to_DuckDB.sh -i ${sampID}.5_8S.fasta.gz -o parquet/${sampID}.5_8S.parquet - fi - - if [ -f ${sampID}.ITS2.fasta.gz ]; then - ITSx_to_DuckDB.sh -i ${sampID}.ITS2.fasta.gz -o parquet/${sampID}.ITS2.parquet - fi - - if [ -f ${sampID}.LSU.fasta.gz ]; then - ITSx_to_DuckDB.sh -i ${sampID}.LSU.fasta.gz -o parquet/${sampID}.LSU.parquet - fi - - echo -e "Parquet files created\\n" - - fi - - echo -e "..Done" - """ -} - -// Collect all ITS parts extracted by ITSx -process itsx_collect { - - label "main_container" - - publishDir "${out_3_itsxp}", mode: "${params.storagemode}" - // cpus 1 - - input: - path(itsx_full, stageAs: "full/*") - path(itsx_ssu, stageAs: "ssu/*") - path(itsx_its1, stageAs: "its1/*") - path(itsx_58s, stageAs: "58s/*") - path(itsx_its2, stageAs: "its2/*") - path(itsx_lsu, stageAs: "lsu/*") - path(itsx_ssu_part, stageAs: "ssu_partial/*") - path(itsx_its1_part, stageAs: "its1_partial/*") - path(itsx_58s_part, stageAs: "58s_partial/*") - path(itsx_its2_part, stageAs: "its2_partial/*") - path(itsx_lsu_part, stageAs: "lsu_partial/*") - - output: - path "ITS_Full.fasta.gz", emit: full, optional: true - path "SSU.fasta.gz", emit: ssu, optional: true - path "ITS1.fasta.gz", emit: its1, optional: true - path "5_8S.fasta.gz", emit: s58, optional: true - path "ITS2.fasta.gz", emit: its2, optional: true - path "LSU.fasta.gz", emit: lsu, optional: true - path "SSU_full_and_partial.fasta.gz", emit: ssu_part, optional: true - path "ITS1_full_and_partial.fasta.gz", emit: its1_part, optional: true - path "5_8S_full_and_partial.fasta.gz", emit: s58_part, optional: true - path "ITS2_full_and_partial.fasta.gz", emit: its2_part, optional: true - path "LSU_full_and_partial.fasta.gz", emit: lsu_part, optional: true - - script: - """ - # Check if each sub-dir has files with rRNA regions, then concatenate - - if [[ ! -n \$(find ./full -name NOFULL) ]]; then - echo -e "Pooling full ITS" - find full -name "*.fasta.gz" \ - | parallel -j1 "cat {}" >> ITS_Full.fasta.gz - fi - - if [[ ! -n \$(find ./ssu -name NOSSU) ]]; then - echo -e "Pooling SSU" - find ssu -name "*.fasta.gz" \ - | parallel -j1 "cat {}" >> SSU.fasta.gz - fi - - if [[ ! -n \$(find ./its1 -name NOITS1) ]]; then - echo -e "Pooling ITS1" - find its1 -name "*.fasta.gz" \ - | parallel -j1 "cat {}" >> ITS1.fasta.gz - fi - - if [[ ! -n \$(find ./58s -name NO58S) ]]; then - echo -e "Pooling 5.8S" - find 58s -name "*.fasta.gz" \ - | parallel -j1 "cat {}" >> 5_8S.fasta.gz - fi - - if [[ ! -n \$(find ./its2 -name NOITS2) ]]; then - echo -e "Pooling ITS2" - find its2 -name "*.fasta.gz" \ - | parallel -j1 "cat {}" >> ITS2.fasta.gz - fi - - if [[ ! -n \$(find ./lsu -name NOLSU) ]]; then - echo -e "Pooling LSU" - find lsu -name "*.fasta.gz" \ - | parallel -j1 "cat {}" >> LSU.fasta.gz - fi - - ##### Full and partial sequences ##### - - if [[ ! -n \$(find ./ssu_partial -name NOSSUPART) ]]; then - echo -e "Pooling SSU partial sequences" - find ssu_partial -name "*.fasta.gz" \ - | parallel -j1 "cat {}" >> SSU_full_and_partial.fasta.gz - fi - - if [[ ! -n \$(find ./its1_partial -name NOITS1PART) ]]; then - echo -e "Pooling ITS1 partial sequences" - find its1_partial -name "*.fasta.gz" \ - | parallel -j1 "cat {}" >> ITS1_full_and_partial.fasta.gz - fi - - if [[ ! -n \$(find ./58s_partial -name NO58SPART) ]]; then - echo -e "Pooling 5.8S partial sequences" - find 58s_partial -name "*.fasta.gz" \ - | parallel -j1 "cat {}" >> 5_8S_full_and_partial.fasta.gz - fi - - if [[ ! -n \$(find ./its2_partial -name NOITS2PART) ]]; then - echo -e "Pooling ITS2 partial sequences" - find its2_partial -name "*.fasta.gz" \ - | parallel -j1 "cat {}" >> ITS2_full_and_partial.fasta.gz - fi - - if [[ ! -n \$(find ./lsu_partial -name NOLSUPART) ]]; then - echo -e "Pooling LSU partial sequences" - find lsu_partial -name "*.fasta.gz" \ - | parallel -j1 "cat {}" >> LSU_full_and_partial.fasta.gz - fi - - echo -e "\\n..Done" - """ -} - - -// Trim primers (do not extract ITS) -// + Estimate sequence qualities -process trim_primers { - - label "main_container" - - publishDir "${out_3_trim}", mode: "${params.storagemode}" - // cpus 2 - - // Add sample ID to the log file - tag "${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}" - - input: - path input - - output: - path "${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}_hash_table.txt.gz", emit: hashes, optional: true - path "${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}_primertrimmed_sorted.fq.gz", emit: primertrimmed_fq, optional: true - path "${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}.fa.gz", emit: primertrimmed_fa, optional: true - path "${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}_uc.uc.gz", emit: uc, optional: true - tuple val("${task.process}"), val('cutadapt'), eval('cutadapt --version'), topic: versions - tuple val("${task.process}"), val('vsearch'), eval('vsearch --version 2>&1 | head -n 1 | sed "s/vsearch //g" | sed "s/,.*//g" | sed "s/^v//" | sed "s/_.*//"'), topic: versions - tuple val("${task.process}"), val('seqkit'), eval('seqkit version | sed "s/seqkit v//"'), topic: versions - tuple val("${task.process}"), val('phredsort'), eval('phredsort -v | sed "s/phredsort //"'), topic: versions - tuple val("${task.process}"), val('seqhasher'), eval('seqhasher -v | sed "s/SeqHasher //"'), topic: versions - - script: - sampID="${input.getSimpleName().replaceAll(/_PrimerChecked/, '')}" - - """ - echo -e "Trimming primers\\n" - echo -e "Input sample: " ${sampID} - echo -e "Forward primer: " ${params.primer_forward} - echo -e "Reverse primer: " ${params.primer_reverse} - - ## Reverse-complement rev priver - RR=\$(rc.sh ${params.primer_reverse}) - echo -e "Reverse primer RC: " "\$RR" - - echo -e "\\nTrimming primers" - cutadapt \ - -a ${params.primer_forward}";required;min_overlap=${params.primer_foverlap}"..."\$RR"";required;min_overlap=${params.primer_roverlap}" \ - --errors ${params.primer_mismatches} \ - --revcomp --rename "{header}" \ - --cores ${task.cpus} \ - --action=trim \ - --discard-untrimmed \ - --minimum-length ${params.trim_minlen} \ - --output ${sampID}_primertrimmed.fq.gz \ - ${input} - - - if [ -n "\$(find . -name ${sampID}_primertrimmed.fq.gz -prune -size +29c)" ]; then - - ## Estimate sequence quality and sort sequences by quality - echo -e "\\nSorting by sequence quality" - seqkit replace -p "\\s.+" ${sampID}_primertrimmed.fq.gz \ - | phredsort -i - -o - --metric meep --header avgphred,maxee,meep \ - | gzip -${params.gzip_compression} \ - > ${sampID}_primertrimmed_sorted.fq.gz - echo -e "..Done" - - rm ${sampID}_primertrimmed.fq.gz - - ## Hash sequences, add sample ID to the header - ## columns: Sample ID - Hash - PacBioID - AvgPhredScore - MaxEE - MEEP - Sequence - Quality - Length - ## Convert to Parquet format - echo -e "\\nCreating hash table" - seqhasher --hash sha1 --name ${sampID} ${sampID}_primertrimmed_sorted.fq.gz - \ - | seqkit fx2tab --length \ - | sed 's/;/\t/ ; s/;/\t/ ; s/ avgphred=/\t/ ; s/ maxee=/\t/ ; s/ meep=/\t/' \ - > ${sampID}_hash_table.txt - echo -e "..Done" - - ## Compress results - echo -e "Compressing result" - gzip -${params.gzip_compression} ${sampID}_hash_table.txt - - ## Dereplicate at sample level - echo -e "\\nDereplicating at sample level" - seqkit fq2fa -w 0 ${sampID}_primertrimmed_sorted.fq.gz \ - | vsearch \ - --derep_fulllength - \ - --output - \ - --strand both \ - --fasta_width 0 \ - --threads 1 \ - --relabel_sha1 \ - --sizein --sizeout \ - --uc ${sampID}_uc.uc \ - --quiet \ - | gzip -${params.gzip_compression} \ - > ${sampID}.fa.gz - - echo -e "..Done" - - ## Compress UC file - gzip -${params.gzip_compression} ${sampID}_uc.uc - - else - - echo -e "\\nNo sequences found after primer removal" - if [ -f ${sampID}_primertrimmed.fq.gz ]; then rm ${sampID}_primertrimmed.fq.gz; fi - - fi - - echo -e "..Done" - - """ -} - - - -// Assemble near-full-length ITS from ITSx output -process assemble_its { - - label "main_container" - - publishDir "${out_3_itsx}", mode: "${params.storagemode}" - // cpus 1 - - // Add sample ID to the log file - tag "${ITS1.getSimpleName()}" - - input: - path ITS1 - path S58 - path ITS2 - - output: - path "${ITS1.getSimpleName()}_ITS1_58S_ITS2.fasta.gz", emit: itsnf, optional: true - // path "ITS1_58S.fasta.gz", emit: its1p, optional: true - // path "58S_ITS2.fasta.gz", emit: its2p, optional: true - - script: - sampID="${ITS1.getSimpleName()}" - - """ - echo -e "Checking if ITS1, 5.8S, and ITS2 parts are available" - - if [[ -f ${ITS1} && ${S58} && ${ITS2} ]]; then - - echo -e "\\n..All parts found" - - ## Prepare tables for ID matching - echo -e "\\n..Converting data to tabular format" - seqkit fx2tab ${ITS1} | sed 's/\t\$//g' | csvtk add-header -t -n id,ITS1 > tmp_1_ITS1.txt - seqkit fx2tab ${S58} | sed 's/\t\$//g' | csvtk add-header -t -n id,58S > tmp_1_s58.txt - seqkit fx2tab ${ITS2} | sed 's/\t\$//g' | csvtk add-header -t -n id,ITS2 > tmp_1_ITS2.txt - - ## Join ITS fragments - echo -e "\\n..Joining ITS fragments" - csvtk join -t -f "id" tmp_1_ITS1.txt tmp_1_s58.txt tmp_1_ITS2.txt > tmp_2_ITS1_58S_ITS2.txt - - ## Check joining results - NUMSEQS=\$(wc -l < tmp_2_ITS1_58S_ITS2.txt) - echo "...Number of joined sequences: " \$((NUMSEQS - 1)) - - if [ "\$NUMSEQS" -gt 1 ]; then - - ## Convert table back to fasta - ## Remove leading and trailing Ns - echo -e "\\n..Preparing fasta" - awk 'NR>1 { print \$1 "\t" \$2\$3\$4 }' tmp_2_ITS1_58S_ITS2.txt \ - | seqkit tab2fx -w 0 \ - | seqkit replace -p "^n+|n+\$" -r "" -is -w 0 \ - | gzip -${params.gzip_compression} > ${sampID}_ITS1_58S_ITS2.fasta.gz - - else - echo "...There are no sequences with all ITS parts present\\n" - echo -e "\\n..Skipping ITS assembly for this sample" - fi - - else - echo -e "\\n..Some or all parts are missing" - echo -e "\\n..Skipping ITS assembly for this sample" - fi - - """ -} - - - -// Merge tables with sequence qualities -process seq_qual { - - label "main_container" - - publishDir "${out_9_db}", mode: "${params.storagemode}" - // cpus 4 - - input: - path(input, stageAs: "hash_tables/*") - - output: - path "SeqQualities.parquet", emit: quals - tuple val("${task.process}"), val('duckdb'), eval('duckdb --version | cut -d" " -f1 | sed "s/^v//"'), topic: versions - - script: - def memoryArg = task.memory ? "-m ${task.memory.toMega()}.MB" : "" - """ - echo -e "Aggregating sequence qualities" - - merge_hash_tables.sh \ - -i ./hash_tables \ - -o SeqQualities.parquet \ - -t ${task.cpus} \ - ${memoryArg} - - echo -e "..Done" - """ -} - - -// Homopolymer compression -process homopolymer { - - label "main_container" - - publishDir "${out_4_homop}", mode: "${params.storagemode}" - // cpus 1 - - // Add sample ID to the log file - tag "${input.getSimpleName().replaceAll(/_ITS1_58S_ITS2/, '')}" - - input: - path input - - output: - path "${input.getSimpleName().replaceAll(/_ITS1_58S_ITS2/, '')}_Homopolymer_compressed.fa.gz", emit: hc, optional: true - path "${input.getSimpleName().replaceAll(/_ITS1_58S_ITS2/, '')}_uch.uc.gz", emit: uch, optional: true - tuple val("${task.process}"), val('vsearch'), eval('vsearch --version 2>&1 | head -n 1 | sed "s/vsearch //g" | sed "s/,.*//g" | sed "s/^v//" | sed "s/_.*//"'), topic: versions - tuple val("${task.process}"), val('seqkit'), eval('seqkit version | sed "s/seqkit v//"'), topic: versions - tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //" | cut -d" " -f1'), topic: versions - tuple val("${task.process}"), val('data.table'), eval('Rscript -e "cat(as.character(packageVersion(\'data.table\')))"'), topic: versions - - script: - sampID="${input.getSimpleName().replaceAll(/_ITS1_58S_ITS2/, '')}" - - """ - - ## Homopolymer compression - echo -e "Homopolymer compression" - - zcat ${input} \ - | homopolymer_compression.sh - \ - > homo_compressed.fa - - echo -e "..Done" - - ## Re-cluster homopolymer-compressed data - echo -e "\\nRe-clustering homopolymer-compressed data" - vsearch \ - --cluster_size homo_compressed.fa \ - --id ${params.hp_similarity} \ - --iddef ${params.hp_iddef} \ - --qmask "dust" \ - --strand "both" \ - --fasta_width 0 \ - --threads ${task.cpus} \ - --sizein --sizeout \ - --minseqlength 20 \ - --centroids homo_clustered.fa \ - --uc ${sampID}_uch.uc - echo -e "..Done" - - ## Check if clustering was succeful - ## (e.g., if all compressed sequences were too short, the file with be empty) - if [ -s homo_clustered.fa ]; then - - ## Compress UC file - gzip -${params.gzip_compression} ${sampID}_uch.uc - - ## Substitute homopolymer-comressed sequences with uncompressed ones - ## (update size annotaions) - echo -e "\\nExtracting sequences" - - seqkit fx2tab ${input} > inp_tab.txt - seqkit fx2tab homo_clustered.fa > clust_tab.txt - - if [ -s inp_tab.txt ]; then - substitute_compressed_seqs.R \ - inp_tab.txt clust_tab.txt res.fa - - echo -e "..Done" - else - echo -e "..Input data looks empty, nothing to proceed with" - fi - - if [ -s res.fa ]; then - gzip -c res.fa > ${sampID}_Homopolymer_compressed.fa.gz - fi - - ## Remove temporary files - rm homo_compressed.fa - rm homo_clustered.fa - rm inp_tab.txt - rm clust_tab.txt - rm res.fa - - else - echo -e "Clustering homopolymer-compressed sequences returned to results" - echo -e "(most likely, sequences were too short)\\n" - fi - - """ -} - - -// If no homopolymer compression is required, just dereplicate the samples -process just_derep { - - label "main_container" - - // publishDir "${out_4_homop}", mode: "${params.storagemode}" - // cpus 1 - - // Add sample ID to the log file - tag "${input.getSimpleName()}" - - input: - path input - - output: - path "${input.getSimpleName()}.fa.gz", emit: nhc, optional: true - path "${input.getSimpleName()}_uch.uc.gz", emit: ucnh, optional: true - tuple val("${task.process}"), val('vsearch'), eval('vsearch --version 2>&1 | head -n 1 | sed "s/vsearch //g" | sed "s/,.*//g" | sed "s/^v//" | sed "s/_.*//"'), topic: versions - - script: - sampID="${input.getSimpleName()}" - - """ - echo -e "Dereplicating sequences\\n" - - vsearch \ - --derep_fulllength ${input} \ - --output - \ - --strand both \ - --fasta_width 0 \ - --threads 1 \ - --sizein --sizeout \ - --uc ${sampID}_uc.uc \ - | gzip -${params.gzip_compression} \ - > ${sampID}.fa.gz - - """ -} - - -// Pool sequences from all samples and add sample ID into header (for OTU and "ASV" table creation) -process pool_seqs { - - label "main_container" - - // publishDir "${out_6_tj}", mode: "${params.storagemode}" - // cpus 2 - - input: - path(input, stageAs: 'sequences/*') - - output: - path "Seq_tab_not_filtered.txt.gz", emit: seqtabnf - path "Seq_not_filtered.fa.gz", emit: seqsnf - tuple val("${task.process}"), val('seqkit'), eval('seqkit version | sed "s/seqkit v//"'), topic: versions - tuple val("${task.process}"), val('parallel'), eval('parallel --version | head -n 1 | sed "s/GNU parallel //"'), topic: versions - - script: - """ - - echo -e "\\nPooling and renaming sequences" - - ## If there is a sample ID in the header already, remove it - parallel -j 1 --group \ - --rpl '{/:} s:(.*/)?([^/.]+)(\\.[^/]+)*\$:\$2:' \ - "zcat {} \ - | sed -r '/^>/ s/;sample=[^;]*/;/g ; s/;;/;/g' \ - | sed 's/>.*/&;sample='{/:}';/ ; s/_NoChimera//g ; s/_RescuedChimera//g ; s/_JoinedPE//g ; s/_Homopolymer_compressed//g' \ - | sed 's/Rescued_Chimeric_sequences.part_//g' \ - | sed -r '/^>/ s/;;/;/g'" \ - ::: sequences/*.fa.gz \ - | vsearch --sortbysize - --sizein --sizeout --fasta_width 0 --output - \ - | sed -r '/^>/ s/;;/;/g' \ - | gzip -${params.gzip_compression} \ - > Seq_not_filtered.fa.gz - - echo "..Done" - - echo -e "\\nExtracting sequence count table" - seqkit seq --name Seq_not_filtered.fa.gz \ - | sed 's/;/\t/g; s/size=//; s/sample=// ; s/\t*\$//' \ - | csvtk -t cut -f 2,1,3 \ - | csvtk -t add-header -n "SampleID,SeqID,Abundance" \ - | gzip -${params.gzip_compression} \ - > Seq_tab_not_filtered.txt.gz - - echo "..Done" - - """ -} - - -// De-novo clustering of sequences for tag-jump removal -process tj_preclust { - - label "main_container" - - // publishDir "${out_6_tj}", mode: "${params.storagemode}" - // cpus 10 - - input: - path input - - output: - path "TJPreclust.uc.parquet", emit: preclust_uc_parquet - tuple val("${task.process}"), val('vsearch'), eval('vsearch --version 2>&1 | head -n 1 | sed "s/vsearch //g" | sed "s/,.*//g" | sed "s/^v//" | sed "s/_.*//"'), topic: versions - - script: - def derep = (params.tj_id as BigDecimal).compareTo(1G) == 0 // to handle floating point comparisons too - """ - echo -e "Pre-clustering sequences prior to tag-jump removal\\n" - - echo -e "Running dereplication\\n" - - vsearch \ - --derep_fulllength ${input} \ - --sizein --sizeout \ - --strand both \ - --fasta_width 0 \ - --threads 1 \ - --uc Dereplicated.uc \ - --output Dereplicated.fa - - echo -e "\\nCompressing files" - pigz -p ${task.cpus} -${params.gzip_compression} Dereplicated.uc - pigz -p ${task.cpus} -${params.gzip_compression} Dereplicated.fa - - ## Additional clustering (e.g., at 99% similarity) - if [[ ${derep} == false ]]; then - - echo -e "\\nAdditional clustering at ${params.tj_id} similarity threshold\\n" - - vsearch \ - --cluster_size Dereplicated.fa.gz \ - --id ${params.tj_id} \ - --iddef ${params.tj_iddef} \ - --sizein --sizeout \ - --qmask dust --strand plus \ - --maxrejects 128 --maxaccepts 1 \ - --fasta_width 0 \ - --threads ${task.cpus} \ - --uc Clustered.uc \ - --centroids Clustered.fa - - echo -e "\\nCompressing files" - pigz -p ${task.cpus} -${params.gzip_compression} Clustered.uc - pigz -p ${task.cpus} -${params.gzip_compression} Clustered.fa - - fi - - - ## Parse UC file - if [[ ${derep} == true ]]; then - - echo -e "\\nParsing UC file" - ucs --map-only --split-id --rm-dups \ - -i Dereplicated.uc.gz \ - -o TJPreclust.uc.parquet - - else - - echo -e "\\nParsing dereplicated UC file" - ucs --map-only --split-id --rm-dups \ - -i Dereplicated.uc.gz \ - -o Dereplicated.parquet - - echo -e "\\nParsing clustered UC file" - ucs --map-only --split-id --rm-dups \ - -i Clustered.uc.gz \ - -o Clustered.parquet - - echo -e "\\nCombining dereplication and clustering UC files" - merge_tj_memberships.sh \ - -d Dereplicated.parquet \ - -c Clustered.parquet \ - -o TJPreclust.uc.parquet \ - -t ${task.cpus} - - fi - - echo -e "\\n..Done" - """ -} - - - -// Tag-jump removal -process tj { - - label "main_container" - - publishDir "${out_6_tj}", mode: "${params.storagemode}" - // cpus 1 - - input: - path seqtab // seq table in long format - path precls // pre-clustered membership - - output: - path "Seq_tab_TagJumpFiltered.txt.gz", emit: seqtabtj - path "TagJump_scores.qs", emit: tjs - path "TagJump_plot.pdf" - tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //" | cut -d" " -f1'), topic: versions - tuple val("${task.process}"), val('data.table'), eval('Rscript -e "cat(as.character(packageVersion(\'data.table\')))"'), topic: versions - tuple val("${task.process}"), val('ggplot2'), eval('Rscript -e "cat(as.character(packageVersion(\'ggplot2\')))"'), topic: versions - - script: - """ - - echo -e "Tag-jump removal" - - tag_jump_removal_longtab.R \ - --seqtab ${seqtab} \ - --precls ${precls} \ - -f ${params.tj_f} \ - -p ${params.tj_p} - - echo "..Done" - - """ -} - - - - -// Prepare a table with non-tag-jumped sequences -// Add quality estimate to singletons -// Add chimera-scores for putative de novo chimeras -process prep_seqtab { - - label "main_container" - - publishDir "${out_7_seq}", mode: "${params.storagemode}" - // cpus 4 - - input: - path seqtab // tag-jump filtered sequence table (long format) - path seqsnf // sequences in FASTA - path denovos // de novo chimera scores - path quals // quality scores - - output: - path "Seqs.parquet", emit: seq_pq - path "Seqs.txt.gz", emit: seq_tl // long table - path "Seqs.fa.gz", emit: seq_fa - // path "Seqs.RData", emit: seq_rd // deprecated - // path "Seq_tab.txt.gz", emit: seq_tw // wide table - tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //" | cut -d" " -f1'), topic: versions - tuple val("${task.process}"), val('data.table'), eval('Rscript -e "cat(as.character(packageVersion(\'data.table\')))"'), topic: versions - tuple val("${task.process}"), val('arrow'), eval('Rscript -e "cat(as.character(packageVersion(\'arrow\')))"'), topic: versions - tuple val("${task.process}"), val('Biostrings'), eval('Rscript -e "cat(as.character(packageVersion(\'Biostrings\')))"'), topic: versions - - script: - """ - - echo -e "Sequence table creation" - - seq_table_assembly.R \ - --seqtab ${seqtab} \ - --fasta ${seqsnf} \ - --chimera ${denovos} \ - --quality ${quals} \ - --threads ${task.cpus} - - echo "..Done" - - """ -} - - - - - - - -// Run summary - count number of reads in the output of different processes -process read_counts { - - label "main_container" - - publishDir "${out_8_smr}", mode: "${params.storagemode}", pattern: "*.xlsx" - publishDir "${out_8_smr}/PerProcessStats", mode: "${params.storagemode}", pattern: "*.txt" - // cpus 4 - - input: - path(input_fastq, stageAs: "1_input/*") - path(qc, stageAs: "2_qc/*") - path(samples_demux, stageAs: "3_demux/*") - path(samples_primerch, stageAs: "4_primerch/*") - path(samples_primermult, stageAs: "4_primerartefacts/*") - path(samples_itsx_or_primertrim, stageAs: "5_itsxtrim/*") - path(homopolymers, stageAs: "5_homopolymers/*") - path(samples_chimref, stageAs: "6_chimref/*") - path(samples_chimdenovo, stageAs: "7_chimdenov/*") - path(chimera_recovered, stageAs: "8_chimrecov/*") - path(samples_tj) - path(seqtab) - - output: - path "Run_summary.xlsx", emit: xlsx - path "Counts_1.RawData.txt", emit: counts_1_raw - path "Counts_2.QC.txt", emit: counts_2_qc - path "Counts_3.Demux.txt", emit: counts_3_demux, optional: true - path "Counts_4.PrimerCheck.txt", emit: counts_4_primer, optional: true - path "Counts_4.PrimerArtefacts.txt", emit: counts_4_primerartef, optional: true - path "Counts_5.ITSx_or_PrimTrim.txt", emit: counts_5_itsx_ptrim, optional: true - path "Counts_5.Homopolymers.txt", emit: counts_5_homopolymers, optional: true - path "Counts_6.ChimRef_reads.txt", emit: counts_6_chimref_r, optional: true - path "Counts_6.ChimRef_uniqs.txt", emit: counts_6_chimref_u, optional: true - path "Counts_7.ChimDenov.txt", emit: counts_7_chimdenov, optional: true - path "Counts_8.ChimRecov_reads.txt", emit: counts_8_chimrecov_r, optional: true - path "Counts_8.ChimRecov_uniqs.txt", emit: counts_8_chimrecov_u, optional: true - tuple val("${task.process}"), val('seqkit'), eval('seqkit version | sed "s/seqkit v//"'), topic: versions - tuple val("${task.process}"), val('parallel'), eval('parallel --version | head -n 1 | sed "s/GNU parallel //"'), topic: versions - tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //" | cut -d" " -f1'), topic: versions - tuple val("${task.process}"), val('data.table'), eval('Rscript -e "cat(as.character(packageVersion(\'data.table\')))"'), topic: versions - - script: - - """ - echo -e "Summarizing run statistics\\n" - echo -e "Counting the number of reads in:\\n" - - - ## Count raw reads - echo -e "\\n..Raw data" - seqkit stat --basename --tabular --threads ${task.cpus} --quiet \ - 1_input/* > Counts_1.RawData.txt - - ## Count number of reads passed QC - echo -e "\\n..Sequenced passed QC" - seqkit stat --basename --tabular --threads ${task.cpus} --quiet \ - 2_qc/* > Counts_2.QC.txt - - ## Count demultiplexed reads - echo -e "\\n..Demultiplexed data" - seqkit stat --basename --tabular --threads ${task.cpus} --quiet \ - 3_demux/* > Counts_3.Demux.txt - - - ## Count primer-checked reads - echo -e "\\n..Primer-checked data" - if [ `find 4_primerch -name no_primerchecked 2>/dev/null` ] - then - echo -e "... No files found" - touch Counts_4.PrimerCheck.txt - else - seqkit stat --basename --tabular --threads ${task.cpus} --quiet \ - 4_primerch/* > Counts_4.PrimerCheck.txt - fi - - - ## Count primer-artefacts - echo -e "\\n..Primer-artefacts" - if [ `find 4_primerartefacts -name no_multiprimer 2>/dev/null` ] - then - echo -e "... No files found" - touch Counts_4.PrimerArtefacts.txt - else - seqkit stat --basename --tabular --threads ${task.cpus} --quiet \ - 4_primerartefacts/* > Counts_4.PrimerArtefacts.txt - fi - - - ## Count ITSx reads or primer-trimmed reads (if ITSx was not used) - ## Take number of reads into account (--sizein) - echo -e "\\n..ITSx- or primer-trimmed data" - if [ `find 5_itsxtrim \\( -name no_itsx -o -name no_primertrim \\) 2>/dev/null` ] - then - echo -e "... No files found" - touch Counts_5.ITSx_or_PrimTrim.txt - else - find 5_itsxtrim -name "*.fasta.gz" \ - | parallel -j ${task.cpus} "count_number_of_reads.sh {} {/.}" \ - | sed '1i SampleID\tNumReads' \ - > Counts_5.ITSx_or_PrimTrim.txt - fi - - - ## Count homopolymer-correction results - echo -e "\\n..Counting homopolymer-corrected reads" - if [ `find 5_homopolymers \\( -name no_homopolymer \\) 2>/dev/null` ] - then - echo -e "... No files found" - touch Counts_5.Homopolymers.txt - else - find 5_homopolymers -name "*.uc.gz" \ - | parallel -j ${task.cpus} "count_homopolymer_stats.sh {} {/.}" \ - | sed '1i SampleID\tQuery\tTarget' \ - > Counts_5.Homopolymers.txt - fi - - ## Count number of reads for reference-based chimeras - echo -e "\\n..Reference-based chimeras" - if [ `find 6_chimref -name no_chimref 2>/dev/null` ] - then - echo -e "... No files found" - touch Counts_6.ChimRef_reads.txt - touch Counts_6.ChimRef_uniqs.txt - else - - ## Count number of reads - find 6_chimref -name "*.fa.gz" \ - | parallel -j ${task.cpus} "count_number_of_reads.sh {} {/.}" \ - | sed '1i SampleID\tNumReads' \ - > Counts_6.ChimRef_reads.txt - - ## Count number of unique sequences - seqkit stat --basename --tabular --threads ${task.cpus} --quiet \ - 6_chimref/* > Counts_6.ChimRef_uniqs.txt - - fi - - - ## Number of de novo chimeras (read counts are not taken into account!) - echo -e "\\n..De novo chimeras" - if [ `find 7_chimdenov -name no_chimdenovo 2>/dev/null` ] - then - echo -e "... No files found" - touch Counts_7.ChimDenov.txt - else - cat 7_chimdenov/* > Counts_7.ChimDenov.txt - fi - - - ## Rescued chimeras - echo -e "\\n..Rescued chimeric sequences" - if [ `find 8_chimrecov -name no_chimrescued 2>/dev/null` ] - then - echo -e "... No files found" - touch Counts_8.ChimRecov_reads.txt - touch Counts_8.ChimRecov_uniqs.txt - else - - ## Count number of reads - echo -e "...Reads" - find 8_chimrecov -name "*.fa.gz" \ - | parallel -j ${task.cpus} "count_number_of_reads.sh {} {/.}" \ - | sed '1i SampleID\tNumReads' \ - > Counts_8.ChimRecov_reads.txt - - ## Count number of unique sequences - echo -e "...Unique sequences" - seqkit stat --basename --tabular --threads ${task.cpus} --quiet \ - 8_chimrecov/* > Counts_8.ChimRecov_uniqs.txt - - fi - - ## Summarize read counts - read_count_summary.R \ - --raw Counts_1.RawData.txt \ - --qc Counts_2.QC.txt \ - --demuxed Counts_3.Demux.txt \ - --primer Counts_4.PrimerCheck.txt \ - --primerartef Counts_4.PrimerArtefacts.txt \ - --itsx Counts_5.ITSx_or_PrimTrim.txt \ - --homopolymer Counts_5.Homopolymers.txt \ - --chimrefn Counts_6.ChimRef_reads.txt \ - --chimrefu Counts_6.ChimRef_uniqs.txt \ - --chimdenovo Counts_7.ChimDenov.txt \ - --chimrecovn Counts_8.ChimRecov_reads.txt \ - --chimrecovu Counts_8.ChimRecov_uniqs.txt \ - --tj ${samples_tj} \ - --seqtab ${seqtab} \ - --maxchim ${params.max_ChimeraScore} \ - --threads ${task.cpus} - - """ -} - -// Quick stats of demultiplexing and primer checking steps -// (for the `seqstats` sub-workflow) -process quick_stats { - - label "main_container" - - publishDir "${out_3_quickstats}", mode: "${params.storagemode}", pattern: "*.xlsx" - publishDir "${out_3_quickstats}/PerProcessStats", mode: "${params.storagemode}", pattern: "*.txt" - // cpus 5 - - input: - path(input_fastq, stageAs: "1_input/*") - path(qc, stageAs: "2_qc/*") - path(samples_demux, stageAs: "3_demux/*") - path(samples_primerch, stageAs: "4_primerch/*") - path(samples_primermult, stageAs: "4_primerartefacts/*") - - output: - path "Run_summary.xlsx", emit: xlsx - path "Counts_1.RawData.txt", emit: counts_1_raw - path "Counts_2.QC.txt", emit: counts_2_qc - path "Counts_3.Demux.txt", emit: counts_3_demux, optional: true - path "Counts_4.PrimerCheck.txt", emit: counts_4_primer, optional: true - path "Counts_4.PrimerArtefacts.txt", emit: counts_4_primerartef, optional: true - tuple val("${task.process}"), val('seqkit'), eval('seqkit version | sed "s/seqkit v//"'), topic: versions - tuple val("${task.process}"), val('parallel'), eval('parallel --version | head -n 1 | sed "s/GNU parallel //"'), topic: versions - tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //" | cut -d" " -f1'), topic: versions - tuple val("${task.process}"), val('data.table'), eval('Rscript -e "cat(as.character(packageVersion(\'data.table\')))"'), topic: versions - - script: - - """ - echo -e "Summarizing run statistics\\n" - echo -e "Counting the number of reads in:\\n" - - - ## Count raw reads - echo -e "\\n..Raw data" - seqkit stat --basename --tabular --threads ${task.cpus} \ - 1_input/* > Counts_1.RawData.txt - - ## Count number of reads passed QC - echo -e "\\n..Sequenced passed QC" - seqkit stat --basename --tabular --threads ${task.cpus} \ - 2_qc/* > Counts_2.QC.txt - - ## Count demultiplexed reads - echo -e "\\n..Demultiplexed data" - seqkit stat --basename --tabular --threads ${task.cpus} \ - 3_demux/* > Counts_3.Demux.txt - - ## Count primer-checked reads - echo -e "\\n..Primer-checked data" - if [ `find 4_primerch -name no_primerchecked 2>/dev/null` ] - then - echo -e "... No files found" - touch Counts_4.PrimerCheck.txt - else - seqkit stat --basename --tabular --threads ${task.cpus} \ - 4_primerch/* > Counts_4.PrimerCheck.txt - fi - - ## Count primer-artefacts - echo -e "\\n..Primer-areifacts" - if [ `find 4_primerartefacts -name no_multiprimer 2>/dev/null` ] - then - echo -e "... No files found" - touch Counts_4.PrimerArtefacts.txt - else - seqkit stat --basename --tabular --threads ${task.cpus} \ - 4_primerartefacts/* > Counts_4.PrimerArtefacts.txt - fi - - ## Summarize read counts - quick_stats.R \ - --raw Counts_1.RawData.txt \ - --qc Counts_2.QC.txt \ - --demuxed Counts_3.Demux.txt \ - --primer Counts_4.PrimerCheck.txt \ - --primerartef Counts_4.PrimerArtefacts.txt \ - --threads ${task.cpus} - - """ -} - -// Auto documentation of analysis procedures -// (generate narrative description of methods with references) -process document_analysis_s1 { - - label "main_container" - - publishDir "${out_tracedir}", mode: 'copy', overwrite: true - // cpus 1 - - input: - path versions // "software_versions.yml" - path params // "pipeline_params.tsv" - - output: - path "README_Step1_Methods.txt", emit: docs - - - script: - """ - echo -e "Descriptive summary generation\\n" - - document_s1.R \ - ${versions} \ - ${params} \ - README_Step1_Methods.txt - - """ -} - - - - -// The default workflow - Step-1 -workflow S1 { - - // Primer disambiguation - disambiguate() - - - /* - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Demultiplex data - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - */ - - // Run demultiplexing - if( params.demultiplexed == false ){ - - // Input file with barcodes (FASTA) - ch_barcodes = Channel.value(params.barcodes) - - // Validate tags - tag_validation(ch_barcodes) - - // PacBio - if ( params.seqplatform == "PacBio" ) { - - // Input file with multiplexed reads (FASTQ.gz or BAM) - ch_input = Channel.value(params.input) - - // Check the extension of input - input_type = file(params.input).getExtension() =~ /bam|BAM/ ? "bam" : "oth" - // println("${input_type}") - - // If BAM is provided as input, convert it to FASTQ - if ( input_type == 'bam'){ - - // Add BAM index file - ch_input_pbi = ch_input + ".pbi" - - bam2fastq(ch_input, ch_input_pbi) - qc_se(bam2fastq.out.fastq) - - } else { - - // Initial QC - qc_se(ch_input) - - } - - // Demultiplexing with dual barcodes requires 4 additional files: - // - "biosamples" with symmertic/asymmetirc tag combinations - // - table for assigning sample names to demuxed files - // - and a table for renaming unknown combinations (if params.lima_remove_unknown == true) - // Create dummy files (for single or symmetic tags) if neccesary - ch_biosamples_sym = tag_validation.out.biosamples_sym.flatten().collect().ifEmpty(file("biosamples_sym")) - ch_biosamples_asym = tag_validation.out.biosamples_asym.flatten().collect().ifEmpty(file("biosamples_asym")) - ch_file_renaming = tag_validation.out.file_renaming.flatten().collect().ifEmpty(file("file_renaming")) - ch_unknown_combs = tag_validation.out.unknown_combinations.flatten().collect().ifEmpty(file("unknown_combinations")) - - // Demultiplexing - demux( - qc_se.out.filtered, - tag_validation.out.fasta, - ch_biosamples_sym, - ch_biosamples_asym, - ch_file_renaming, - ch_unknown_combs) - - // Check primers - primer_check( - demux.out.samples_demux.flatten(), - disambiguate.out.F, - disambiguate.out.R, - disambiguate.out.Fr, - disambiguate.out.Rr - ) - - } // end of PacBio-specific tasks - - // Illumina - if ( params.seqplatform == "Illumina" ) { - - // Input file with multiplexed pair-end reads (FASTQ.gz) - ch_inputR1 = Channel.value(params.input_R1) - ch_inputR2 = Channel.value(params.input_R2) - - // Initial QC - qc_pe(ch_inputR1, ch_inputR2) - - // PE assembly - merge_pe( - qc_pe.out.filtered_R1, - qc_pe.out.filtered_R2) - - // Modify barcodes (restict search window) - prep_barcodes(tag_validation.out.fasta) - - // Demultiplexing - demux_illumina( - merge_pe.out.r12, - prep_barcodes.out.barcodesm) - - ch_demux_merged = demux_illumina.out.samples_demux.flatten() - - // Illumina nonmerged PE reads sub-workflow (optional) - if(params.illumina_keep_notmerged == true){ - - // Demultiplexing non-merged reads - demux_illumina_notmerged( - merge_pe.out.nm, - prep_barcodes.out.barcodesm) - - // Channel of non-merged reads by sample (split into sample tuples) - // ch_R1 = demux_illumina_notmerged.out.demux_pe.... - - // Non-merged sample list - ch_nonmerged = demux_illumina_notmerged.out.samples_nonm_pe.splitText().map{it -> it.trim()} - - // Trim primers of nonmerged PE reads - // Estimate sequence qualities - // Dereplicate R1 and R2 independently - // trim_primers_pe(demux_illumina_notmerged.out.demux_pe.flatten()) - - // Join nonmerged reads with poly-N pads - join_pe( - ch_nonmerged, - demux_illumina_notmerged.out.demux_pe.flatten().collect() // all non-merged R1 and R2 files - ) - - // Add joined reads to the merged reads - ch_joined = join_pe.out.jj_FQ.flatten() - ch_demuxed = ch_demux_merged.concat(ch_joined) - - } else { // end of Illumina non-merged reads - - // Channel with demultiplexed reads - ch_demuxed = ch_demux_merged - - } - - // Check primers - primer_check( - ch_demuxed, - disambiguate.out.F, - disambiguate.out.R, - disambiguate.out.Fr, - disambiguate.out.Rr - ) - - } // end of Illumina-specific tasks - - } // end of demultiplexing - - - - // If samples were already demuliplexed - if( params.demultiplexed == true ){ - - // Input files with demultiplexed reads (FASTQ.gz) - ch_input = Channel.fromPath( params.input + '/*.{fastq.gz,fastq,fq.gz,fq}' ) - - // Check if the input channel is empty - ch_input - .ifEmpty { - error("ERROR: No FASTQ files found in the input directory: ${params.input}") - exit 1 - } - - // QC - qc_se(ch_input) - - // Check primers - primer_check( - qc_se.out.filtered, - disambiguate.out.F, - disambiguate.out.R, - disambiguate.out.Fr, - disambiguate.out.Rr - ) - - } // end of pre-demultiplexed branch - - - /* - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - ITS extraction or primer trimming - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - */ - - // Extract ITS - if(params.its_region == "full" || params.its_region == "ITS1" || params.its_region == "ITS2" || params.its_region == "SSU" || params.its_region == "LSU"){ - - // Run ITSx - itsx(primer_check.out.fq_primer_checked) - - // Merge tables with sequence qualities - seq_qual(itsx.out.hashes.collect()) - } - - // Trim the primers (instead of ITS extraction) - if(params.its_region == "none"){ - - // Trim primers with cutadapt - trim_primers(primer_check.out.fq_primer_checked) - - // Merge tables with sequence qualities - seq_qual(trim_primers.out.hashes.collect()) - } - - // Trim the primers, run ITSx, and assemble near-full-length ITS - if(params.its_region == "ITS1_5.8S_ITS2"){ - - // Run ITSx - itsx(primer_check.out.fq_primer_checked) - - // Assemble ITS1-5.8S-ITS2 from ITSx-extracted parts - if (params.ITSx_partial == 0) { - assemble_its( - itsx.out.itsx_its1, - itsx.out.itsx_58s, - itsx.out.itsx_its2) - } else { - assemble_its( - itsx.out.itsx_its1_part, - itsx.out.itsx_58s, - itsx.out.itsx_its2_part) - } - - // Merge tables with sequence qualities - seq_qual(itsx.out.hashes.collect()) - } - - - // Collect ITSx-extracted sequences - if(params.its_region == "full" || params.its_region == "ITS1" || params.its_region == "ITS2" || params.its_region == "SSU" || params.its_region == "LSU" || params.its_region == "ITS1_5.8S_ITS2"){ - - // Collect rRNA parts into separate channels - ch_cc_full = itsx.out.itsx_full.flatten().collect().ifEmpty(file("NOFULL")) - ch_cc_ssu = itsx.out.itsx_ssu.flatten().collect().ifEmpty(file("NOSSU")) - ch_cc_its1 = itsx.out.itsx_its1.flatten().collect().ifEmpty(file("NOITS1")) - ch_cc_58s = itsx.out.itsx_58s.flatten().collect().ifEmpty(file("NO58S")) - ch_cc_its2 = itsx.out.itsx_its2.flatten().collect().ifEmpty(file("NOITS2")) - ch_cc_lsu = itsx.out.itsx_lsu.flatten().collect().ifEmpty(file("NOLSU")) - - ch_cc_ssu_part = itsx.out.itsx_ssu_part.flatten().collect().ifEmpty(file("NOSSUPART")) - ch_cc_its1_part = itsx.out.itsx_its1_part.flatten().collect().ifEmpty(file("NOITS1PART")) - ch_cc_58s_part = itsx.out.itsx_58s_part.flatten().collect().ifEmpty(file("NO58SPART")) - ch_cc_its2_part = itsx.out.itsx_its2_part.flatten().collect().ifEmpty(file("NOITS2PART")) - ch_cc_lsu_part = itsx.out.itsx_lsu_part.flatten().collect().ifEmpty(file("NOLSUPART")) - - itsx_collect( - ch_cc_full, - ch_cc_ssu, - ch_cc_its1, - ch_cc_58s, - ch_cc_its2, - ch_cc_lsu, - ch_cc_ssu_part, - ch_cc_its1_part, - ch_cc_58s_part, - ch_cc_its2_part, - ch_cc_lsu_part - ) - - } // end of ITSx-extracted sequences - - - /* - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Homopolymer compression & chimera removal - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - */ - - // Homopolymer compression - if(params.hp == true){ - - // --Full-length ITS sequences - if(params.its_region == "full"){ - homopolymer(itsx.out.itsx_full) - } - // --ITS1 sequences - if(params.its_region == "ITS1"){ - if (params.ITSx_partial == 0) { - homopolymer(itsx.out.itsx_its1) - } else { - homopolymer(itsx.out.itsx_its1_part) - } - } - // --ITS2 sequences - if(params.its_region == "ITS2"){ - if (params.ITSx_partial == 0) { - homopolymer(itsx.out.itsx_its2) - } else { - homopolymer(itsx.out.itsx_its2_part) - } - } - // --SSU sequences - if(params.its_region == "SSU"){ - if (params.ITSx_partial == 0) { - homopolymer(itsx.out.itsx_ssu) - } else { - homopolymer(itsx.out.itsx_ssu_part) - } - } - // --LSU sequences - if(params.its_region == "LSU"){ - if (params.ITSx_partial == 0) { - homopolymer(itsx.out.itsx_lsu) - } else { - homopolymer(itsx.out.itsx_lsu_part) - } - } - - // --Primer-trimmed sequences - if(params.its_region == "none"){ - homopolymer(trim_primers.out.primertrimmed_fa) - } - // Near-full-length ITS - if(params.its_region == "ITS1_5.8S_ITS2"){ - homopolymer(assemble_its.out.itsnf) - } - - - } else { - // No homopolymer comression is required, - // Just dereplicate the data - - if(params.its_region == "full" || params.its_region == "ITS1" || params.its_region == "ITS2" || params.its_region == "SSU" || params.its_region == "LSU"){ - - // --Full-length ITS sequences - if(params.its_region == "full"){ - just_derep(itsx.out.itsx_full) - } - // --ITS1 sequences - if(params.its_region == "ITS1"){ - if (params.ITSx_partial == 0) { - just_derep(itsx.out.itsx_its1) - } else { - just_derep(itsx.out.itsx_its1_part) - } - } - // --ITS2 sequences - if(params.its_region == "ITS2"){ - if (params.ITSx_partial == 0) { - just_derep(itsx.out.itsx_its2) - } else { - just_derep(itsx.out.itsx_its2_part) - } - } - // --SSU sequences - if(params.its_region == "SSU"){ - if (params.ITSx_partial == 0) { - just_derep(itsx.out.itsx_ssu) - } else { - just_derep(itsx.out.itsx_ssu_part) - } - } - // --LSU sequences - if(params.its_region == "LSU"){ - if (params.ITSx_partial == 0) { - just_derep(itsx.out.itsx_lsu) - } else { - just_derep(itsx.out.itsx_lsu_part) - } - } - - } // end of ITS - - - } // end of homopolymer correction condition - - - /* - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Chimera removal - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - */ - - - // Chimera removal (optional) - ch_chimerabd = Channel.value(params.chimera_db) - - // Input depends on the selected workflow - if(params.hp == true){ - - ch_input_for_chim = homopolymer.out.hc - - } else { - - if(params.its_region == "none"){ - ch_input_for_chim = trim_primers.out.primertrimmed_fa - } else if(params.its_region == "ITS1_5.8S_ITS2"){ - ch_input_for_chim = assemble_its.out.itsnf - } else { - ch_input_for_chim = just_derep.out.nhc - } - - } - - CHIMERA_REMOVAL(ch_input_for_chim, ch_chimerabd) - - - - /* - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Data aggregation - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - */ - - // Pool sequences (for a final sequence table) - pool_seqs(CHIMERA_REMOVAL.out.filtered) - - // Tag-jump removal - if(params.tj == true){ - - // Pre-clustering prior to tag-jump removal - tj_preclust(pool_seqs.out.seqsnf) - - // Tag-jump removal - tj( - pool_seqs.out.seqtabnf, - tj_preclust.out.preclust_uc_parquet) - - ch_seqtab_after_tj = tj.out.seqtabtj - ch_tj_scores = tj.out.tjs - - } else { - - // Skip tag-jump removal - ch_seqtab_after_tj = pool_seqs.out.seqtabnf - ch_tj_scores = file("no_tj") - - } - - // Check optional channel with de novo chimera scores - ch_denovoscores = CHIMERA_REMOVAL.out.denovo_agg.ifEmpty(file('DeNovo_Chimera.txt')) - - // Create sequence table - prep_seqtab( - ch_seqtab_after_tj, // (optionally) tag-jump-filtered sequence table (long format) - pool_seqs.out.seqsnf, // Sequences in FASTA format - ch_denovoscores, // de novo chimera scores - seq_qual.out.quals // sequence qualities - ) - - - - /* - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Read count summary - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - */ - - // Initial data - Per-sample input channels - if( params.demultiplexed == false ){ - - if(params.seqplatform == "PacBio"){ - - // Input data and QC = single multiplexed file - ch_counts_1 = ch_input - ch_counts_2 = qc_se.out.filtered - - ch_all_demux = demux.out.samples_demux.flatten().collect() - } - - if(params.seqplatform == "Illumina"){ - ch_all_demux = demux_illumina.out.samples_demux.flatten().collect() - } - - } else { - - // Input data and QC = several demultiplexed files - ch_counts_1 = ch_input.flatten().collect() - ch_counts_2 = qc_se.out.filtered.flatten().collect() - - ch_all_demux = Channel.fromPath( params.input + '/*.{fastq.gz,fastq,fq.gz,fq}' ).flatten().collect() - } - - - // Primer-checked and multiprimer sequences - ch_all_primerchecked = primer_check.out.fq_primer_checked.flatten().collect().ifEmpty(file("no_primerchecked")) - ch_all_primerartefacts = primer_check.out.primerartefacts.flatten().collect().ifEmpty(file("no_multiprimer")) - - // ITSx and primer trimming channel - if(params.its_region == "full"){ - ch_all_trim = itsx.out.itsx_full.flatten().collect().ifEmpty(file("no_itsx")) - } - if(params.its_region == "ITS1"){ - if (params.ITSx_partial == 0) { - ch_all_trim = itsx.out.itsx_its1.flatten().collect().ifEmpty(file("no_itsx")) - } else { - ch_all_trim = itsx.out.itsx_its1_part.flatten().collect().ifEmpty(file("no_itsx")) - } - } - if(params.its_region == "ITS2"){ - if (params.ITSx_partial == 0) { - ch_all_trim = itsx.out.itsx_its2.flatten().collect().ifEmpty(file("no_itsx")) - } else { - ch_all_trim = itsx.out.itsx_its2_part.flatten().collect().ifEmpty(file("no_itsx")) - } - } - if(params.its_region == "SSU"){ - if (params.ITSx_partial == 0) { - ch_all_trim = itsx.out.itsx_ssu.flatten().collect().ifEmpty(file("no_itsx")) - } else { - ch_all_trim = itsx.out.itsx_ssu_part.flatten().collect().ifEmpty(file("no_itsx")) - } - } - if(params.its_region == "LSU"){ - if (params.ITSx_partial == 0) { - ch_all_trim = itsx.out.itsx_lsu.flatten().collect().ifEmpty(file("no_itsx")) - } else { - ch_all_trim = itsx.out.itsx_lsu_part.flatten().collect().ifEmpty(file("no_itsx")) - } - } - if(params.its_region == "ITS1_5.8S_ITS2"){ - ch_all_trim = assemble_its.out.itsnf.flatten().collect().ifEmpty(file("no_itsx")) - } - if(params.its_region == "none"){ - ch_all_trim = trim_primers.out.primertrimmed_fq.flatten().collect().ifEmpty(file("no_primertrim")) - } - - // Homopolymer-correction channel - if(params.hp == true){ - ch_homopolymers = homopolymer.out.uch.flatten().collect().ifEmpty(file("no_homopolymer")) - } else { - ch_homopolymers = file("no_homopolymer") - } - - // Chimeric channels - ch_chimref = CHIMERA_REMOVAL.out.chimeric.flatten().collect().ifEmpty(file("no_chimref")) - ch_chimdenovo = CHIMERA_REMOVAL.out.denovo_agg.flatten().collect().ifEmpty(file("no_chimdenovo")) - ch_chimrescued = CHIMERA_REMOVAL.out.rescued.flatten().collect().ifEmpty(file("no_chimrescued")) - - // Count reads and prepare summary stats for the run - // Currently, implemented only for PacBio - // For Illumina, need replace: - // `ch_input` -> `ch_inputR1` & `ch_inputR2` - // `qc_se` -> `qc_pe` - - if(params.seqplatform == "PacBio"){ - - read_counts( - ch_counts_1, // input data (single multiplexed file or several demultiplexed files) - ch_counts_2, // data that passed QC (single or several demuxed files) - ch_all_demux, // demultiplexed sequences per sample - ch_all_primerchecked, // primer-cheched sequences - ch_all_primerartefacts, // multiprimer artefacts - ch_all_trim, // ITSx-extracted or primer-trimmed sequences - ch_homopolymers, // Homopolymer stats - ch_chimref, // Reference-based chimeras - ch_chimdenovo, // De novo chimeras - ch_chimrescued, // Rescued chimeras - ch_tj_scores, // Tag-jump filtering scores - prep_seqtab.out.seq_pq // Final table with sequences (in Parquet format) - ) - - } // end of read_counts for PacBio - - - - // Dump the software versions to a file - ch_versions_yml = software_versions_to_yaml(Channel.topic('versions')) - .collectFile( - storeDir: "${params.tracedir}", - name: 'software_versions.yml', - sort: true, - newLine: true - ) - - // Dump the parameters to a file - ch_params_tsv = dumpParamsTsv() - .collectFile( - storeDir: "${params.tracedir}", - name: "pipeline_params.tsv", - sort: true, - newLine: true - ) - - // Document the analysis procedures - document_analysis_s1( - ch_versions_yml, - ch_params_tsv) - -} - - - - - - -// Quick workflow for demultiplexing and estimation of the number of reads per sample -// Only PacBio non-demultiplexed reads are supported -workflow seqstats { - - // Primer disambiguation - disambiguate() - - // Input file with barcodes (FASTA) - ch_barcodes = Channel.value(params.barcodes) - - // Input file with multiplexed reads (FASTQ.gz) - ch_input = Channel.value(params.input) - - // Initial QC - qc_se(ch_input) - - // Validate tags - tag_validation(ch_barcodes) - - // Tag-validation channels - ch_biosamples_sym = tag_validation.out.biosamples_sym.flatten().collect().ifEmpty(file("biosamples_sym")) - ch_biosamples_asym = tag_validation.out.biosamples_asym.flatten().collect().ifEmpty(file("biosamples_asym")) - ch_file_renaming = tag_validation.out.file_renaming.flatten().collect().ifEmpty(file("file_renaming")) - ch_unknown_combs = tag_validation.out.unknown_combinations.flatten().collect().ifEmpty(file("unknown_combinations")) - - // Demultiplexing - demux( - qc_se.out.filtered, - tag_validation.out.fasta, - ch_biosamples_sym, - ch_biosamples_asym, - ch_file_renaming, - ch_unknown_combs) - - // Check primers - primer_check( - demux.out.samples_demux.flatten(), - disambiguate.out.F, - disambiguate.out.R, - disambiguate.out.Fr, - disambiguate.out.Rr - ) - - // Prepare input channels - ch_all_demux = demux.out.samples_demux.flatten().collect() - ch_all_primerchecked = primer_check.out.fq_primer_checked.flatten().collect().ifEmpty(file("no_primerchecked")) - ch_all_primerartefacts = primer_check.out.primerartefacts.flatten().collect().ifEmpty(file("no_multiprimer")) - - // Count reads and prepare summary stats for the run - quick_stats( - ch_input, // input data - qc_se.out.filtered, // data that passed QC - ch_all_demux, // demultiplexed sequences per sample - ch_all_primerchecked, // primer-cheched sequences - ch_all_primerartefacts // primer artefacts - ) - -} // end of `seqstats` subworkflow - diff --git a/src/pipecraft-core/service_scripts/NextITS/workflows/STEP2.nf b/src/pipecraft-core/service_scripts/NextITS/workflows/STEP2.nf deleted file mode 100644 index 75ef3fa3..00000000 --- a/src/pipecraft-core/service_scripts/NextITS/workflows/STEP2.nf +++ /dev/null @@ -1,868 +0,0 @@ -/* -============================================================================ - NextITS: Pipeline to process eukaryotic ITS amplicons -============================================================================ - License: Apache-2.0 - Github : https://github.com/vmikk/NextITS - Website: https://Next-ITS.github.io/ ----------------------------------------------------------------------------- -*/ - -// ---- Step-2 workflow ---- - - -// Step-2 workflow: -// - Dereplicate sequences -// - (optionally) Denoize with UNOISE or DADA2 -// - (optionally) Cluster: -// * SWARM -// * VSEARCH -// - LULU (via MUMU implementation) -// - Prepare OTU table (wide, aggregate sequence abundance by ASV/OTU/Swarm cluster) - - - -// Enable DSL2 syntax -nextflow.enable.dsl = 2 - -include { software_versions_to_yaml } from '../modules/version_parser.nf' -include { CLUSTERING } from '../subworkflows/clustering_subworkflow.nf' -include { dumpParamsTsv } from '../modules/dump_parameters.nf' - -// Directory for storing pipeline information -out_tracedir = params.tracedir - - - - - - -// Aggregate sequences from all sequencing runs, remove de novo chimeras -process aggregate_sequences { - - label "main_container" - - // cpus 6 - - input: - path(inputs, stageAs: "?/*") - - output: - path "Seqs.fa.gz", emit: seqs - path "Seqs.parquet", emit: seqs_parquet - tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //" | cut -d" " -f1'), topic: versions - tuple val("${task.process}"), val('data.table'), eval('Rscript -e "cat(as.character(packageVersion(\'data.table\')))"'), topic: versions - tuple val("${task.process}"), val('arrow'), eval('Rscript -e "cat(as.character(packageVersion(\'arrow\')))"'), topic: versions - tuple val("${task.process}"), val('Biostrings'), eval('Rscript -e "cat(as.character(packageVersion(\'Biostrings\')))"'), topic: versions - - script: - """ - echo -e "Aggregating sequences\\n" - - aggregate_sequences.R \ - --seqtabs . \ - --maxchim ${params.max_ChimeraScore} \ - --recoverdenovo ${params.recover_denovochimeras} \ - --output Seqs \ - --threads ${task.cpus} - - """ -} - -// Pool and dereplicate sequences from all sequencing runs -process dereplication { - - label "main_container" - - publishDir "${params.outdir}/01.Dereplicated", mode: "${params.storagemode}" - // cpus 8 - - input: - path seqs - - output: - path "Dereplicated.fa.gz", emit: derep - path "Dereplicated.uc.gz", emit: derep_uc - tuple val("${task.process}"), val('vsearch'), eval('vsearch --version 2>&1 | head -n 1 | sed "s/vsearch //g" | sed "s/,.*//g" | sed "s/^v//" | sed "s/_.*//"'), topic: versions - - script: - def minlen = params.ampliconlen_min ? "--minseqlength ${params.ampliconlen_min}" : "" - def maxlen = params.ampliconlen_max ? "--maxseqlength ${params.ampliconlen_max}" : "" - - // Calculate optimal number of threads - def maxPigzThreads = 8 // Maximum threads per pigz instance - def totalCPUs = task.cpus - - // Try to maximize CPUs per pigz while ensuring full CPU utilization - def pigzCPUs = Math.min(maxPigzThreads, Math.ceil(Math.sqrt(totalCPUs * 2)).intValue()) - def parallelJobs = Math.max(1, Math.floor(totalCPUs / pigzCPUs).intValue()) - - // Recalculate pigzCPUs to use all available CPUs - pigzCPUs = Math.min(maxPigzThreads, Math.floor(totalCPUs / parallelJobs).intValue()) - - """ - echo -e "Dereplicating sequences\\n" - - vsearch \ - --derep_fulllength ${seqs} \ - --output Dereplicated.fa \ - --strand both \ - ${minlen} ${maxlen} \ - --fasta_width 0 \ - --threads 1 \ - --sizein --sizeout \ - --uc Dereplicated.uc - - echo -e "..Dereplication finished\\n" - - ## Compress results - echo -e "\\nCompressing results" - parallel -j ${parallelJobs} \ - "pigz -p ${pigzCPUs} -${params.gzip_compression} {}" \ - ::: "Dereplicated.uc" "Dereplicated.fa" - - """ -} - - -// Pool sequences from all sequencing runs, -// Dereplicate allowing query sequences to vary in length at 100% similarity (by default, 4% length variation allowed) -process dereplication_unite { - - label "main_container" - - publishDir "${params.outdir}/01.Dereplicated", mode: "${params.storagemode}" - // cpus 8 - - input: - path(inputs, stageAs: "?/*") - - output: - path "Dereplicated.fa.gz", emit: derep - path "Dereplicated.uc.gz", emit: derep_uc - - script: - """ - echo -e "Dereplicating sequences\\n" - - ## NB. by default, UNITE uses `cluster_fast`, which sorts sequences by length - ## Here, we use `cluster_size`, which sorts by abundance - - find . -name "*.fa.gz" | parallel -j1 \ - "zcat {}" \ - | sed '/^>/ s/;sample=.*;/;/' \ - | vsearch \ - --cluster_size - \ - --id 1 \ - --iddef 2 \ - --query_cov ${params.unite_querycov} \ - --target_cov ${params.unite_targetcov} \ - --strand both \ - --sizein --sizeout \ - --threads ${task.cpus} \ - --uc Dereplicated.uc \ - --centroids Dereplicated.fa - - echo -e "..Dereplication finished" - - ## Compress results - echo -e "\\nCompressing results" - parallel -j ${task.cpus} "gzip -${params.gzip_compression} {}" \ - ::: "Dereplicated.uc" "Dereplicated.fa" - - """ -} - - -// Fast pre-clustering of the dataset (to split into chunks prior processing) -process linclust { - - label "main_container" - - input: - path input - - output: - path "DB_clu.tsv", emit: db_clu - tuple val("${task.process}"), val('mmseqs'), eval('mmseqs version'), topic: versions - - script: - """ - - ## Create DB - echo -e "..DB creation\\n" - - mmseqs createdb \ - --dbtype 2 \ - --createdb-mode 0 \ - --shuffle 0 \ - ${input} \ - mmseqs_db - - - ## Run (cascaded) clustering - echo -e "..Lin-Clustering\\n" - - mmseqs linclust \ - mmseqs_db \ - linclusters_db \ - tmplc \ - --min-seq-id ${params.chunking_id} \ - --cluster-mode 0 \ - --similarity-type 2 \ - -c 0.7 --cov-mode 0 \ - -k 15 \ - --kmer-per-seq 100 \ - --kmer-per-seq-scale 0.3 \ - --spaced-kmer-mode 0 \ - --mask 0 \ - --split-memory-limit 100G \ - --remove-tmp-files 1 \ - --threads ${task.cpus} - - ## Generate a TSV-formatted output of clustering - echo -e "..Generating TSV-formatted output of clustering\\n" - - mmseqs createtsv \ - mmseqs_db mmseqs_db \ - linclusters_db \ - DB_clu.tsv \ - --threads ${task.cpus} - - """ -} - -// Bucketize sequences into clusters -process bucketize { - - label "main_container" - - input: - path sequences - path clusters - - output: - path "bucket_*.fa.gz", emit: buckets - tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //" | cut -d" " -f1'), topic: versions - tuple val("${task.process}"), val('data.table'), eval('Rscript -e "cat(as.character(packageVersion(\'data.table\')))"'), topic: versions - tuple val("${task.process}"), val('Biostrings'), eval('Rscript -e "cat(as.character(packageVersion(\'Biostrings\')))"'), topic: versions - - script: - numchunks = params.chunking_n ? "--numbuckets ${params.chunking_n}" : "" - """ - echo -e "..Bucketizing sequences\\n" - - bucketize_db.R \ - --db ${clusters} \ - --fasta ${sequences} \ - ${numchunks} \ - --summary bucket_summary.txt \ - --threads ${task.cpus} - - """ -} - - - - - -// Merge processed buckets (e.g., clustered sequences) -process merge_buckets { - - label "main_container" - - // Conditional publishing to match non-chunked directory structure - // (only enabled when chunking is used) - - // Pre-clustering results (if any) - publishDir "${params.outdir}/02.Homopolymer", - mode: "${params.storagemode}", - enabled: (params.chunking_n != null && params.chunking_n >= 2) && params.preclustering == "homopolymer", - pattern: "PreClustered.{fa,uc}.gz", - saveAs: { filename -> - switch(filename) { - case "PreClustered.fa.gz": return "HomopolymerCompressed.fa.gz" - case "PreClustered.uc.gz": return "HomopolymerCompressed.uc.gz" - default: return null - } - } - - publishDir "${params.outdir}/02.UNOISE", - mode: "${params.storagemode}", - enabled: (params.chunking_n != null && params.chunking_n >= 2) && params.preclustering == "unoise", - pattern: "PreClustered.{fa,uc}.gz", - saveAs: { filename -> - switch(filename) { - case "PreClustered.fa.gz": return "UNOISE.fa.gz" - case "PreClustered.uc.gz": return "UNOISE.uc.gz" - default: return null - } - } - - publishDir "${params.outdir}/02.DADA2", - mode: "${params.storagemode}", - enabled: (params.chunking_n != null && params.chunking_n >= 2) && params.preclustering == "dada2", - pattern: "PreClustered.{fa,uc}.gz", - saveAs: { filename -> - switch(filename) { - case "PreClustered.fa.gz": return "DADA2_denoised.fa.gz" - case "PreClustered.uc.gz": return "DADA2_denoised.uc.gz" - default: return null - } - } - - publishDir "${params.outdir}/02.Preclustered_SWARM_d1", - mode: "${params.storagemode}", - enabled: (params.chunking_n != null && params.chunking_n >= 2) && params.preclustering == "swarm_d1", - pattern: "PreClustered.{fa,uc}.gz", - saveAs: { filename -> - switch(filename) { - case "PreClustered.fa.gz": return "SWARM.fa.gz" - case "PreClustered.uc.gz": return "SWARM.uc.gz" - default: return null - } - } - - // Final clustering results - publish to clustering directory if clustering != "none" - publishDir "${params.outdir}/03.Clustered_VSEARCH", - mode: "${params.storagemode}", - enabled: (params.chunking_n != null && params.chunking_n >= 2) && params.clustering == "vsearch", - pattern: "Clustered.{fa,uc}.gz" - // No saveAs needed - files already have correct names for VSEARCH - - publishDir "${params.outdir}/03.Clustered_SWARM", - mode: "${params.storagemode}", - enabled: (params.chunking_n != null && params.chunking_n >= 2) && params.clustering == "swarm", - pattern: "Clustered.{fa,uc}.gz", - saveAs: { filename -> - switch(filename) { - case "Clustered.fa.gz": return "SWARM_representatives.fa.gz" - case "Clustered.uc.gz": return "SWARM.uc.gz" - default: return null - } - } - - - // Since there are name collisions, we need to stage files with unique names - input: - path(preclustuc_chunks, stageAs: "pre/?/*") // UC files for pre-clustering (optional) - path(preclustaf_chunks, stageAs: "pre/?/*") // FASTA files for pre-clustering (optional) - path(cluster_chunks, stageAs: "cls/?/*") // Sequence representatives - path(clustuc_chunks, stageAs: "ucs/?/*") // UC files for clustering - - output: - path "PreClustered.uc.gz", emit: preclustuc_ch, optional: true - path "PreClustered.fa.gz", emit: preclustaf_ch, optional: true - path "Clustered.fa.gz", emit: cluster_ch - path "Clustered.uc.gz", emit: clustuc_ch - - - script: - """ - echo -e "Merging buckets\\n" - - ## Pool sequence representatives - echo -e "..Pooling sequence representatives\\n" - find cls -name "*.fa.gz" \ - | parallel -j 1 "cat {}" \ - > Clustered.fa.gz - - ## Pool UC files - echo -e "..Pooling UC files\\n" - find ucs -name "*.uc.gz" \ - | parallel -j 1 "cat {}" \ - > Clustered.uc.gz - - ## Check if pre-clustering was performed - if [[ -e pre/1/NoPrecluster || -L "pre/1/NoPrecluster" ]]; then - echo -e "..Pre-clustering was not performed. Skipping pooling these data\\n" - else - echo -e "..Pre-clustering was performed\\n" - - echo -e "..Pooling pre-clustered UC files\\n" - find pre -name "*.uc.gz" \ - | parallel -j 1 "cat {}" \ - > PreClustered.uc.gz - - echo -e "..Pooling pre-clustered FASTA files\\n" - find pre -name "*.fa.gz" \ - | parallel -j 1 "cat {}" \ - | vsearch \ - --sortbysize - \ - --sizein --sizeout \ - --threads 1 \ - --fasta_width 0 \ - --output - \ - | pigz -p ${task.cpus} -${params.gzip_compression} \ - > PreClustered.fa.gz - fi - - echo -e "..Done\\n" - """ -} - - - -// Merge UC files -process merge_uc { - - label "main_container" - - publishDir "${params.outdir}/04.PooledResults", mode: "${params.storagemode}" - // cpus 4 - - input: - path(uc_derep) - path(uc_preclust) - path(uc_clust) - - output: - path "UC_Pooled.parquet", emit: uc - tuple val("${task.process}"), val('ucs'), eval('ucs --version | sed "s/ucs //"'), topic: versions - tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //" | cut -d" " -f1'), topic: versions - tuple val("${task.process}"), val('duckdb'), eval('Rscript -e "cat(as.character(packageVersion(\'duckdb\')))"'), topic: versions - - script: - """ - echo -e "Merging UC files\\n" - - ## Parse UC files from different steps, convert to parquet format - echo -e "..Parsing dereplicated UC file\\n" - ucs --input ${uc_derep} --output UC_derep.parquet - - if [ -f ${uc_preclust} ] && [ "${uc_preclust}" != "NoPrecluster" ]; then - echo -e "..Parsing pre-clustered UC file\\n" - ucs --input ${uc_preclust} --output UC_preclust.parquet - UCPRECLUST="UC_preclust.parquet" - else - UCPRECLUST="NoPrecluster" - fi - - if [ -f ${uc_clust} ]; then - echo -e "..Parsing clustered UC file\\n" - ucs --input ${uc_clust} --output UC_clust.parquet - fi - - ## Merge UC files into a single file - echo -e "..Merging UC files\\n" - merge_uc_files.R \ - --ucderep UC_derep.parquet \ - --ucpreclust \${UCPRECLUST} \ - --ucclust UC_clust.parquet \ - --output UC_Pooled.parquet - - """ -} - - -// Summarize sequence abundance by OTU -process summarize { - - label "main_container" - - publishDir "${params.outdir}/04.PooledResults", mode: "${params.storagemode}" - // cpus 4 - - input: - path(seqtab) - path(uc_parquet) - path(otus_fasta) - - output: - path "OTU_table_wide.txt.gz", emit: otutabwide - path "OTU_table_long.txt.gz", emit: otutablong - path "OTU_table_wide.RData", emit: otutabwider - path "OTU_table_long.RData", emit: otutablongr - path "OTUs.fa.gz", emit: seqs - tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //" | cut -d" " -f1'), topic: versions - tuple val("${task.process}"), val('data.table'), eval('Rscript -e "cat(as.character(packageVersion(\'data.table\')))"'), topic: versions - tuple val("${task.process}"), val('arrow'), eval('Rscript -e "cat(as.character(packageVersion(\'arrow\')))"'), topic: versions - tuple val("${task.process}"), val('Biostrings'), eval('Rscript -e "cat(as.character(packageVersion(\'Biostrings\')))"'), topic: versions - - script: - """ - echo -e "Summarizing clustered data\\n" - - summarize_clustered_data.R \ - --seqtab ${seqtab} \ - --uc ${uc_parquet} \ - --otus ${otus_fasta} \ - --maxmeep ${params.max_MEEP} \ - --recoversinglet ${params.recover_lowqsingletons} \ - --mergesamples ${params.merge_replicates} \ - --threads ${task.cpus} - - """ -} - - -// Summarize dereplicated data -process summarize_dereplicated_data { - - label "main_container" - publishDir "${params.outdir}/04.PooledResults", mode: "${params.storagemode}" - // cpus 4 - - input: - path(seqtab) // Sequence tables in long format, parquet - path(uc_derep) // UC file from dereplication - path(fasta) // FASTA file with sequences - - output: - path "UC_Pooled.parquet", emit: uc - path "OTU_table_wide.txt.gz", emit: otutabwide - path "OTU_table_long.txt.gz", emit: otutablong - path "OTU_table_wide.RData", emit: otutabwider - path "OTU_table_long.RData", emit: otutablongr - path "OTUs.fa.gz", emit: seqs - tuple val("${task.process}"), val('ucs'), eval('ucs --version | sed "s/ucs //"'), topic: versions - tuple val("${task.process}"), val('R'), eval('Rscript -e "cat(R.version.string)" | sed "s/R version //" | cut -d" " -f1'), topic: versions - tuple val("${task.process}"), val('data.table'), eval('Rscript -e "cat(as.character(packageVersion(\'data.table\')))"'), topic: versions - tuple val("${task.process}"), val('arrow'), eval('Rscript -e "cat(as.character(packageVersion(\'arrow\')))"'), topic: versions - tuple val("${task.process}"), val('Biostrings'), eval('Rscript -e "cat(as.character(packageVersion(\'Biostrings\')))"'), topic: versions - - script: - """ - echo -e "Summarizing clustered data\\n" - - ## Parse UC file from dereplication - echo -e "..Parsing dereplicated UC file" - ucs --input ${uc_derep} --output UC_Pooled.parquet - - ## Summarize sequence abundance by OTU and sample - echo -e "\\n..Summarizing sequence abundance by OTU and sample\\n" - summarize_dereplicated_data.R \ - --seqtab ${seqtab} \ - --uc UC_Pooled.parquet \ - --seqs ${fasta} \ - --maxmeep ${params.max_MEEP} \ - --recoversinglet ${params.recover_lowqsingletons} \ - --mergesamples ${params.merge_replicates} \ - --threads ${task.cpus} - - """ -} - - -// Post-clustering curation -process lulu { - - label "main_container" - - publishDir "${params.outdir}/05.LULU", mode: "${params.storagemode}" - // cpus 8 - - input: - path otu_table - path sequences - - output: - path "OTU_table_LULU.txt.gz", emit: lulu - path "LULU_match_list.txt.gz", emit: matches - path "LULU_merging_statistics.txt.gz", emit: stats - path "OTUs_LULU.fa.gz", emit: fasta - tuple val("${task.process}"), val('mumu'), eval('mumu --version | head -n 1 | sed "s/mumu //"'), topic: versions - tuple val("${task.process}"), val('vsearch'), eval('vsearch --version 2>&1 | head -n 1 | sed "s/vsearch //g" | sed "s/,.*//g" | sed "s/^v//" | sed "s/_.*//"'), topic: versions - tuple val("${task.process}"), val('ripgrep'), eval('rg --version | head -1 | sed "s/ripgrep //"'), topic: versions - - script: - """ - echo -e "Post-clustering curation with MUMU (C++ implementation of LULU)\\n" - - ## If Clustered.fa.gz used as input - ## (but there are sequences excluded from the OTU table) - # echo -e "Removing size annotations from sequence headers" - # zcat ${sequences} \ - # | sed -r '/^>/ s/;size=[0-9]+//g' \ - # | gzip -${params.gzip_compression} > tmp_sequences.fa.gz - - - ## MUMU similarity threshold is specified as % (e.g., 84.0) - ## while VSEARCH requires a value in 0-1 range (e.g., 0.84) - - ## With bc - # VSID=\$(echo "scale=4; x = ${params.lulu_match} / 100; if(x<1) print 0; x" | bc) - - ## With awk - VSID=\$(awk -v a=${params.lulu_match} 'BEGIN { print(a/100) }') - - echo -e "VSEARCH similarity threshold: " "\$VSID" - - ## Prepare match list (+ remove size annotations) - echo -e "Preparing match list\\n" - vsearch \ - --usearch_global ${sequences} \ - --db ${sequences} \ - --self \ - --id "\$VSID" \ - --iddef 1 \ - --gapopen ${params.vsearch_gapopen} \ - --gapext ${params.vsearch_gapext } \ - --query_cov 0.9 \ - --userfields query+target+id \ - --maxaccepts 0 \ - --maxhits ${params.lulu_maxhits} \ - --threads ${task.cpus} \ - --userout LULU_match_list.txt - - # Input otu_table = tab-separated, samples in columns - # Input match_list = tab-separated, OTU pairwise similarity scores - - - echo -e "\\nUnpacking OTU table\\n" - gunzip --stdout ${otu_table} > tmp_OTU_table.txt - - echo -e "\\nRunning MUMU\\n" - mumu \ - --otu_table tmp_OTU_table.txt \ - --match_list LULU_match_list.txt \ - --new_otu_table OTU_table_LULU.txt \ - --log LULU_merging_statistics.txt \ - --threads ${task.cpus} \ - --minimum_match ${params.lulu_match} \ - --minimum_ratio ${params.lulu_ratio} \ - --minimum_ratio_type ${params.lulu_ratiotype} \ - --minimum_relative_cooccurence ${params.lulu_relcooc} - - echo -e "..Compressing LULU-curated OTU table\\n" - parallel -j 1 \ - "pigz -p ${task.cpus} -${params.gzip_compression} {}" \ - ::: "OTU_table_LULU.txt" "LULU_merging_statistics.txt" "LULU_match_list.txt" - - echo -e "..LULU done\\n" - - echo -e "\\nPreparing sequence subset\\n" - - echo -e "..Extracting OTU IDs\\n" - zcat OTU_table_LULU.txt.gz \ - | awk 'NR > 1 {print \$1}' \ - > curated_OTU_ids.txt - - echo -e "..Extracting sequences\\n" - rg -z -A 1 \ - -f curated_OTU_ids.txt \ - --context-separator "" \ - --threads ${task.cpus} \ - ${sequences} \ - | sed '/^\$/d' \ - | gzip -${params.gzip_compression} \ - > OTUs_LULU.fa.gz - - ## Remove temporary files - echo -e "\\nAll done!\\n" - echo -e "Removing temporary files\\n" - # rm tmp_sequences.fa.gz - rm tmp_OTU_table.txt curated_OTU_ids.txt - - """ -} - -// LULU merging statistics format: -// 1. `query_otu_name` - name of query OTU -// 2. `parent_otu_name` - name of potential parent OTU -// 3. `similarity_pct` - percentage of similarity (0 to 100) -// 4. `query_total_abundance` - total abundance of the query OTU (sum through all samples) -// 5. `parent_total_abundance` - total abundance of the potential parent OTU (sum through all samples) -// 6. `query_overlap_abundance` - overlap abundance of the query OTU (sum through all samples where the potential parent OTU is also present) -// 7. `parent_overlap_abundance` - overlap abundance of the potential parent OTU (sum through all samples where the query OTU is also present) -// 8. `query_incidence` - incidence of the query OTU (number of samples where the query OTU is present) -// 9. `parent_incidence` - incidence of the potential parent OTU (number of samples where the potential parent OTU is present) -// 10. `both_incidence` - incidence of the potential parent OTU (number of samples where both the potential parent OTU and the query OTU are present) -// 11. `smallest_abundance_ratio` - smallest abundance ratio (for each sample, compute the abundance of the potential parent OTU divided by the abundance of the query OTU) -// 12. `sum_abundance_ratios` - sum of the abundance ratios -// 13. `avg_abundance_ratio` - average value of abundance ratios -// 14. `smallest_nonnull_ratio` - smallest non-null abundance ratio (exclude ratios for samples where the query OTU is present but not the potential parent OTU) -// 15. `avg_nonnull_ratio` - average value of non-null abundance ratios (exclude ratios for samples where the query OTU is present but not the potential parent OTU) -// 16. `largest_ratio` - largest ratio value -// 17. `relative_cooccurrence` - relative co-occurence value (number of samples where both the potential parent OTU and the query OTU are present divided by the number of samples where the query OTU is present) -// 18. `status` - status: 'accepted' or 'rejected' -// The potential parent OTU is either accepted as a parent, or rejected - -// Auto documentation of analysis procedures -// (generate narrative description of methods) -process document_analysis_s2 { - - label "main_container" - - publishDir "${out_tracedir}", mode: 'copy', overwrite: true - // cpus 1 - - input: - path versions // "software_versions.yml" - path params // "pipeline_params.tsv" - - output: - path "README_Step2_Methods.txt", emit: docs - - - script: - """ - echo -e "Descriptive summary generation\n" - - document_s2.R \ - ${versions} \ - ${params} \ - README_Step2_Methods.txt - - """ -} - - - - -// Step-2 workflow -workflow S2 { - - // Find quality-filtered sequence tables - ch_seqtabs = Channel.fromPath( - params.data_path + "/**/07_SeqTable/Seqs.parquet", - checkIfExists: true).collect() - - // Aggregate sequences, remove de novo chimeras - aggregate_sequences(ch_seqtabs) - - // Pool and dereplicate all sequences - dereplication(aggregate_sequences.out.seqs) - derep_ch = dereplication.out.derep - derepuc_ch = dereplication.out.derep_uc - - - // // Pool and dereplicate all sequences - // if(params.alignment_penalties == "UNITE"){ - // // Clustering-based dereplication, allowing for a slight length variation of sequences - // dereplication_unite(ch_seqs) - // derep_ch = dereplication_unite.out.derep - // derepuc_ch = dereplication_unite.out.derep_uc - // } - // if(params.alignment_penalties == "default"){ - // // Fast, hash-based dereiplication - // dereplication(ch_seqs) - // derep_ch = dereplication.out.derep - // derepuc_ch = dereplication.out.derep_uc - // } - // - // NB. In case with large number of sequences, UNITE-style dereplication is extremly slow. - // Probably, it is possible to improve the speed, by using two steps: - // hash-based dereplication first, then additional round of clustering-based derep. - // But it would add extra complexity to manage and combine two UC files. - - // Prepare sequence table based on dereplicated sequences - // (no clustering, pre-clustering, or denoising) - if(params.preclustering == "none" & params.clustering == "none"){ - - summarize_dereplicated_data( - aggregate_sequences.out.seqs_parquet, - derepuc_ch, - derep_ch - ) - - } else { - - /* - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Clustering / pre-clustering / denoising with optional chunking - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - */ - - // No chunking (process all sequences at once) - if(params.chunking_n == null || params.chunking_n < 2){ - - CLUSTERING(derep_ch) - - preclustuc_ch = CLUSTERING.out.preclustuc_ch - cluster_ch = CLUSTERING.out.cluster_ch - clustuc_ch = CLUSTERING.out.clustuc_ch - - } else { - // Chunking (process sequences in N chunks) - - // Groupd sequences into clusters - linclust(derep_ch) - - // Bucketize sequence clusters into chunks - bucketize(derep_ch, linclust.out.db_clu) - buckets_ch = bucketize.out.buckets.flatten() - - // Run clustering/pre-clustering/denoising subworkflow - CLUSTERING(buckets_ch) - - // collect UC and FASTA files from all chunks - preclustuc_chunks = CLUSTERING.out.preclustuc_ch.collect() - preclustaf_chunks = CLUSTERING.out.preclustaf_ch.collect() - cluster_chunks = CLUSTERING.out.cluster_ch.collect() - clustuc_chunks = CLUSTERING.out.clustuc_ch.collect() - - // Merge buckets into a single file - merge_buckets( - preclustuc_chunks, - preclustaf_chunks, - cluster_chunks, - clustuc_chunks) - - cluster_ch = merge_buckets.out.cluster_ch - clustuc_ch = merge_buckets.out.clustuc_ch - preclustuc_ch = merge_buckets.out.preclustuc_ch.ifEmpty(file('NoPrecluster')) - - } - - - /* - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Result processing - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - */ - - - // Pool UC files - merge_uc( - derepuc_ch, // UC file with dereplication info - preclustuc_ch, // UC file with pre-clustering or denoising (optional) - clustuc_ch // UC file with OTU clustering info - ) - - // Summarize sequence abundances by OTU and sample - summarize( - aggregate_sequences.out.seqs_parquet, // Step-1 sequnece tables in long format with de novo chimeras removed - merge_uc.out.uc, // Combined UC files with sequence membership info - cluster_ch // FASTA file with OTUs - ) - - // Post-clustering curation with LULU - if ( params.lulu == true ) { - lulu( - summarize.out.otutabwide, - summarize.out.seqs - // cluster_ch // In the Clustered.fa.gz, there are seqs excluded from OTU table - ) - } - - } // end of preclustering == "none" & clustering == "none" - - - // Run statistics - // run_summary() - - // Dump the software versions to a file - ch_versions_yml = software_versions_to_yaml(Channel.topic('versions')) - .collectFile( - storeDir: "${params.tracedir}", - name: 'software_versions.yml', - sort: true, - newLine: true - ) - - // Dump the parameters to a file - ch_params_tsv = dumpParamsTsv() - .collectFile( - storeDir: "${params.tracedir}", - name: "pipeline_params.tsv", - sort: true, - newLine: true - ) - - // Auto documentation of analysis procedures - document_analysis_s2( - ch_versions_yml, - ch_params_tsv) - -} From dc9c0021745a4c03c88cce5bfcd1548a77d503eb Mon Sep 17 00:00:00 2001 From: Vladimir Mikryukov Date: Tue, 17 Feb 2026 16:27:02 +0200 Subject: [PATCH 2/5] upd Dockerfile for nextits --- image_development/Dockerfile_nextits_2 | 31 +++++++++++++++++++++----- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/image_development/Dockerfile_nextits_2 b/image_development/Dockerfile_nextits_2 index 9caddcc2..ce287ca2 100644 --- a/image_development/Dockerfile_nextits_2 +++ b/image_development/Dockerfile_nextits_2 @@ -1,10 +1,29 @@ +ARG NEXTITS_VERSION=1.1.0 +ARG NEXTFLOW_VERSION=25.10.4 -# Stage 1 (Nextflow - minimal stage) -FROM nextflow/nextflow:25.10.2 AS nextflow +FROM vmikk/nextits:${NEXTITS_VERSION} AS main -## Build stage 3 - Main -FROM vmikk/nextits:1.1.0 AS main +## Re-declare build args inside this stage so RUN can use them +ARG NEXTITS_VERSION +ARG NEXTFLOW_VERSION -# Copy Nextflow binary from the Nextflow stage -COPY --from=nextflow /usr/local/bin/nextflow /usr/local/bin/nextflow +ENV NEXTITS_DIR=/opt/pipelines/NextITS +ENV NXF_HOME=/opt/software/conda/bin +# ENV PATH="${NXF_HOME}:${PATH}" + +USER root + +RUN set -eux; \ + mkdir -p "${NXF_HOME}" "${NEXTITS_DIR}"; \ + ## Install Nextflow into $NXF_HOME + NXF_VER="${NEXTFLOW_VERSION}" curl -s https://get.nextflow.io | bash ; \ + mv nextflow "${NXF_HOME}/nextflow"; \ + chmod 0755 "${NXF_HOME}/nextflow"; \ + ## Pull NextITS into $NEXTITS_DIR + git clone --depth 1 --branch "${NEXTITS_VERSION}" https://github.com/vmikk/NextITS "${NEXTITS_DIR}" ; \ + rm -rf "${NEXTITS_DIR}/.git" ; \ + ## Ensure the runtime user can access everything + chown -R nextits:nextits "${NXF_HOME}" "${NEXTITS_DIR}" + +USER nextits From ad43ed6991dbb9bf2179c6c4ef787328a11e55a5 Mon Sep 17 00:00:00 2001 From: Vladimir Mikryukov Date: Tue, 17 Feb 2026 16:27:51 +0200 Subject: [PATCH 3/5] fix nexits sh wrapper --- .../service_scripts/NextITS_Pipeline.sh | 30 ++----------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/src/pipecraft-core/service_scripts/NextITS_Pipeline.sh b/src/pipecraft-core/service_scripts/NextITS_Pipeline.sh index 8302cce0..83a3c767 100755 --- a/src/pipecraft-core/service_scripts/NextITS_Pipeline.sh +++ b/src/pipecraft-core/service_scripts/NextITS_Pipeline.sh @@ -1,38 +1,13 @@ #!/bin/bash -export NXF_HOME="/Input/.nextflow" +export NXF_HOME="/opt/software/conda/bin" export NXF_ANSI_LOG="false" export NXF_LOG_COLOR="false" export NXF_ANSI="false" export TERM="dumb" -mkdir -p $NXF_HOME BASEDIR=$(pwd) -fix_permissions() { - # Try different possible locations for the NextITS scripts - for dir in \ - "/scripts/NextITS/bin" \ - "$NXF_HOME/assets/vmikk/NextITS/bin" \ - "/Input/.nextflow/assets/vmikk/NextITS/bin" \ - "$HOME/.nextflow/assets/vmikk/NextITS/bin" \ - "./work/*/vmikk/NextITS/bin" - do - if [ -d "$dir" ]; then - echo "Setting permissions for scripts in $dir" - find "$dir" -name "*.R" -exec chmod +x {} \; 2>/dev/null - find "$dir" -name "*.py" -exec chmod +x {} \; 2>/dev/null - find "$dir" -name "*.sh" -exec chmod +x {} \; 2>/dev/null - # Also fix line endings in case they're causing issues - find "$dir" -name "*.R" -exec sed -i 's/\r$//' {} \; 2>/dev/null - find "$dir" -name "*.py" -exec sed -i 's/\r$//' {} \; 2>/dev/null - find "$dir" -name "*.sh" -exec sed -i 's/\r$//' {} \; 2>/dev/null - fi - done -} - -fix_permissions - ls -la ## Run Step-1 for all sequencing runs @@ -51,7 +26,8 @@ find /Input/ -mindepth 1 -maxdepth 1 -type d \ ## Step-2 - standard VSEARCH clustering -stdbuf -oL -eL nextflow run /scripts/NextITS \ +stdbuf -oL -eL \ + nextflow run /opt/pipelines/NextITS/main.nf \ -resume \ --storagemode "copy" \ -params-file /scripts/NextFlowConfig.json \ From 7f97da9b116b8daacf54fc4aa70085620ffb8e2b Mon Sep 17 00:00:00 2001 From: Vladimir Mikryukov Date: Tue, 17 Feb 2026 16:29:24 +0200 Subject: [PATCH 4/5] nextits s1 - fix path to the pipeline --- src/pipecraft-core/service_scripts/submodules/NextITS_Step1.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipecraft-core/service_scripts/submodules/NextITS_Step1.sh b/src/pipecraft-core/service_scripts/submodules/NextITS_Step1.sh index 26ea9932..31ba6b9b 100755 --- a/src/pipecraft-core/service_scripts/submodules/NextITS_Step1.sh +++ b/src/pipecraft-core/service_scripts/submodules/NextITS_Step1.sh @@ -38,7 +38,7 @@ ## Step-1 - with pre-demultiplexed data -run_nextflow run /scripts/NextITS \ +run_nextflow run /opt/pipelines/NextITS/main.nf \ -resume \ --step "Step1" \ --storagemode "copy" \ From e91377fce9de52536e2902ca333cd8f5767cac63 Mon Sep 17 00:00:00 2001 From: Vladimir Mikryukov Date: Tue, 17 Feb 2026 16:36:23 +0200 Subject: [PATCH 5/5] nextits docker - add nf-schema plugin --- image_development/Dockerfile_nextits_2 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/image_development/Dockerfile_nextits_2 b/image_development/Dockerfile_nextits_2 index ce287ca2..bd25ae4e 100644 --- a/image_development/Dockerfile_nextits_2 +++ b/image_development/Dockerfile_nextits_2 @@ -19,6 +19,8 @@ RUN set -eux; \ NXF_VER="${NEXTFLOW_VERSION}" curl -s https://get.nextflow.io | bash ; \ mv nextflow "${NXF_HOME}/nextflow"; \ chmod 0755 "${NXF_HOME}/nextflow"; \ + "${NXF_HOME}"/nextflow plugin install nf-schema@2.4.1; \ + rm -rf ./.nextflow; \ ## Pull NextITS into $NEXTITS_DIR git clone --depth 1 --branch "${NEXTITS_VERSION}" https://github.com/vmikk/NextITS "${NEXTITS_DIR}" ; \ rm -rf "${NEXTITS_DIR}/.git" ; \