From 21a487e21ed87fa65680a3215723917600f051c2 Mon Sep 17 00:00:00 2001 From: miguelrosell Date: Wed, 18 Feb 2026 21:27:17 +0100 Subject: [PATCH 1/9] feat: integrated manifold learning subworkflow and CLI scripts --- bin/run_causality.py | 56 ++++++++++++++++++ bin/run_diffmap.py | 63 ++++++++++++++++++++ bin/run_phate.py | 81 ++++++++++++++++++++++++++ bin/run_topology.py | 90 +++++++++++++++++++++++++++++ conf/modules.config | 40 +++++++++++++ nextflow.config | 7 +++ workflows/scdownstream.nf | 118 +++++++++++++++++++++----------------- 7 files changed, 402 insertions(+), 53 deletions(-) create mode 100755 bin/run_causality.py create mode 100755 bin/run_diffmap.py create mode 100755 bin/run_phate.py create mode 100755 bin/run_topology.py diff --git a/bin/run_causality.py b/bin/run_causality.py new file mode 100755 index 00000000..e1d55323 --- /dev/null +++ b/bin/run_causality.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python + +import argparse +import sys +import scanpy as sc +import numpy as np +import pandas as pd + +def parse_args(): + parser = argparse.ArgumentParser(description='Run Causal Inference on Manifold.') + parser.add_argument('--input', required=True, help='Input AnnData file (.h5ad)') + parser.add_argument('--output', required=True, help='Output AnnData file (.h5ad)') + return parser.parse_args() + +def main(): + args = parse_args() + + print(f"Reading input from {args.input}...") + try: + adata = sc.read_h5ad(args.input) + except Exception as e: + sys.exit(f"Error loading AnnData: {e}") + + # Logic: We compute Gene Rank based on centrality in the manifold graph + # This simulates finding "driver genes" + print("Computing causal graph metrics...") + + if 'connectivities' not in adata.obsp: + print("Computing neighbors first...") + sc.pp.neighbors(adata, use_rep='X_pca') + + # Compute PAGA (Partition-based Graph Abstraction) as a proxy for causal trajectory + # This requires clusters. If no clusters, then we cluster first. + if 'leiden' not in adata.obs: + print("Clustering (Leiden) needed for causality inference...") + sc.tl.leiden(adata) + + print("Running PAGA for trajectory inference...") + sc.tl.paga(adata, groups='leiden') + + # Store "causal" results (Pseudotime / PAGA connectivity) + adata.uns['causal_inference'] = { + 'method': 'PAGA_Trajectory', + 'connectivities': adata.uns['paga']['connectivities_tree'] + } + + print(f"Saving results to {args.output}...") + adata.write_h5ad(args.output) + + # Versions + with open("versions.yml", "w") as f: + f.write('process:\n') + f.write(f' scanpy: "{sc.__version__}"\n') + +if __name__ == "__main__": + main() diff --git a/bin/run_diffmap.py b/bin/run_diffmap.py new file mode 100755 index 00000000..efaee492 --- /dev/null +++ b/bin/run_diffmap.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python + +import argparse +import sys +import scanpy as sc +import pandas as pd +import numpy as np + +def parse_args(): + parser = argparse.ArgumentParser(description='Run Diffusion Maps (Diffmap) on an AnnData object.') + parser.add_argument('--input', required=True, help='Input AnnData file (.h5ad)') + parser.add_argument('--output', required=True, help='Output AnnData file (.h5ad)') + parser.add_argument('--n_neighbors', type=int, default=15, help='Number of nearest neighbors for graph construction') + parser.add_argument('--n_comps', type=int, default=15, help='Number of diffusion components to compute') + return parser.parse_args() + +def main(): + args = parse_args() + + # 1. Load data + print(f"Reading input from {args.input}...") + try: + adata = sc.read_h5ad(args.input) + except Exception as e: + sys.exit(f"Error loading AnnData: {e}") + + # 2. Validation + if 'X_pca' not in adata.obsm: + sys.exit("Error: 'X_pca' not found in adata.obsm. Diffmap requires pre-computed PCA coordinates.") + + # 3. Compute Neighbors + # Diffmap requires a neighborhood graph. We compute it on X_pca. + print(f"Computing neighbors graph (k={args.n_neighbors})...") + sc.pp.neighbors(adata, n_neighbors=args.n_neighbors, use_rep='X_pca') + + # 4. Run diffusion maps + print(f"Running Diffusion Maps with n_comps={args.n_comps}...") + try: + sc.tl.diffmap(adata, n_comps=args.n_comps) + except Exception as e: + sys.exit(f"Error running diffmap: {e}") + + # The result is stored in adata.obsm['X_diffmap'] automatically by scanpy + + # 5. Save output + print(f"Saving results to {args.output}...") + try: + adata.write_h5ad(args.output) + except Exception as e: + sys.exit(f"Error saving AnnData: {e}") + + # 6. Generate versions.yml + import scanpy as s_lib + + with open("versions.yml", "w") as f: + f.write('process:\n') + f.write(f' scanpy: "{s_lib.__version__}"\n') + f.write(f' numpy: "{np.__version__}"\n') + + print("Diffmap computation completed successfully.") + +if __name__ == "__main__": + main() diff --git a/bin/run_phate.py b/bin/run_phate.py new file mode 100755 index 00000000..d3efbce1 --- /dev/null +++ b/bin/run_phate.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python + +import argparse +import sys +import scanpy as sc +import phate +import pandas as pd +import numpy as np + +def parse_args(): + parser = argparse.ArgumentParser(description='Run PHATE on an AnnData object.') + parser.add_argument('--input', required=True, help='Input AnnData file (.h5ad)') + parser.add_argument('--output', required=True, help='Output AnnData file (.h5ad)') + parser.add_argument('--k', type=int, default=5, help='Number of nearest neighbors') + parser.add_argument('--a', type=float, default=40, help='Alpha decay parameter') + parser.add_argument('--t', type=str, default='auto', help='Diffusion time (int or "auto")') + parser.add_argument('--n_jobs', type=int, default=1, help='Number of threads') + parser.add_argument('--gamma', type=float, default=1, help='Informational distance parameter') + return parser.parse_args() + +def main(): + args = parse_args() + + # 1. Load data + print(f"Reading input from {args.input}...") + try: + adata = sc.read_h5ad(args.input) + except Exception as e: + sys.exit(f"Error loading AnnData: {e}") + + # 2. Validation: Making sure that PCA exists (nf-core/scdownstream standard) + if 'X_pca' not in adata.obsm: + sys.exit("Error: 'X_pca' not found in adata.obsm. PHATE requires pre-computed PCA coordinates.") + + # 3. Prepare parameters + # PHATE requires specific type handling for "auto" + t_param = 'auto' if args.t == 'auto' else int(args.t) + + print(f"Running PHATE with k={args.k}, a={args.a}, t={t_param} on X_pca...") + + # 4. Run PHATE + # We initialize the PHATE operator explicitly + phate_op = phate.PHATE( + n_pca=None, # PCA is already computed + knn=args.k, + decay=args.a, + t=t_param, + gamma=args.gamma, + n_jobs=args.n_jobs, + verbose=1, + random_state=42 # Ensure reproducibility + ) + + # Fit and transform the PCA data + # Note: We pass X_pca directly to avoid recomputation + X_phate = phate_op.fit_transform(adata.obsm['X_pca']) + + # 5. Store results + # Save coordinates in the standard Scanpy slot + adata.obsm['X_phate'] = X_phate + + # Save metadata for reproducibility and downstream usage (e.g. TDA) + adata.uns['phate_params'] = { + 'k': args.k, + 'a': args.a, + 't': t_param, + 'gamma': args.gamma, + 'diff_potential': phate_op.diff_potential # Crucial for Potential Distance + } + + # 6. Save output + print(f"Saving results to {args.output}...") + try: + adata.write_h5ad(args.output) + except Exception as e: + sys.exit(f"Error saving AnnData: {e}") + + print("PHATE computation completed successfully.") + +if __name__ == "__main__": + main() diff --git a/bin/run_topology.py b/bin/run_topology.py new file mode 100755 index 00000000..ff5dabea --- /dev/null +++ b/bin/run_topology.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python + +import argparse +import sys +import scanpy as sc +import numpy as np + +# Secure import for ripser +try: + from ripser import ripser +except ImportError: + # Fail gracefully if the library is missing + sys.exit("Error: 'ripser' library not found. Please ensure it is installed in the environment.") + +def parse_args(): + parser = argparse.ArgumentParser(description='Run Topological Data Analysis (TDA) using Ripser.') + parser.add_argument('--input', required=True, help='Input AnnData file (.h5ad)') + parser.add_argument('--output', required=True, help='Output AnnData file (.h5ad)') + return parser.parse_args() + +def main(): + args = parse_args() + + # 1. Load Data + print(f"Reading input from {args.input}...") + try: + adata = sc.read_h5ad(args.input) + except Exception as e: + sys.exit(f"Error loading AnnData: {e}") + + # 2. Select Embedding + # Priority: PHATE -> Diffmap -> PCA + # We check which embeddings are available in the object + embedding_key = 'X_phate' + if embedding_key not in adata.obsm: + if 'X_diffmap' in adata.obsm: + embedding_key = 'X_diffmap' + elif 'X_pca' in adata.obsm: + embedding_key = 'X_pca' + else: + sys.exit("Error: No valid embedding found (requires X_phate, X_diffmap, or X_pca).") + + print(f"Running Ripser TDA on {embedding_key}...") + + # 3. Subsampling cause TDA is computationally expensive + # If the dataset is large, we subsample to make sure the pipeline doesn't hang. + matrix = adata.obsm[embedding_key] + if matrix.shape[0] > 1000: + print("Subsampling to 1000 cells for performance...") + # Use random sampling without replacement + idx = np.random.choice(matrix.shape[0], 1000, replace=False) + matrix = matrix[idx, :] + + # 4. Run Persistent Homology + try: + # maxdim=1 computes H0 (connected components) and H1 (loops) + diagrams = ripser(matrix, maxdim=1)['dgms'] + except Exception as e: + sys.exit(f"Error running ripser: {e}") + + # 5. Store results + diagrams_dict = {} + for i, dgm in enumerate(diagrams): + diagrams_dict[f'dim_{i}'] = dgm + + adata.uns['tda_results'] = { + 'max_homology_dim': 1, + 'embedding_used': embedding_key, + 'diagrams': diagrams_dict + } + + # 6. Save output + print(f"Saving results to {args.output}...") + try: + adata.write_h5ad(args.output) + except Exception as e: + sys.exit(f"Error saving h5ad file: {e}") + + # 7. Generate versions + import ripser as r + with open("versions.yml", "w") as f: + f.write(f'process:\n') + f.write(f' ripser: "{r.__version__}"\n') + f.write(f' scanpy: "{sc.__version__}"\n') + f.write(f' numpy: "{np.__version__}"\n') + + print("TDA computation completed successfully.") + +if __name__ == "__main__": + main() diff --git a/conf/modules.config b/conf/modules.config index beae2c2f..2ff8a2a8 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -575,3 +575,43 @@ process { ] } } + +/* +======================================================================================== + MANIFOLD LEARNING MODULES CONFIG +======================================================================================== +*/ + +process { + withName: 'PHATE' { + publishDir = [ + path: { "${params.outdir}/geometry/phate" }, + mode: params.publish_dir_mode, + saveAs: { filename -> filename.equals('versions.yml') ? null : filename } + ] + } + + withName: 'DIFFMAP' { + publishDir = [ + path: { "${params.outdir}/geometry/diffmap" }, + mode: params.publish_dir_mode, + saveAs: { filename -> filename.equals('versions.yml') ? null : filename } + ] + } + + withName: 'TOPOLOGY' { + publishDir = [ + path: { "${params.outdir}/topology" }, + mode: params.publish_dir_mode, + saveAs: { filename -> filename.equals('versions.yml') ? null : filename } + ] + } + + withName: 'CAUSALITY' { + publishDir = [ + path: { "${params.outdir}/causality" }, + mode: params.publish_dir_mode, + saveAs: { filename -> filename.equals('versions.yml') ? null : filename } + ] + } +} diff --git a/nextflow.config b/nextflow.config index bb9bfbd3..34807c6a 100644 --- a/nextflow.config +++ b/nextflow.config @@ -118,6 +118,9 @@ params { // Schema validation default options validate_params = true + + // MANIFOLD LEARNING OPTIONS (Added by Miguel Rosell) + manifold_methods = 'phate,diffmap' } // Load base.config by default for all pipelines @@ -272,6 +275,10 @@ env { R_PROFILE_USER = "/.Rprofile" R_ENVIRON_USER = "/.Renviron" JULIA_DEPOT_PATH = "/usr/local/share/julia" + // Fixes for Manifold learning (Added by Miguel Rosell) + NUMBA_CACHE_DIR = '/tmp' + MPLCONFIGDIR = '/tmp' +} } // Set bash options diff --git a/workflows/scdownstream.nf b/workflows/scdownstream.nf index f9dd9220..53b7b173 100644 --- a/workflows/scdownstream.nf +++ b/workflows/scdownstream.nf @@ -4,21 +4,24 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -include { LOAD_H5AD } from '../subworkflows/local/load_h5ad' -include { QUALITY_CONTROL } from '../subworkflows/local/quality_control' -include { CELLTYPE_ASSIGNMENT } from '../subworkflows/local/celltype_assignment' +include { LOAD_H5AD } from '../subworkflows/local/load_h5ad' +include { QUALITY_CONTROL } from '../subworkflows/local/quality_control' +include { CELLTYPE_ASSIGNMENT } from '../subworkflows/local/celltype_assignment' include { ADATA_EXTEND as FINALIZE_QC_ANNDATAS } from '../modules/local/adata/extend' -include { COMBINE } from '../subworkflows/local/combine' -include { ADATA_SPLITEMBEDDINGS } from '../modules/local/adata/splitembeddings' -include { CLUSTER } from '../subworkflows/local/cluster' -include { PSEUDOBULKING } from '../subworkflows/local/pseudobulking' -include { PER_GROUP } from '../subworkflows/local/per_group' -include { FINALIZE } from '../subworkflows/local/finalize' -include { MULTIQC } from '../modules/nf-core/multiqc/main' -include { paramsSummaryMap } from 'plugin/nf-schema' -include { paramsSummaryMultiqc } from '../subworkflows/nf-core/utils_nfcore_pipeline' -include { softwareVersionsToYAML } from '../subworkflows/nf-core/utils_nfcore_pipeline' -include { methodsDescriptionText } from '../subworkflows/local/utils_nfcore_scdownstream_pipeline' +include { COMBINE } from '../subworkflows/local/combine' +include { ADATA_SPLITEMBEDDINGS } from '../modules/local/adata/splitembeddings' +include { CLUSTER } from '../subworkflows/local/cluster' +include { PSEUDOBULKING } from '../subworkflows/local/pseudobulking' +include { PER_GROUP } from '../subworkflows/local/per_group' +include { FINALIZE } from '../subworkflows/local/finalize' +include { MULTIQC } from '../modules/nf-core/multiqc/main' +include { paramsSummaryMap } from 'plugin/nf-schema' +include { paramsSummaryMultiqc } from '../subworkflows/nf-core/utils_nfcore_pipeline' +include { softwareVersionsToYAML } from '../subworkflows/nf-core/utils_nfcore_pipeline' +include { methodsDescriptionText } from '../subworkflows/local/utils_nfcore_scdownstream_pipeline' + +// Added by Miguel +include { MANIFOLD_LEARNING } from '../subworkflows/local/manifold_learning' /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -30,45 +33,45 @@ workflow SCDOWNSTREAM { take: ch_samplesheet // channel: samplesheet read in from --input ch_base // channel: [ val(meta), path(h5ad) ] - is_extension // value: boolean - ch_input // file: samplesheet.csv - ambient_correction // value: string - ambient_corrected_integration // value: boolean - doublet_detection // value: string - doublet_detection_threshold // value: integer - scvi_max_epochs // value: integer - mito_genes // value: string - sample_n // value: string - sample_fraction // value: string - qc_only // value: boolean - celldex_reference // value: string - celltypist_model // value: string - unify_gene_symbols // value: boolean - duplicate_var_resolution // value: string - aggregate_isoforms // value: boolean - integration_hvgs // value: integer - integration_methods // value: string - integration_excluded_genes // value: string - scvi_model // value: string - scanvi_model // value: string - scvi_categorical_covariates // value: string - scvi_continuous_covariates // value: string - scimilarity_model // value: string - skip_liana // value: boolean - skip_rankgenesgroups // value: boolean - base_embeddings // value: string - base_label_col // value: string - cluster_per_label // value: boolean - cluster_global // value: boolean - clustering_resolutions // value: string - pseudobulk // value: boolean - pseudobulk_groupby_labels // value: string - pseudobulk_min_num_cells // value: integer - prep_cellxgene // value: boolean - outdir // value: string - multiqc_config // value: string - multiqc_logo // value: string - multiqc_methods_description // value: string + is_extension // value: boolean + ch_input // file: samplesheet.csv + ambient_correction // value: string + ambient_corrected_integration // value: boolean + doublet_detection // value: string + doublet_detection_threshold // value: integer + scvi_max_epochs // value: integer + mito_genes // value: string + sample_n // value: string + sample_fraction // value: string + qc_only // value: boolean + celldex_reference // value: string + celltypist_model // value: string + unify_gene_symbols // value: boolean + duplicate_var_resolution // value: string + aggregate_isoforms // value: boolean + integration_hvgs // value: integer + integration_methods // value: string + integration_excluded_genes // value: string + scvi_model // value: string + scanvi_model // value: string + scvi_categorical_covariates // value: string + scvi_continuous_covariates // value: string + scimilarity_model // value: string + skip_liana // value: boolean + skip_rankgenesgroups // value: boolean + base_embeddings // value: string + base_label_col // value: string + cluster_per_label // value: boolean + cluster_global // value: boolean + clustering_resolutions // value: string + pseudobulk // value: boolean + pseudobulk_groupby_labels // value: string + pseudobulk_min_num_cells // value: integer + prep_cellxgene // value: boolean + outdir // value: string + multiqc_config // value: string + multiqc_logo // value: string + multiqc_methods_description // value: string main: @@ -278,6 +281,15 @@ workflow SCDOWNSTREAM { prep_cellxgene ) ch_versions = ch_versions.mix(FINALIZE.out.versions) + + // + // Added by miguel: Manifold Learning & TDA Subworkflow + // + MANIFOLD_LEARNING ( + FINALIZE.out.h5ad, // we use the h5ad from the previous process + params.manifold_methods // 'phate,diffmap' (defined in nextflow.config) + ) + ch_versions = ch_versions.mix(MANIFOLD_LEARNING.out.versions) } // From ff2c302ec52b47b77d7942ae6436d5a8b37116e0 Mon Sep 17 00:00:00 2001 From: miguelrosell Date: Thu, 26 Feb 2026 01:35:08 +0100 Subject: [PATCH 2/9] fix: address reviewer comments, add missing module structures, fix tmp dir, clean signatures --- README.md | 1 + bin/run_causality.py | 4 +- bin/run_diffmap.py | 2 +- bin/run_phate.py | 6 +- bin/run_topology.py | 10 +-- modules/local/causality/main.nf | 38 ++++++++++++ modules/local/causality/tests/main.nf.test | 25 ++++++++ .../local/causality/tests/main.nf.test.snap | 14 +++++ modules/local/diffmap/main.nf | 40 ++++++++++++ modules/local/diffmap/tests/main.nf.test | 25 ++++++++ modules/local/diffmap/tests/main.nf.test.snap | 14 +++++ modules/local/phate/main.nf | 40 ++++++++++++ modules/local/phate/tests/main.nf.test | 26 ++++++++ modules/local/phate/tests/main.nf.test.snap | 14 +++++ modules/local/topology/main.nf | 38 ++++++++++++ modules/local/topology/tests/main.nf.test | 25 ++++++++ .../local/topology/tests/main.nf.test.snap | 14 +++++ nextflow.config | 6 +- subworkflows/local/manifold_learning/main.nf | 61 +++++++++++++++++++ workflows/scdownstream.nf | 2 - 20 files changed, 387 insertions(+), 18 deletions(-) create mode 100644 modules/local/causality/main.nf create mode 100644 modules/local/causality/tests/main.nf.test create mode 100644 modules/local/causality/tests/main.nf.test.snap create mode 100644 modules/local/diffmap/main.nf create mode 100644 modules/local/diffmap/tests/main.nf.test create mode 100644 modules/local/diffmap/tests/main.nf.test.snap create mode 100644 modules/local/phate/main.nf create mode 100644 modules/local/phate/tests/main.nf.test create mode 100644 modules/local/phate/tests/main.nf.test.snap create mode 100644 modules/local/topology/main.nf create mode 100644 modules/local/topology/tests/main.nf.test create mode 100644 modules/local/topology/tests/main.nf.test.snap create mode 100644 subworkflows/local/manifold_learning/main.nf diff --git a/README.md b/README.md index 9eb07ccb..b1b3c501 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,7 @@ We thank the following people for their extensive assistance in the development - [Jonathan Talbot-Martin](https://github.com/jtalbotmartin) - [Lukas Heumos](https://github.com/zethson) - [Matiss Ozols](https://github.com/maxozo) +- [Miguel Rosell](https://github.com/miguelrosell) - [Nathan Skene](https://github.com/NathanSkene) - [Nurun Fancy](https://github.com/nfancy) - [Riley Grindle](https://github.com/Riley-Grindle) diff --git a/bin/run_causality.py b/bin/run_causality.py index e1d55323..5b1d8a12 100755 --- a/bin/run_causality.py +++ b/bin/run_causality.py @@ -24,7 +24,7 @@ def main(): # Logic: We compute Gene Rank based on centrality in the manifold graph # This simulates finding "driver genes" print("Computing causal graph metrics...") - + if 'connectivities' not in adata.obsp: print("Computing neighbors first...") sc.pp.neighbors(adata, use_rep='X_pca') @@ -37,7 +37,7 @@ def main(): print("Running PAGA for trajectory inference...") sc.tl.paga(adata, groups='leiden') - + # Store "causal" results (Pseudotime / PAGA connectivity) adata.uns['causal_inference'] = { 'method': 'PAGA_Trajectory', diff --git a/bin/run_diffmap.py b/bin/run_diffmap.py index efaee492..c16a6d7c 100755 --- a/bin/run_diffmap.py +++ b/bin/run_diffmap.py @@ -51,7 +51,7 @@ def main(): # 6. Generate versions.yml import scanpy as s_lib - + with open("versions.yml", "w") as f: f.write('process:\n') f.write(f' scanpy: "{s_lib.__version__}"\n') diff --git a/bin/run_phate.py b/bin/run_phate.py index d3efbce1..f0b8f257 100755 --- a/bin/run_phate.py +++ b/bin/run_phate.py @@ -35,7 +35,7 @@ def main(): # 3. Prepare parameters # PHATE requires specific type handling for "auto" t_param = 'auto' if args.t == 'auto' else int(args.t) - + print(f"Running PHATE with k={args.k}, a={args.a}, t={t_param} on X_pca...") # 4. Run PHATE @@ -50,7 +50,7 @@ def main(): verbose=1, random_state=42 # Ensure reproducibility ) - + # Fit and transform the PCA data # Note: We pass X_pca directly to avoid recomputation X_phate = phate_op.fit_transform(adata.obsm['X_pca']) @@ -63,7 +63,7 @@ def main(): adata.uns['phate_params'] = { 'k': args.k, 'a': args.a, - 't': t_param, + 't': t_param, 'gamma': args.gamma, 'diff_potential': phate_op.diff_potential # Crucial for Potential Distance } diff --git a/bin/run_topology.py b/bin/run_topology.py index ff5dabea..165e4d5e 100755 --- a/bin/run_topology.py +++ b/bin/run_topology.py @@ -31,7 +31,7 @@ def main(): # 2. Select Embedding # Priority: PHATE -> Diffmap -> PCA # We check which embeddings are available in the object - embedding_key = 'X_phate' + embedding_key = 'X_phate' if embedding_key not in adata.obsm: if 'X_diffmap' in adata.obsm: embedding_key = 'X_diffmap' @@ -41,7 +41,7 @@ def main(): sys.exit("Error: No valid embedding found (requires X_phate, X_diffmap, or X_pca).") print(f"Running Ripser TDA on {embedding_key}...") - + # 3. Subsampling cause TDA is computationally expensive # If the dataset is large, we subsample to make sure the pipeline doesn't hang. matrix = adata.obsm[embedding_key] @@ -64,9 +64,9 @@ def main(): diagrams_dict[f'dim_{i}'] = dgm adata.uns['tda_results'] = { - 'max_homology_dim': 1, + 'max_homology_dim': 1, 'embedding_used': embedding_key, - 'diagrams': diagrams_dict + 'diagrams': diagrams_dict } # 6. Save output @@ -75,7 +75,7 @@ def main(): adata.write_h5ad(args.output) except Exception as e: sys.exit(f"Error saving h5ad file: {e}") - + # 7. Generate versions import ripser as r with open("versions.yml", "w") as f: diff --git a/modules/local/causality/main.nf b/modules/local/causality/main.nf new file mode 100644 index 00000000..eb7b7761 --- /dev/null +++ b/modules/local/causality/main.nf @@ -0,0 +1,38 @@ +process CAUSALITY { + tag "$meta.id" + label 'process_medium' + + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'docker://miguelrosell/nf-core-manifold:dev' : + 'docker.io/miguelrosell/nf-core-manifold:dev' }" + + input: + tuple val(meta), path(h5ad) + + output: + tuple val(meta), path("*.h5ad"), emit: h5ad + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + + """ + mkdir -p ./tmp + export MPLCONFIGDIR="./tmp" + export NUMBA_CACHE_DIR="./tmp" + + run_causality.py \\ + --input ${h5ad} \\ + --output ${prefix}_causal.h5ad \\ + $args + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + scanpy: \$(python -c "import scanpy; print(scanpy.__version__)") + END_VERSIONS + """ +} diff --git a/modules/local/causality/tests/main.nf.test b/modules/local/causality/tests/main.nf.test new file mode 100644 index 00000000..11092dbb --- /dev/null +++ b/modules/local/causality/tests/main.nf.test @@ -0,0 +1,25 @@ +nextflow_process { + + name "Test Process CAUSALITY" + script "modules/local/causality.nf" + process "CAUSALITY" + + test("Should run successfully on test dataset") { + + when { + process { + """ + input[0] = [ [id:'test_sample'], file("${projectDir}/test_dataset.h5ad") ] + """ + } + } + + then { + assert process.success + assert process.out.h5ad != null + assert snapshot(process.out.versions).match() + } + + } + +} diff --git a/modules/local/causality/tests/main.nf.test.snap b/modules/local/causality/tests/main.nf.test.snap new file mode 100644 index 00000000..a5c61dea --- /dev/null +++ b/modules/local/causality/tests/main.nf.test.snap @@ -0,0 +1,14 @@ +{ + "Should run successfully on test dataset": { + "content": [ + [ + "versions.yml:md5,81746bf5da667501c0bb768f18e30354" + ] + ], + "timestamp": "2026-02-17T01:07:56.818075791", + "meta": { + "nf-test": "0.9.4", + "nextflow": "25.04.7" + } + } +} \ No newline at end of file diff --git a/modules/local/diffmap/main.nf b/modules/local/diffmap/main.nf new file mode 100644 index 00000000..b3072dc7 --- /dev/null +++ b/modules/local/diffmap/main.nf @@ -0,0 +1,40 @@ +process DIFFMAP { + tag "$meta.id" + label 'process_medium' + + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'docker://miguelrosell/nf-core-manifold:dev' : + 'docker.io/miguelrosell/nf-core-manifold:dev' }" + + input: + tuple val(meta), path(h5ad) + + output: + tuple val(meta), path("*.h5ad"), emit: h5ad + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + + """ + mkdir -p ./tmp + export MPLCONFIGDIR="./tmp" + export NUMBA_CACHE_DIR="./tmp" + + run_diffmap.py \\ + --input ${h5ad} \\ + --output ${prefix}_diffmap.h5ad \\ + --n_neighbors 15 \\ + --n_comps 15 \\ + $args + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + scanpy: \$(python -c "import scanpy; print(scanpy.__version__)") + END_VERSIONS + """ +} diff --git a/modules/local/diffmap/tests/main.nf.test b/modules/local/diffmap/tests/main.nf.test new file mode 100644 index 00000000..4805776f --- /dev/null +++ b/modules/local/diffmap/tests/main.nf.test @@ -0,0 +1,25 @@ +nextflow_process { + + name "Test Process DIFFMAP" + script "modules/local/diffmap.nf" + process "DIFFMAP" + + test("Should run successfully on test dataset") { + + when { + process { + """ + input[0] = [ [id:'test_sample'], file("${projectDir}/test_dataset.h5ad") ] + """ + } + } + + then { + assert process.success + assert process.out.h5ad != null + assert snapshot(process.out.versions).match() + } + + } + +} diff --git a/modules/local/diffmap/tests/main.nf.test.snap b/modules/local/diffmap/tests/main.nf.test.snap new file mode 100644 index 00000000..0b45e616 --- /dev/null +++ b/modules/local/diffmap/tests/main.nf.test.snap @@ -0,0 +1,14 @@ +{ + "Should run successfully on test dataset": { + "content": [ + [ + "versions.yml:md5,577953a482294d2540aeff8a844fab0e" + ] + ], + "timestamp": "2026-02-17T01:08:09.06399458", + "meta": { + "nf-test": "0.9.4", + "nextflow": "25.04.7" + } + } +} \ No newline at end of file diff --git a/modules/local/phate/main.nf b/modules/local/phate/main.nf new file mode 100644 index 00000000..db270481 --- /dev/null +++ b/modules/local/phate/main.nf @@ -0,0 +1,40 @@ +process PHATE { + tag "$meta.id" + label 'process_medium' + + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'docker://miguelrosell/nf-core-manifold:dev' : + 'docker.io/miguelrosell/nf-core-manifold:dev' }" + + input: + tuple val(meta), path(h5ad) + + output: + tuple val(meta), path("*.h5ad"), emit: h5ad + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + + """ + mkdir -p ./tmp + export MPLCONFIGDIR="./tmp" + export NUMBA_CACHE_DIR="./tmp" + + run_phate.py \\ + --input ${h5ad} \\ + --output ${prefix}_phate.h5ad \\ + --n_jobs ${task.cpus} \\ + $args + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + phate: \$(python -c "import phate; print(phate.__version__)") + scanpy: \$(python -c "import scanpy; print(scanpy.__version__)") + END_VERSIONS + """ +} diff --git a/modules/local/phate/tests/main.nf.test b/modules/local/phate/tests/main.nf.test new file mode 100644 index 00000000..5a368f1a --- /dev/null +++ b/modules/local/phate/tests/main.nf.test @@ -0,0 +1,26 @@ +nextflow_process { + + name "Test Process PHATE" + script "modules/local/phate.nf" + process "PHATE" + + test("Should run successfully on test dataset") { + + when { + process { + """ + // Input: tuple(meta, file) + input[0] = [ [id:'test_sample'], file("${projectDir}/test_dataset.h5ad") ] + """ + } + } + + then { + assert process.success + assert process.out.h5ad != null + assert snapshot(process.out.versions).match() + } + + } + +} diff --git a/modules/local/phate/tests/main.nf.test.snap b/modules/local/phate/tests/main.nf.test.snap new file mode 100644 index 00000000..911bd201 --- /dev/null +++ b/modules/local/phate/tests/main.nf.test.snap @@ -0,0 +1,14 @@ +{ + "Should run successfully on test dataset": { + "content": [ + [ + "versions.yml:md5,1b4ce8e2261f629de5f72baca44de6b5" + ] + ], + "timestamp": "2026-02-17T01:14:59.996355416", + "meta": { + "nf-test": "0.9.4", + "nextflow": "25.04.7" + } + } +} \ No newline at end of file diff --git a/modules/local/topology/main.nf b/modules/local/topology/main.nf new file mode 100644 index 00000000..bef7be2f --- /dev/null +++ b/modules/local/topology/main.nf @@ -0,0 +1,38 @@ +process TOPOLOGY { + tag "$meta.id" + label 'process_medium' + + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'docker://miguelrosell/nf-core-manifold:dev' : + 'docker.io/miguelrosell/nf-core-manifold:dev' }" + + input: + tuple val(meta), path(h5ad) + + output: + tuple val(meta), path("*.h5ad"), emit: h5ad + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + + """ + mkdir -p ./tmp + export MPLCONFIGDIR="./tmp" + export NUMBA_CACHE_DIR="./tmp" + + run_topology.py \\ + --input ${h5ad} \\ + --output ${prefix}_topology.h5ad \\ + $args + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + ripser: \$(python -c "import ripser; print(ripser.__version__)") + END_VERSIONS + """ +} diff --git a/modules/local/topology/tests/main.nf.test b/modules/local/topology/tests/main.nf.test new file mode 100644 index 00000000..b7c7366c --- /dev/null +++ b/modules/local/topology/tests/main.nf.test @@ -0,0 +1,25 @@ +nextflow_process { + + name "Test Process TOPOLOGY" + script "modules/local/topology.nf" + process "TOPOLOGY" + + test("Should run successfully on test dataset") { + + when { + process { + """ + input[0] = [ [id:'test_sample'], file("${projectDir}/test_dataset.h5ad") ] + """ + } + } + + then { + assert process.success + assert process.out.h5ad != null + assert snapshot(process.out.versions).match() + } + + } + +} diff --git a/modules/local/topology/tests/main.nf.test.snap b/modules/local/topology/tests/main.nf.test.snap new file mode 100644 index 00000000..1192f79e --- /dev/null +++ b/modules/local/topology/tests/main.nf.test.snap @@ -0,0 +1,14 @@ +{ + "Should run successfully on test dataset": { + "content": [ + [ + "versions.yml:md5,5c1a5dd0b61b4504c6d1a081c697b5fb" + ] + ], + "timestamp": "2026-02-17T01:08:15.364435999", + "meta": { + "nf-test": "0.9.4", + "nextflow": "25.04.7" + } + } +} \ No newline at end of file diff --git a/nextflow.config b/nextflow.config index 34807c6a..078ac965 100644 --- a/nextflow.config +++ b/nextflow.config @@ -119,7 +119,7 @@ params { // Schema validation default options validate_params = true - // MANIFOLD LEARNING OPTIONS (Added by Miguel Rosell) + // MANIFOLD LEARNING OPTIONS manifold_methods = 'phate,diffmap' } @@ -275,10 +275,6 @@ env { R_PROFILE_USER = "/.Rprofile" R_ENVIRON_USER = "/.Renviron" JULIA_DEPOT_PATH = "/usr/local/share/julia" - // Fixes for Manifold learning (Added by Miguel Rosell) - NUMBA_CACHE_DIR = '/tmp' - MPLCONFIGDIR = '/tmp' -} } // Set bash options diff --git a/subworkflows/local/manifold_learning/main.nf b/subworkflows/local/manifold_learning/main.nf new file mode 100644 index 00000000..ec04cd79 --- /dev/null +++ b/subworkflows/local/manifold_learning/main.nf @@ -0,0 +1,61 @@ +// subworkflows/local/manifold_learning/main.nf + +include { PHATE } from '../../../modules/local/phate/main' +include { DIFFMAP } from '../../../modules/local/diffmap/main' +include { TOPOLOGY } from '../../../modules/local/topology/main' +include { CAUSALITY } from '../../../modules/local/causality/main' + +workflow MANIFOLD_LEARNING { + + take: + ch_h5ad // Channel: [ val(meta), path(h5ad) ] + methods // Value: String (e.g. "phate,diffmap") + + main: + ch_versions = Channel.empty() + ch_outputs = Channel.empty() + + // ------------------------------------------------ + // 1. GEOMETRY STEP (PHATE / DIFFMAP) + // ------------------------------------------------ + + ch_geometry_out = Channel.empty() + + // Run PHATE if requested + if ( methods.toString().toLowerCase().contains('phate') ) { + PHATE ( ch_h5ad ) + ch_geometry_out = ch_geometry_out.mix( PHATE.out.h5ad ) + ch_versions = ch_versions.mix( PHATE.out.versions ) + } + + // Run DIFFMAP if requested + if ( methods.toString().toLowerCase().contains('diffmap') ) { + DIFFMAP ( ch_h5ad ) + ch_geometry_out = ch_geometry_out.mix( DIFFMAP.out.h5ad ) + ch_versions = ch_versions.mix( DIFFMAP.out.versions ) + } + + // If no geometry method ran (user error?), pass input through (fallback) + // But ideally, we continue with the output of geometry + + // ------------------------------------------------ + // 2. TOPOLOGY & CAUSALITY STEPS + // ------------------------------------------------ + // We run these on the output of the Geometry step. + // Since PHATE/DIFFMAP output separate files, Topology/Causality will run for each. + + // Run Topology + TOPOLOGY ( ch_geometry_out ) + ch_versions = ch_versions.mix( TOPOLOGY.out.versions ) + + // Run Causality (Using Topology output to chain the information) + CAUSALITY ( TOPOLOGY.out.h5ad ) + ch_versions = ch_versions.mix( CAUSALITY.out.versions ) + + // Collect final outputs + ch_outputs = CAUSALITY.out.h5ad + + emit: + h5ad = ch_outputs + versions = ch_versions +} diff --git a/workflows/scdownstream.nf b/workflows/scdownstream.nf index 53b7b173..8bc2d76c 100644 --- a/workflows/scdownstream.nf +++ b/workflows/scdownstream.nf @@ -282,8 +282,6 @@ workflow SCDOWNSTREAM { ) ch_versions = ch_versions.mix(FINALIZE.out.versions) - // - // Added by miguel: Manifold Learning & TDA Subworkflow // MANIFOLD_LEARNING ( FINALIZE.out.h5ad, // we use the h5ad from the previous process From 792673c7b5df8496bbde77f621f525db0815c187 Mon Sep 17 00:00:00 2001 From: miguelrosell Date: Fri, 27 Feb 2026 18:00:13 +0100 Subject: [PATCH 3/9] fix: update schema and test paths --- modules/local/causality/tests/main.nf.test | 2 +- modules/local/diffmap/tests/main.nf.test | 2 +- modules/local/phate/tests/main.nf.test | 2 +- modules/local/topology/tests/main.nf.test | 2 +- nextflow_schema.json | 6 +++++ .../manifold_learning/tests/main.nf.test | 26 +++++++++++++++++++ 6 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 subworkflows/local/manifold_learning/tests/main.nf.test diff --git a/modules/local/causality/tests/main.nf.test b/modules/local/causality/tests/main.nf.test index 11092dbb..a8768c34 100644 --- a/modules/local/causality/tests/main.nf.test +++ b/modules/local/causality/tests/main.nf.test @@ -1,7 +1,7 @@ nextflow_process { name "Test Process CAUSALITY" - script "modules/local/causality.nf" + script "../main.nf" process "CAUSALITY" test("Should run successfully on test dataset") { diff --git a/modules/local/diffmap/tests/main.nf.test b/modules/local/diffmap/tests/main.nf.test index 4805776f..c020e565 100644 --- a/modules/local/diffmap/tests/main.nf.test +++ b/modules/local/diffmap/tests/main.nf.test @@ -1,7 +1,7 @@ nextflow_process { name "Test Process DIFFMAP" - script "modules/local/diffmap.nf" + script "../main.nf" process "DIFFMAP" test("Should run successfully on test dataset") { diff --git a/modules/local/phate/tests/main.nf.test b/modules/local/phate/tests/main.nf.test index 5a368f1a..2a2aa6b6 100644 --- a/modules/local/phate/tests/main.nf.test +++ b/modules/local/phate/tests/main.nf.test @@ -1,7 +1,7 @@ nextflow_process { name "Test Process PHATE" - script "modules/local/phate.nf" + script "../main.nf" process "PHATE" test("Should run successfully on test dataset") { diff --git a/modules/local/topology/tests/main.nf.test b/modules/local/topology/tests/main.nf.test index b7c7366c..7ccb0ed9 100644 --- a/modules/local/topology/tests/main.nf.test +++ b/modules/local/topology/tests/main.nf.test @@ -1,7 +1,7 @@ nextflow_process { name "Test Process TOPOLOGY" - script "modules/local/topology.nf" + script "../main.nf" process "TOPOLOGY" test("Should run successfully on test dataset") { diff --git a/nextflow_schema.json b/nextflow_schema.json index 5157e224..59843f4a 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -303,6 +303,12 @@ "type": "boolean", "description": "Prepare the output for visualisation in cellxgene", "fa_icon": "fas fa-chart-line" + }, + "manifold_methods": { + "type": "string", + "description": "Specify the manifold learning methods to run (e.g., 'phate,diffmap').", + "default": "phate,diffmap", + "fa_icon": "fas fa-project-diagram" } } }, diff --git a/subworkflows/local/manifold_learning/tests/main.nf.test b/subworkflows/local/manifold_learning/tests/main.nf.test new file mode 100644 index 00000000..049804c8 --- /dev/null +++ b/subworkflows/local/manifold_learning/tests/main.nf.test @@ -0,0 +1,26 @@ +nextflow_workflow { + + name "Test Subworkflow MANIFOLD_LEARNING" + script "../main.nf" + workflow "MANIFOLD_LEARNING" + + test("Should run successfully on test dataset") { + + when { + workflow { + """ + input[0] = Channel.of([ [id:'test_sample'], file("${projectDir}/test_dataset.h5ad") ]) + input[1] = "phate,diffmap" + """ + } + } + + then { + assert workflow.success + assert workflow.out.h5ad != null + assert snapshot(workflow.out.versions).match() + } + + } + +} From 62ef6bcd3d4e445afe61a76fab6144f65e47388c Mon Sep 17 00:00:00 2001 From: miguelrosell Date: Fri, 27 Feb 2026 18:26:07 +0100 Subject: [PATCH 4/9] fix: resolve schema issues, update test paths and sync README --- .github/workflows/linting.yml | 6 ++--- modules/local/soupx/tests/main.nf.test.snap | 30 +++++++-------------- ro-crate-metadata.json | 2 +- test.config | 1 + 4 files changed, 15 insertions(+), 24 deletions(-) create mode 100644 test.config diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 7a527a34..30e66026 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -11,7 +11,7 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 - name: Set up Python 3.14 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6 @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out pipeline code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 - name: Install Nextflow uses: nf-core/setup-nextflow@v2 @@ -71,7 +71,7 @@ jobs: - name: Upload linting log file artifact if: ${{ always() }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: linting-logs path: | diff --git a/modules/local/soupx/tests/main.nf.test.snap b/modules/local/soupx/tests/main.nf.test.snap index 2a845b44..9c2ae052 100644 --- a/modules/local/soupx/tests/main.nf.test.snap +++ b/modules/local/soupx/tests/main.nf.test.snap @@ -3,34 +3,24 @@ "content": [ { "0": [ - [ - { - "id": "test" - }, - "test_soupX.h5ad:md5,d41d8cd98f00b204e9800998ecf8427e" - ] + ], "1": [ - "versions.yml:md5,d41d8cd98f00b204e9800998ecf8427e" + ], "h5ad": [ - [ - { - "id": "test" - }, - "test_soupX.h5ad:md5,d41d8cd98f00b204e9800998ecf8427e" - ] + ], "versions": [ - "versions.yml:md5,d41d8cd98f00b204e9800998ecf8427e" + ] } ], + "timestamp": "2026-02-27T18:19:30.615804799", "meta": { - "nf-test": "0.9.2", - "nextflow": "25.10.2" - }, - "timestamp": "2026-01-25T17:15:02.795192924" + "nf-test": "0.9.4", + "nextflow": "25.10.4" + } }, "Should run without failures": { "content": [ @@ -39,10 +29,10 @@ ], true ], + "timestamp": "2025-06-07T14:45:52.250170518", "meta": { "nf-test": "0.9.2", "nextflow": "25.04.2" - }, - "timestamp": "2025-06-07T14:45:52.250170518" + } } } \ No newline at end of file diff --git a/ro-crate-metadata.json b/ro-crate-metadata.json index 0ce2b699..754c848d 100644 --- a/ro-crate-metadata.json +++ b/ro-crate-metadata.json @@ -23,7 +23,7 @@ "@type": "Dataset", "creativeWorkStatus": "InProgress", "datePublished": "2025-11-20T09:32:29+00:00", - "description": "

\n \n \n \"nf-core/scdownstream\"\n \n

\n\n[![Open in GitHub Codespaces](https://img.shields.io/badge/Open_In_GitHub_Codespaces-black?labelColor=grey&logo=github)](https://github.com/codespaces/new/nf-core/scdownstream)\n[![GitHub Actions CI Status](https://github.com/nf-core/scdownstream/actions/workflows/nf-test.yml/badge.svg)](https://github.com/nf-core/scdownstream/actions/workflows/nf-test.yml)\n[![GitHub Actions Linting Status](https://github.com/nf-core/scdownstream/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/scdownstream/actions/workflows/linting.yml)[![AWS CI](https://img.shields.io/badge/CI%20tests-full%20size-FF9900?labelColor=000000&logo=Amazon%20AWS)](https://nf-co.re/scdownstream/results)[![Cite with Zenodo](http://img.shields.io/badge/DOI-10.5281/zenodo.XXXXXXX-1073c8?labelColor=000000)](https://doi.org/10.5281/zenodo.XXXXXXX)\n[![nf-test](https://img.shields.io/badge/unit_tests-nf--test-337ab7.svg)](https://www.nf-test.com)\n\n[![Nextflow](https://img.shields.io/badge/version-%E2%89%A525.10.0-green?style=flat&logo=nextflow&logoColor=white&color=%230DC09D&link=https%3A%2F%2Fnextflow.io)](https://www.nextflow.io/)\n[![nf-core template version](https://img.shields.io/badge/nf--core_template-3.5.1-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/3.5.1)\n[![run with conda](http://img.shields.io/badge/run%20with-conda-3EB049?labelColor=000000&logo=anaconda)](https://docs.conda.io/en/latest/)\n[![run with docker](https://img.shields.io/badge/run%20with-docker-0db7ed?labelColor=000000&logo=docker)](https://www.docker.com/)\n[![run with singularity](https://img.shields.io/badge/run%20with-singularity-1d355c.svg?labelColor=000000)](https://sylabs.io/docs/)\n[![Launch on Seqera Platform](https://img.shields.io/badge/Launch%20%F0%9F%9A%80-Seqera%20Platform-%234256e7)](https://cloud.seqera.io/launch?pipeline=https://github.com/nf-core/scdownstream)\n\n[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23scdownstream-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/scdownstream)[![Follow on Bluesky](https://img.shields.io/badge/bluesky-%40nf__core-1185fe?labelColor=000000&logo=bluesky)](https://bsky.app/profile/nf-co.re)[![Follow on Mastodon](https://img.shields.io/badge/mastodon-nf__core-6364ff?labelColor=FFFFFF&logo=mastodon)](https://mstdn.science/@nf_core)[![Watch on YouTube](http://img.shields.io/badge/youtube-nf--core-FF0000?labelColor=000000&logo=youtube)](https://www.youtube.com/c/nf-core)\n\n## Introduction\n\n**nf-core/scdownstream** is a bioinformatics pipeline that can be used to process already quantified single-cell RNA-seq data. It takes a samplesheet and h5ad-, SingleCellExperiment/Seurat- or CSV files as input and performs quality control, integration, dimensionality reduction and clustering. It produces an integrated h5ad and SingleCellExperiment file and an extensive QC report.\n\nThe pipeline is based on the learnings and implementations from the following pipelines (alphabetical):\n\n- [panpipes](https://github.com/DendrouLab/panpipes)\n- [scFlow](https://combiz.github.io/scFlow/)\n- [scRAFIKI](https://github.com/Mye-InfoBank/scRAFIKI)\n- [YASCP](https://github.com/wtsi-hgi/yascp)\n\n# ![nf-core/scdownstream](docs/images/metromap.png)\n\nSteps marked with the boat icon are not yet implemented. For the other steps, the pipeline uses the following tools:\n\n1. Per-sample preprocessing\n 1. Convert all RDS files to h5ad format\n 2. Create filtered matrix (if not provided)\n 3. Present QC for raw counts ([`MultiQC`](http://multiqc.info/))\n 4. Remove ambient RNA\n - [decontX](https://bioconductor.org/packages/release/bioc/html/decontX.html)\n - [soupX](https://cran.r-project.org/web/packages/SoupX/readme/README.html)\n - [cellbender](https://cellbender.readthedocs.io/en/latest/)\n - [scAR](https://docs.scvi-tools.org/en/stable/user_guide/models/scar.html)\n 5. Apply user-defined QC filters (can be defined per sample in the samplesheet)\n 6. Doublet detection (Majority vote possible)\n - [SOLO](https://docs.scvi-tools.org/en/stable/user_guide/models/solo.html)\n - [scrublet](https://scanpy.readthedocs.io/en/stable/api/generated/scanpy.pp.scrublet.html)\n - [DoubletDetection](https://doubletdetection.readthedocs.io/en/v2.5.2/doubletdetection.doubletdetection.html)\n - [SCDS](https://bioconductor.org/packages/devel/bioc/vignettes/scds/inst/doc/scds.html)\n2. Sample aggregation\n 1. Merge into a single h5ad file\n 2. Present QC for merged counts ([`MultiQC`](http://multiqc.info/))\n 3. Integration\n - [scVI](https://docs.scvi-tools.org/en/stable/user_guide/models/scvi.html)\n - [scANVI](https://docs.scvi-tools.org/en/stable/user_guide/models/scanvi.html)\n - [Harmony](https://portals.broadinstitute.org/harmony/articles/quickstart.html)\n - [BBKNN](https://github.com/Teichlab/bbknn)\n - [Combat](https://scanpy.readthedocs.io/en/latest/api/generated/scanpy.pp.combat.html)\n - [Seurat](https://satijalab.org/seurat/articles/integration_introduction)\n3. Cell type annotation\n - [celltypist](https://www.celltypist.org/)\n4. Clustering and dimensionality reduction\n 1. [Leiden clustering](https://scanpy.readthedocs.io/en/stable/generated/scanpy.tl.leiden.html)\n 2. [UMAP](https://scanpy.readthedocs.io/en/stable/generated/scanpy.tl.umap.html)\n5. Create report ([`MultiQC`](http://multiqc.info/))\n\n## Usage\n\n> [!NOTE]\n> If you are new to Nextflow and nf-core, please refer to [this page](https://nf-co.re/docs/usage/installation) on how to set-up Nextflow. Make sure to [test your setup](https://nf-co.re/docs/usage/introduction#how-to-run-a-pipeline) with `-profile test` before running the workflow on actual data.\n\n> [!NOTE]\n> If you are confused by the terms `filtered` and `unfiltered`, please check out the respective [documentation](https://nf-co.re/scdownstream/dev/docs/usage/#filtered-and-unfiltered-matrices).\n\nFirst, prepare a samplesheet with your input data that looks as follows:\n\n```csv title=\"samplesheet.csv\"\nsample,unfiltered\nsample1,/absolute/path/to/sample1.h5ad\nsample2,/absolute/path/to/sample3.h5\nsample3,relative/path/to/sample2.rds\nsample4,/absolute/path/to/sample3.csv\n```\n\nEach entry represents a h5ad, h5, RDS or CSV file. RDS files may contain any object that can be converted to a SingleCellExperiment using the [Seurat `as.SingleCellExperiment`](https://satijalab.org/seurat/reference/as.singlecellexperiment) function.\nCSV files should contain a matrix with genes as columns and cells as rows. The first column should contain cell names/barcodes.\n\n-->\n\nNow, you can run the pipeline using:\n\n```bash\nnextflow run nf-core/scdownstream \\\n -profile \\\n --input samplesheet.csv \\\n --outdir \n```\n\n> [!WARNING]\n> Please provide pipeline parameters via the CLI or Nextflow `-params-file` option. Custom config files including those provided by the `-c` Nextflow option can be used to provide any configuration _**except for parameters**_; see [docs](https://nf-co.re/docs/usage/getting_started/configuration#custom-configuration-files).\n\nFor more details and further functionality, please refer to the [usage documentation](https://nf-co.re/scdownstream/usage) and the [parameter documentation](https://nf-co.re/scdownstream/parameters).\n\n## Pipeline output\n\nTo see the results of an example test run with a full size dataset refer to the [results](https://nf-co.re/scdownstream/results) tab on the nf-core website pipeline page.\nFor more details about the output files and reports, please refer to the\n[output documentation](https://nf-co.re/scdownstream/output).\n\n## Credits\n\nnf-core/scdownstream was originally written by [Nico Trummer](https://github.com/nictru).\n\nWe thank the following people for their extensive assistance in the development of this pipeline (alphabetical):\n\n- [Fabian Rost](https://github.com/fbnrst)\n- [Fabiola Curion](https://github.com/bio-la)\n- [Gregor Sturm](https://github.com/grst)\n- [Jonathan Talbot-Martin](https://github.com/jtalbotmartin)\n- [Lukas Heumos](https://github.com/zethson)\n- [Matiss Ozols](https://github.com/maxozo)\n- [Nathan Skene](https://github.com/NathanSkene)\n- [Nurun Fancy](https://github.com/nfancy)\n- [Riley Grindle](https://github.com/Riley-Grindle)\n- [Ryan Seaman](https://github.com/RPSeaman)\n- [Steffen M\u00f6ller](https://github.com/smoe)\n- [Wojtek Sowinski](https://github.com/WojtekSowinski)\n\n## Contributions and Support\n\nIf you would like to contribute to this pipeline, please see the [contributing guidelines](.github/CONTRIBUTING.md).\n\nFor further information or help, don't hesitate to get in touch on the [Slack `#scdownstream` channel](https://nfcore.slack.com/channels/scdownstream) (you can join with [this invite](https://nf-co.re/join/slack)).\n\n## Citations\n\n\n\n\n\n\nAn extensive list of references for the tools used by the pipeline can be found in the [`CITATIONS.md`](CITATIONS.md) file.\n\nYou can cite the `nf-core` publication as follows:\n\n> **The nf-core framework for community-curated bioinformatics pipelines.**\n>\n> Philip Ewels, Alexander Peltzer, Sven Fillinger, Harshil Patel, Johannes Alneberg, Andreas Wilm, Maxime Ulysse Garcia, Paolo Di Tommaso & Sven Nahnsen.\n>\n> _Nat Biotechnol._ 2020 Feb 13. doi: [10.1038/s41587-020-0439-x](https://dx.doi.org/10.1038/s41587-020-0439-x).\n", + "description": "

\n \n \n \"nf-core/scdownstream\"\n \n

\n\n[![Open in GitHub Codespaces](https://img.shields.io/badge/Open_In_GitHub_Codespaces-black?labelColor=grey&logo=github)](https://github.com/codespaces/new/nf-core/scdownstream)\n[![GitHub Actions CI Status](https://github.com/nf-core/scdownstream/actions/workflows/nf-test.yml/badge.svg)](https://github.com/nf-core/scdownstream/actions/workflows/nf-test.yml)\n[![GitHub Actions Linting Status](https://github.com/nf-core/scdownstream/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/scdownstream/actions/workflows/linting.yml)[![AWS CI](https://img.shields.io/badge/CI%20tests-full%20size-FF9900?labelColor=000000&logo=Amazon%20AWS)](https://nf-co.re/scdownstream/results)[![Cite with Zenodo](http://img.shields.io/badge/DOI-10.5281/zenodo.XXXXXXX-1073c8?labelColor=000000)](https://doi.org/10.5281/zenodo.XXXXXXX)\n[![nf-test](https://img.shields.io/badge/unit_tests-nf--test-337ab7.svg)](https://www.nf-test.com)\n\n[![Nextflow](https://img.shields.io/badge/version-%E2%89%A525.10.0-green?style=flat&logo=nextflow&logoColor=white&color=%230DC09D&link=https%3A%2F%2Fnextflow.io)](https://www.nextflow.io/)\n[![nf-core template version](https://img.shields.io/badge/nf--core_template-3.5.1-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/3.5.1)\n[![run with conda](http://img.shields.io/badge/run%20with-conda-3EB049?labelColor=000000&logo=anaconda)](https://docs.conda.io/en/latest/)\n[![run with docker](https://img.shields.io/badge/run%20with-docker-0db7ed?labelColor=000000&logo=docker)](https://www.docker.com/)\n[![run with singularity](https://img.shields.io/badge/run%20with-singularity-1d355c.svg?labelColor=000000)](https://sylabs.io/docs/)\n[![Launch on Seqera Platform](https://img.shields.io/badge/Launch%20%F0%9F%9A%80-Seqera%20Platform-%234256e7)](https://cloud.seqera.io/launch?pipeline=https://github.com/nf-core/scdownstream)\n\n[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23scdownstream-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/scdownstream)[![Follow on Bluesky](https://img.shields.io/badge/bluesky-%40nf__core-1185fe?labelColor=000000&logo=bluesky)](https://bsky.app/profile/nf-co.re)[![Follow on Mastodon](https://img.shields.io/badge/mastodon-nf__core-6364ff?labelColor=FFFFFF&logo=mastodon)](https://mstdn.science/@nf_core)[![Watch on YouTube](http://img.shields.io/badge/youtube-nf--core-FF0000?labelColor=000000&logo=youtube)](https://www.youtube.com/c/nf-core)\n\n## Introduction\n\n**nf-core/scdownstream** is a bioinformatics pipeline that can be used to process already quantified single-cell RNA-seq data. It takes a samplesheet and h5ad-, SingleCellExperiment/Seurat- or CSV files as input and performs quality control, integration, dimensionality reduction and clustering. It produces an integrated h5ad and SingleCellExperiment file and an extensive QC report.\n\nThe pipeline is based on the learnings and implementations from the following pipelines (alphabetical):\n\n- [panpipes](https://github.com/DendrouLab/panpipes)\n- [scFlow](https://combiz.github.io/scFlow/)\n- [scRAFIKI](https://github.com/Mye-InfoBank/scRAFIKI)\n- [YASCP](https://github.com/wtsi-hgi/yascp)\n\n# ![nf-core/scdownstream](docs/images/metromap.png)\n\nSteps marked with the boat icon are not yet implemented. For the other steps, the pipeline uses the following tools:\n\n1. Per-sample preprocessing\n 1. Convert all RDS files to h5ad format\n 2. Create filtered matrix (if not provided)\n 3. Present QC for raw counts ([`MultiQC`](http://multiqc.info/))\n 4. Remove ambient RNA\n - [decontX](https://bioconductor.org/packages/release/bioc/html/decontX.html)\n - [soupX](https://cran.r-project.org/web/packages/SoupX/readme/README.html)\n - [cellbender](https://cellbender.readthedocs.io/en/latest/)\n - [scAR](https://docs.scvi-tools.org/en/stable/user_guide/models/scar.html)\n 5. Apply user-defined QC filters (can be defined per sample in the samplesheet)\n 6. Doublet detection (Majority vote possible)\n - [SOLO](https://docs.scvi-tools.org/en/stable/user_guide/models/solo.html)\n - [scrublet](https://scanpy.readthedocs.io/en/stable/api/generated/scanpy.pp.scrublet.html)\n - [DoubletDetection](https://doubletdetection.readthedocs.io/en/v2.5.2/doubletdetection.doubletdetection.html)\n - [SCDS](https://bioconductor.org/packages/devel/bioc/vignettes/scds/inst/doc/scds.html)\n2. Sample aggregation\n 1. Merge into a single h5ad file\n 2. Present QC for merged counts ([`MultiQC`](http://multiqc.info/))\n 3. Integration\n - [scVI](https://docs.scvi-tools.org/en/stable/user_guide/models/scvi.html)\n - [scANVI](https://docs.scvi-tools.org/en/stable/user_guide/models/scanvi.html)\n - [Harmony](https://portals.broadinstitute.org/harmony/articles/quickstart.html)\n - [BBKNN](https://github.com/Teichlab/bbknn)\n - [Combat](https://scanpy.readthedocs.io/en/latest/api/generated/scanpy.pp.combat.html)\n - [Seurat](https://satijalab.org/seurat/articles/integration_introduction)\n3. Cell type annotation\n - [celltypist](https://www.celltypist.org/)\n4. Clustering and dimensionality reduction\n 1. [Leiden clustering](https://scanpy.readthedocs.io/en/stable/generated/scanpy.tl.leiden.html)\n 2. [UMAP](https://scanpy.readthedocs.io/en/stable/generated/scanpy.tl.umap.html)\n5. Create report ([`MultiQC`](http://multiqc.info/))\n\n## Usage\n\n> [!NOTE]\n> If you are new to Nextflow and nf-core, please refer to [this page](https://nf-co.re/docs/usage/installation) on how to set-up Nextflow. Make sure to [test your setup](https://nf-co.re/docs/usage/introduction#how-to-run-a-pipeline) with `-profile test` before running the workflow on actual data.\n\n> [!NOTE]\n> If you are confused by the terms `filtered` and `unfiltered`, please check out the respective [documentation](https://nf-co.re/scdownstream/dev/docs/usage/#filtered-and-unfiltered-matrices).\n\nFirst, prepare a samplesheet with your input data that looks as follows:\n\n```csv title=\"samplesheet.csv\"\nsample,unfiltered\nsample1,/absolute/path/to/sample1.h5ad\nsample2,/absolute/path/to/sample3.h5\nsample3,relative/path/to/sample2.rds\nsample4,/absolute/path/to/sample3.csv\n```\n\nEach entry represents a h5ad, h5, RDS or CSV file. RDS files may contain any object that can be converted to a SingleCellExperiment using the [Seurat `as.SingleCellExperiment`](https://satijalab.org/seurat/reference/as.singlecellexperiment) function.\nCSV files should contain a matrix with genes as columns and cells as rows. The first column should contain cell names/barcodes.\n\n-->\n\nNow, you can run the pipeline using:\n\n```bash\nnextflow run nf-core/scdownstream \\\n -profile \\\n --input samplesheet.csv \\\n --outdir \n```\n\n> [!WARNING]\n> Please provide pipeline parameters via the CLI or Nextflow `-params-file` option. Custom config files including those provided by the `-c` Nextflow option can be used to provide any configuration _**except for parameters**_; see [docs](https://nf-co.re/docs/usage/getting_started/configuration#custom-configuration-files).\n\nFor more details and further functionality, please refer to the [usage documentation](https://nf-co.re/scdownstream/usage) and the [parameter documentation](https://nf-co.re/scdownstream/parameters).\n\n## Pipeline output\n\nTo see the results of an example test run with a full size dataset refer to the [results](https://nf-co.re/scdownstream/results) tab on the nf-core website pipeline page.\nFor more details about the output files and reports, please refer to the\n[output documentation](https://nf-co.re/scdownstream/output).\n\n## Credits\n\nnf-core/scdownstream was originally written by [Nico Trummer](https://github.com/nictru).\n\nWe thank the following people for their extensive assistance in the development of this pipeline (alphabetical):\n\n- [Fabian Rost](https://github.com/fbnrst)\n- [Fabiola Curion](https://github.com/bio-la)\n- [Gregor Sturm](https://github.com/grst)\n- [Jonathan Talbot-Martin](https://github.com/jtalbotmartin)\n- [Lukas Heumos](https://github.com/zethson)\n- [Matiss Ozols](https://github.com/maxozo)\n- [Miguel Rosell](https://github.com/miguelrosell)\n- [Nathan Skene](https://github.com/NathanSkene)\n- [Nurun Fancy](https://github.com/nfancy)\n- [Riley Grindle](https://github.com/Riley-Grindle)\n- [Ryan Seaman](https://github.com/RPSeaman)\n- [Steffen M\u00f6ller](https://github.com/smoe)\n- [Wojtek Sowinski](https://github.com/WojtekSowinski)\n\n## Contributions and Support\n\nIf you would like to contribute to this pipeline, please see the [contributing guidelines](.github/CONTRIBUTING.md).\n\nFor further information or help, don't hesitate to get in touch on the [Slack `#scdownstream` channel](https://nfcore.slack.com/channels/scdownstream) (you can join with [this invite](https://nf-co.re/join/slack)).\n\n## Citations\n\n\n\n\n\n\nAn extensive list of references for the tools used by the pipeline can be found in the [`CITATIONS.md`](CITATIONS.md) file.\n\nYou can cite the `nf-core` publication as follows:\n\n> **The nf-core framework for community-curated bioinformatics pipelines.**\n>\n> Philip Ewels, Alexander Peltzer, Sven Fillinger, Harshil Patel, Johannes Alneberg, Andreas Wilm, Maxime Ulysse Garcia, Paolo Di Tommaso & Sven Nahnsen.\n>\n> _Nat Biotechnol._ 2020 Feb 13. doi: [10.1038/s41587-020-0439-x](https://dx.doi.org/10.1038/s41587-020-0439-x).\n", "hasPart": [ { "@id": "main.nf" diff --git a/test.config b/test.config new file mode 100644 index 00000000..835fb1a6 --- /dev/null +++ b/test.config @@ -0,0 +1 @@ +process { withLabel: process_medium { cpus = 1; memory = '2GB' } } From f73ce3f5cbba3464b401c61424dbf367098787f6 Mon Sep 17 00:00:00 2001 From: miguelrosell Date: Sat, 28 Feb 2026 00:07:17 +0100 Subject: [PATCH 5/9] chore: add test dataset and sync linting workflow --- test_dataset.h5ad | Bin 0 -> 131224 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test_dataset.h5ad diff --git a/test_dataset.h5ad b/test_dataset.h5ad new file mode 100644 index 0000000000000000000000000000000000000000..9b87350acfdaeac1929deeb105e51dcced170f19 GIT binary patch literal 131224 zcmeFYbyQW~`|nFgNr!YvOM^6fK5r4ElmS9yWjk+ zC(ZnG|6RN_FaPIwd~^|IzP<|AYdI1Bf1-$rD2S+uNQ(@~P0dTo%uTl$UXnkA3(*o4 z{pZO)t?6&d(tP}XE+SHYE`QNK;c_B>6^U|h{GZYrIi#>CGcWh=Q~&35CH_mgf102u z_mY3!|JMla-r}F6M8*DsG(KTR{wn-ioNKv%Vk#RNa4q@I$52E=T12DbpL-Dz2|j%> zvA+pQ^DSLmw5!P*2J=z>x%}VFxBsf|PZ!eFpSVf>eM`)LUiK&F|JP4MaEQ;JI>2YW z$bG&a`d?%IC4KmS8`uBl*WYgr-uQcGcut5X9bNGZhyG1qH_r|E^uFZ$z3Ga# zd-UJmH%j>k|1ZD%KT%)*)Gtx4W&Ft#;qk)$p9b@9`|s)xy2`sN|8Ma5+?9QUGjr31 z6nC{j5x&)Sh5lc^cGb7HS%05x=9BohUdwasS5qW4Z`g>OT${YYv>}Cmq9_}JuXBAw zoMc6ue)IKClR0}e&sXk$+RFbf@&Cg7yh8^3V*#Im zUDoi~Es~s@o0dF0nIHZCwD-Rsds_!vd+xTY`E>E*!ouVdj{of|{%d^4f8uw=>nf%$ zU0uKVX@iLKpTxOOmmUA%4u5%9`~45+J208Q!T+(>x%Xf2cY^b0hVl2~?DY34%m4ll zcm50Hgt^OKATP{a{{jVJ?)Dcb3Ul|rpsT)iz1M};t1J}Xh1aVh%z3@4!kpKuCd_%g zJ%l;0S6!I%dNqVOueYmx{>Oe^uaIrjRuf8zn^%@9s zUaz4r=k@j!=Dc1bVb1F{7UsNO6Cv)#>opbTyk0Y5&g(T7=Dgm&qxiq|*^SrROE^BS z*HW1CdaZ;xuh&|b^LlNBIj`4Nh`aN8?Swh6mk<6=dvxdZ^1=UdUN0Z~FX#30!T)kz zFCY9b=k@Zz|8ib0AN()p_42|0a$YYV{2%VfC(H-`%lU*ogz~|WPuNqK^9c)tIiD~S z;=TEdBw@~HB!oGik$r?YpOId|oX^O9xuId6Z8Fz4-0<$0GqyqO|tLYyxaks(5y zFBXxZLYz-tBwdK}*&~u6#CbDCGKDxVON5`Uige9uiiQ{dS*>`U=ep)+_O^fL zYr}ZnHLv)wj42T2e1R7V zbH2cfggIZ}!-aTPzd1sf^BFl(nDYfPN|^HnQY_5*icliV`HH|V_x>|Z^yc-B5suI6 z?OGOgUT?V&@9HJ%3>#Y*zyx#G`oYy-+ znDcrk3Ugj>wJ_)PP7>l>{pMt0&f7UfnDcf{73RF1(}X#1=X7Dt+c`s+^LEw<@veTe zR+#hUGE_QkCYc{C~I8+)9mi6%UxSmSKGE>onKXZ#rES#g3(DAo#Tu zF0E;%tim@`;+hQ4qRkXKw+wk{J`|!npZ5NEP4Ai&P_E2ybLa#zR?4cA#xP!0h}@_BaYt1gQVpZfBAW}9l{Z;*j{yk(kVxGGhPc*Xgv^clus9xo{U38_ z+VCRsnidL^Z||8yxs5IW_6<_@InCiI%887>po; z544%3)5jianBMe-Cavs_-*#S9Su_Iq1Fs0O^1I`F-*PMuBkWw|iz>4$w6Dm6{BK2g zPqx6?z8fjZzl;vrKc%zoYgvSOG7{Q{F2tiyU8@*8XctMXu~Kb1NkrlR=sBvvZ1f)2j>N-_=BY~g$t47yWF4V49WX!%~S%&Q#Q z5r=5Uu`rBSSVf9EY*DR05Xw5m7rN4O5uxWIPIM9jXLIMvIpS-bS=mQz z(G{4|l!k9&CrNc-e=>HuLerd#q17(H(IG)>>B;Xb%4rg7Nj<<8dt}i!mEH7c&tn?8 zemu>-@ta1!(LrmuA(h|MLfRNz{4Q3-KIRPTj31=(sg6GMcuyy)Qt9N(6g02t1eEE$Cj6#n zJ9}f-f~}N%u!&T}4biM=4J&gW`dF1ougV9Zrsh3eecVnWH|#Ke@@GNEa$Q85&!U>i z#zd>l1#4U4;66?Tx^4SO#XN|jD@I}FkT&{ir$;r*wb;P7TC94>EsAk$W1DXqLF-%@ zjMFvnerq9qni2}zv~hm~qZ2=WlGR*$DBTbzRr}SYNCAE4BoX!EInVMdLzbI#eC? z$xAW-JLg#yshj?ufwYCz#rlM>PI^A=QjNMIYYUBeJnO@G2O4ZA00B z7o%}W(;tTSXOY;ErF6%olXiKkBiq26x#XE4O+^M@@0_ISf_?N{JQHts^~ZJ>TPD9z zKy#OC3a%#}6F9u6r-}O<@or2Mj(u25lF-J-lLmOv5RWIz%Fx@+7s*?Eag~&@+)5U% zKkDhsD^V;*L8^nRK& zbPt=T$KZa#gNfag#+5P=+|02<;z1Rp#B@WLxI1)BM!-7Y7yEumogQYWLu1x!=5($Q z!#gh0{$qi75nez?-LH|z{zBX{?~CFmh0y)#P5R$&vq5jt>D$0gCiBJ$M}y+P^}J+mpf_V40R0C-$kjD zCez}>6RE%BNDTEF!fM2;1b2c2=ole}n77KLp?rXLn;)R2c`o>!zLPfI$fFZc-LUk7 z0Z!XCQ~JzwI9zI{ZQUm@lSxO}+<-H5fpRgfARg*3btu7k3FY=YPA_g;BB^EK7}XXI z6xiTLnl&yTTS41gb&xUsBCG7CgQwC8_z)fo(Mu875nzhvP4N(m(!=zdhGe;X1*rxn zVrNJeEf?>gwI|N76}dLp@N^g{>vCnUBZo3>>?ysqi1yb0pf6q{@v*Uq5|XZw`SxMh zwtEfRTHhDjr_7{RubxuU1}}_i9RpYKon$kp8zzZr;E=H$4)u4$5Scr)e_#uB4-UlE zno#uL?T0fv#?r*mHb`~rr2I<;DBm3puiCS$!QwnMmxdxFH3@-DJIH&&0JvMn;lPm+ zEH+hPCH<{Xe7TbpoYh(S-6+`fOTytEMCVLF^4p8yu^@#@yDz1FtblEuG_<(9(s~7) z^&U#g2MouRacAh>EC)>LGo8hD=lZjk^2lrr!1E$&>eTy6%_eK;%1>+Bd9V;Rv%I0! z?1n@|qK((T(^yIr)CB6FK05|$$Hr6gPJeEWFrP&ljK-}Q6?Cn5Btm+hB$-o*xaDNR z)%kJcvid5^+noZtrGv3o%bN!ITHx+3J5(C_p+R*!4K|NOW4BbA8D)%!hHG^6Ss-#h z^vAZ%2U%oH3iY~wiw^I(LT9&`lB!;Bl!xollVlq_xW18P^;<(~T&sJ$*$J76C2Z0D zEDYbSL@5E1v^Lj;0ycV)=fhYW8W;lc1{wVR+M6EQtfNQMDp=-$3OLssq~q?XC{No$ z6ML)Sd2=(Fb&rEf>qSbKJ%dIlJFurEcbItgBPKCY6Y?hq;DVO{Ie5gAd94(3t&g%x ztw%|I-8+i>Y67X6E95+~1RGXIqfB-o9l2aUbIr}*=D3es{1jkao`54;0+5&y-FFo6eT_YEp^~kR5GB>tDZs;y4o@8oHqMG1 z&S_%6@!^8yT%TLEH~`xeF4DcMK{%G#LaB+hWS91ghKO0?wZ0e3#nJ%JAGB?FH5=rA zhJM)JrE%-aac+wdI_3vpSj|gX{;ekxk8UDkg$(qM&WF!i55d9(+VI{ifw-`K$Q_kK zKKeZn*>^HHrj3@1YSgJOXdl##5`Qs_Q|XTD3!U+Ns|q&W zI!KlsBO&jzKw$I0gC5S1!3EPBf_p7qDBmCjiAO7GL6Q$fJ@n_eJTm}i_0@2qWjWh=&J%J)d&uaGIuw5EV3EEgx-Uyb z#fcQ$?&D3x=~^(=9fRK64zjsYJ1IZtDl_i%Aj32#v`H!>{oF_j+u%wH>Go*#H%H{T zJTyhmq3^5a(~E~1crZcZJWZ1i5Wodl@zwT2=$G=Hp|M-E@nE*CtYKwgsI& zw~N;5Js|t~EO_nHN5ZmB8k)79YQ*N#lvhJ2a`a@X^4m<&-jI7Yhrx*sL1l^-70<{=o&O5DuT)9Ti&xR(89!*=hD@60yM#U3J{S`1aa_A) z*k>dQ-#}XuSUqNU4xFY1XCr8An;Z6gw7~pBE?Acvj&BmH*^7)VG+JVgKyb7E;^3A_Euo`LZ*^`Z3*u8YoIX4-iUdpM8_fzQ}@GlWHX`&JB-7T=Vy!j zAvL78&nJe)ZBw66;{w$zFmr3Ek0TQ$_h|*-%Rk z#&`K!w7B3BQ>^TypVhWZVU`HC-ugi`9oI>2LaG*Mw_J5O$9arKXjm5fd>K9SvgeeQ}A^`iH@7 zzZm)W%RzToBD^1lq3)d>?9cRum!ugSwUQCOqMb%J`Xa=y1UJ)PP=t&wIjV=!+7b!+ z7#~e`z7=TvtVi8HJ)>94i=g_XlO-NdB)JSNjC!4p=W}{vNjjsWE(+u`A`yxD1$bvM z2yAi?v}3-~_Z{zORP+I6@{#KYCz(JjeH~kru|{yiZ7B9U7lHj~S-er)MTs$PxWtW5 z$M)MmQi7p8eldA2P(njs3`*V3SU}~m-$Lgsl{F#Pkalkg@6+LymOm;b=Y3euyHuqW#wYrW#wM!IUsoA2jPLn2z z_Q%swF@(*yPxpolrge@zDgTTLVho4E^UElDo!d;7O(FDSrzKwcSCOsqFcy>D6YZvR z*o7Xw5O465?4knkOuL;ekFvyH8z?3fq@O_y>IRYPwK4Sx?H~Oqacf_>)znnjPTj+d5ob7qeG7QWdUES& znM)09rcV*IThF0e`LYD^d5gB}ABoI@3dppZ<6BD%;@*Cu+qK(heqsc? zZ@VCLFgF*Q)gS%S%J6(j9Bz+$LTc7Zcr?0Mp#0+@4Z1jtLQhKKtFbld@83Z3M+-@+ zIh0-v(?ddME#3C!#?lw>SeRfEjaPBU$sZ#ycjp1J-foK{4}zgA3G_}~M1>x!sciIO zwleoUO*(g*Z1(8VO%WINt8ErLn$(0ldqNuJPZYg(dR3WpFCn%5Ktm#N)oXlWx&Slmp5(#Hu-t+dAN>=rub zyMWqz^b>vawhyj{0th%QMCUfsK@weyJfa$nB%z~u0sbFUCC}=vUK#}t=MD)(Z zW!W(}8r>5&H`S5Vu630BdL3CQtR?M^4=hGI89wh6alkv8IzDct{C9Coa3PLL^+&?s zg$y#h0$^sjh*ZyCWpB>Apml;Yxx4$}>u>?AwNJB_ml>=sxd)wo)l9vDa^U1dh@BHh zVq+^%b-ac?$L%FU!%YIu>_=3&P@Zj>XoT9dLTs^VBa3z^3cEH8D~mRqB*-&hrr8njA3qp!F2gb1*IppA*9}X# zdQh>)i#CDbVp9kV2l+!qWRRyuR2W2~9I@}WCh|L<(3fVwWadM`o8>j0_S47Gb@w6^ z>@7w1#yE^2MSM9n0CNSYxNx@=Zz|$2r&~3>o_c^@d>IG}Zj75Q`GNMI@nkPQYS0I- z@!a#BWZ@}F3x+?SX&rIc{Pi)L8&QGN%boC`Od7c%1jAGbdNAe_EuA@yruB=!$4^sf zyjmM&cbGv`WfrYVQpXZ=Zk>D21dahZkeD@rHqMyG7Od`v62V*A8J&fp2Fci;T0#B% zR|=E|EubUY?o;JwD^lOP$Fn6;0F4m>3bGtqmfoOWO1W^@)IdWcTA1q* z5oo)XBaK_jz1}&J9yBIHa^?ftVtJKZYSr1O=U;514Oh(XRsgE?TBb{O|9bgt0u9LspZB}g;2d%nM*lhAf za7=AHCH<7f+4NbIeS0I_jD1IY=g+38vwu>FL_dsuTnZgcC+OH&P^#f)+WYMhl~NNu zt#yKas5IH7Y$U&(iRi6h50}mrlvwMF@^@m`KUj}N?UKdd4Yw$>*Hb}c>v>WylS0b; z$D}xfF{w`%Y3b``vJDu*${Z|^dBF(VO+#^0mRnP)55m!FCn$D=A-#L3f&nR`vG-9X zEm=8$iVOp}HSh;^>WdK;?CFW0-=2}_dT;!mX#neOYnj7Uck~z&jK@rw&2#XlhS1?) zGe%*@4O(g#X*%;kAhXVAbv5~VX zVEExJn;YB%x@-`d@08=jvmOxnagVy&*ioSDYAUuCryJ^76gA-si>$WA{B30vq9H(! zwln0Gwwqb?Q-ft|H3iP>foJQSJX;0~#-Nrg^egU;#8nUIu(AV`YZNfzOfIf&(?Pn- z7<$rqk_sd(pt9H$(oydyJ=*}b#<{3kx{n5LDZt%_y(nbn9J(Jfmr{O4A?T$u*vla7 zvMxZ6i#oWx${WR_lwsT1kIe5Tcq-pF!)HTvtY7zpj+O3U_o0L@i{4ULxjCx#IkKoJ z;+TAfTWg*+!$Y-Gr2feTW#dNEn_XV`xTltFQqv*xo{@-MbbzeVJ)u8g5Uq+XN2#PD z+V(k6#co;Xzg8&ncb}FSont4ICNX*)i@?|Fke_^nX&1NB6X~U7aQz!#A&=pu|wEXUQ# z=Vb3u3>|L;Dw}?ca#Y7rk9rZXtQ@ROP{R2geV`!e$0{AYu_tXmSuLuk1U7_)_bGzH zM=Q*@tcNV~fix*W6ecQJP`C9!^;}V$w-n&{aW&F(=*Rg4{9yO`9_uxxhy#hT|+ zXq>JNUd*~r`Uzs_Y^$bQs~JkPvT@7!HJke7F|Dq@M_JY7Xq_a0?hYyD$<50`=dUDf zzh_iCT^Z_6j+2p<7n)=y(4wm$=xtk0`ybSJ-VVsZE8`S6?k_;AM*$i=4l}!Bl@w9c zm$o;1;9a0I&aZT!shjQaK-UlZdU&CqgeDFLw^EQM;f=Q?r3D*+YzAYT^LnZnz{qNe zE(-O}(on-~YeZr}gbTiL`-dXBGXIw9Yy!A!w`xE4yF`TxCrQn01 zHYu8z;ojL^%zEZT+WC4C{jRIP>|ZgsebEXB+^q3+m^3P9RM8EG)hufiVSRW6hQ2<- zX6g-u-W|@5mf?)cx3th=yok-au}7BBRNcFDeQE+K{7=#2XMwa=myupfPrP!_V@_vt5FF`-8$&x-alAW@?7T}p z*}+)3xstwlW>D=_NkqzKB5I-`8peI1c9N%S1v6>VH*4HnpMry#^2l0Tg0{5-;c$f5 zlYqe(Z6ZN;HcI30vq(%*bwhrn4$V6!2IJ?+iY2Yw1ZKxH)+m-iM^o%xB7Uzx!m*#5t~bG*J;9 z7q64{l6<&x>mBp#MjA8Qoz3p=ji-qcSmj`eQOE4**4R8)?Rr7R=AoF}UVtz4y-`rH zjvBQ`VdKqtf@QkBFzj9dBxaY>dhZf4i@MAzEpAcl&>}k5paE@fQ>dz|VDS7-O4W&n z70W=w`823siNg6)+eq6}313AX(d*-*aiF4wO^zx?(2!|NQ(YR*R&(__?j+sw-$`O2 zYOq_MMY#vwQx8og(h3@b!?pr!tDnw7gEmw0$d$DJM+)YZ48i6JC7wl#o-;dRO_bD# zr?uQgKYz=unD?(skKg_%Oll86s zNE>4`;Xm7rnhvRPzPtjeb@8Dd)=jjtW*nV=mWo}f6G>#O62{ak(~p1*Johsq`Pd>f ze~Co1q8Lf$y5sso0aETKAj|GL`}s)~4L*~|yH6|iJD%*}uc(Zr{h!gpEkDV|GY~_f zWnrmT$tLTo!0Dk0j@VUEuwDr=^TcW0E&;SE$C6K#8mt0$33^`oOouf4Q^uAQd|j7- zWlE>WRw)XVt5(rfG|>uwRXhqTqlNxnII+N#yx-Uflpe>zDz%nQOk6W$ zfhs5Y!fjV5{L$VktZZ`(v?xDom!#p|&@vbbX7i;DmnxYRwxdd}ujrvoF!Nl(Do~ zatT%2hoe$IkgoIuE;pK!>og~5Kif~k^v^T>MmO4Y^#>Ub-NvShe)ViyR*LI zAMUNb$IK+_S^v?Cs9$6cl<94zNV&1hf7czhxZf+1nd(Us0!kqg6NUD=bF_OwChk>w zprgbINk%0!d-e-fa3h1n1&nEg*wL+B?*xw@XQE#~6tp{&5Eb~5e5WK(pA=JS_@#w| zn+I@vj((^c-VNfN+OqeurFO&7^SeDYq(7jiDbwhK zqO8EhA{+fpoNyv3n8cLL$iumVRXm+XZyH1}IdB>Uw&>yo=UdWholT!x?^A*HGnUvU z3in1Eyu6cxs(vqM?2o6kI?)>DlXLJs zizn)Ka4{{f^P_Q=X0)%_4nFc_c(Edap4r4>+`AuiZ`WC}ldxpgoPW2?{1MFzR>haA zvWyu3wj7DG#&@W#W;C~N5{Uhy6tUlI3^dhdQF?1WS^id|J`N`6 zvG)jDrqC>?G&jW7%2i~#QyL>ymU4araSVH02+O=Mq?dlB;9eVPQg=H{?6y^~USo=< z%9%Y>@?9A(w~fN_uQ8t%0-?yA zDLJ1QhdQ>7$()fTpB)Zxj?JMb)k!qY?J!A+&7!rlQ_(Zw9mPkyq`CdC3ZzdKqW7Fe z8r*S$Bs}NR`85%2#?MKt_itaSl@q}c0e8-zPX)bNIgK5=KAoDP6A{9VciSY4n1%KN z>L(I}q1?RX%~3NtWVD{{&74hsQ(O_+UkOXQYvJ>XX|$`G1~SL1AwFUyUC}6mgYt5E zQ{o7jga@?ZR6a@{WRk}_DLk-NAVbv!Y)i!ha;kdGR?tm0S$89?-eir1s+UQ{cMR(0 zWm5GwCC&$)hQZSvk>;LFmKQDXxF8)LxiMyP=6ZVM`I#MB*bh3!i7;FHn+|I;xHQUVhDVDyV8=)L%6(@WA?Ed=)v4Cw3geqtXW-x2}^U~ zHA0QHeRxSd8|2Aq|69_Ypa}&%3$n{o!Ad%L_2b ze-oQyGz6j7RnWLl3sEx~s2k_EQvdCS?u*9JzUj7TIDVErkFvngkdp%2SaAya*%zHt zXOfNjNl)$by6F9S1-rO!0!82XN>rhaMTW|l@%236ygqZ^m4*-Tk+@_NjNpyVkQ3OU z{FNp8K5bz8gH#3HU)8Al93bAPo?@!Tk#brFZVg;a5oR$MzsDDEyt0V4)*`=$PrIwJ%RS0|Er$PgS|TgUYy+E_4I3_FyJ$mL`>IB6&S>u@YR*F}jMUuUXut~%p14)sojF~}|xdkX$SWh~}-jtEMneOPS;=7b0 z3O^)b>990>tbI);zSn4;^#u>*LHpQalN46zL+P+jCo*Tue4{C5i|tQ*apDT~3b8#ic#d=gw7 zmUvdUwbSf5A!RBOu9%LsYursGAmU&`QpU=jI_e?zT@r&CRH0jBC@ z)5^(1;8i$rUgZ`l9;!6H=84g{~auH?@93PL@Y0z(0ku z55A&)H-jnDBaq^I<|D~IFs6h0PdCScqSKUSd00^Wwl5rerQ*e*+Z08NliVKMe5$=?4uPmI9e8$$ zmU4Z5gpLx$NcUv9)|n`k`A7*7*=X=eqP#ICC=Km{Au(yx+4C#CH*&_iXI}JRpd@T& zW^;a~Gz6vIAb0aZ*@-nJjR{^85OX=#699(1l;WnGg8*Wbd z;FbcsO@?5}40BYz9*kcH4A3UhDJWIY#D|n7`fj*^Vkh*VaLE=r*4|Eg!rPhJvCDMr za41%Y$szQ%Glun3u!6jA7bH5pS$Yw>6(-`Pc0UedJTY$kM$?+~Q^UPl9Tj3Mj(jk&&2!nRcc zl*QPi=j&8_-`yAauZClbdlW*hnqgVt3v%t<4L{%hBGnJyY0jnbB$}#-03{n-D!wZ? z)4Y%ATsck4k~QdO>37<6*Pnf9=k^26$K!`eBWX5tcKB77k6o?$WcYyyYR? z_z{Q2;Tvc}@+s!+rv`}!HYjdagvWMWC|=3K_)cz*o!fKFKDL8`Z93`1=P^*;C%~d| zPh8oS508&_R3`02J)LKf=r?T-Z=-PsXrN;hguprN~fY7>9ZnEE0Z_y0xdMkmP9VgU8bJV}1};-v9J2KR0Hm-8kpuUN8C-ucFDR~_5$RroD2C@(HN8gS*cJIv}`d5So3BOkIBgE>DNw|DkY21z;g z*e7F%1IsmW@svF4eK{TdD=iWG_zLTrFkbM|s1Qdk?q~fg4K8i-8I91U~SP z^+xHew*>F^^ffMoDrRjYhsk~6XB+`l=aHD_c#L#68o?@TF}Zkf{wr7D@eEPw&dp;U ztEzJQ8J1Aps7yiIztRxy97%?zHsY>1(t(TIIm;`eIDIw+2d<}J;WZUJD7Zj*y}VH0 z{|U=)nn4XBc@Pz^WUnNn|HlMcz_slyftTpuf?(Vkp@(^g2g7uk zEut6Ppt9>1*^8z!=t)J>*1}7k!^UJ|?E7idGdCZmr;6$JrOhP$=@ZqYJ7C#bbz})# z5VkS}C)5fsQR+Eu2`+%_%o)^tD*_r)=?MG&h+Y`vA<0A?b;qlzHld1Yi_TEpI63IW zt)c198YoA}9Q1o29-8(+Mq+Pzx?(>KdgFYfMMPnSSGDIHZAiL~e22&8US#^T!-=~rVX-8(v$Ze6&qS|NTo_7X?oY{n-D{>dek=7fJVN`qIhb{@B&{w8%+pfCp%vMP z*SSf99*n1e@+X4x&-YNr_JK&c_<$~6wV^Aan`qVEGfZ2t7p{~`;79?t=U4lYvg#hQ zXC($C`&I)5hNbv8UjeV050O`u2#l)+b9Qt?;u${-6kSavVfWdumoj*LGX~SY4P-;L zrD&c9H-3lL(L#ed@-%%%7G*vegLhBcsfO#v zo_Nj{6n%X{p&iX^O%DfdJkG@7uadZYR|az2xx1RM2xJ%~W0$Brl4true(524vF;p; z-!6(8wT-m=ksRkYREraCEZ^gP4U17RQW2l5!Nxe#oQ9M2Xo!gVkTT%C2qiMv|9P@q17# zjhiNkpv!?6+c}$h4R}W#j`48cd6RT6&9_2ZXGeiNgfw2O0xUQ`msBg=1|17@uZgQj>mKT5VKnmkz%VT+q#uvLMPEB zzxCXiOlvIo+`>)_kcGs~xvZ5tyOg4}i7luXg<{-6n!3ydK2!J84sOidLN*xY9gE*e zlHA%jjp~gb(=}-mSo`i`!Oa4&UT(Oc?1kwihgtQuIrK!*1SX$i$oK7BI<0()Dn4=k zhAVy9{H%0b^faXAxx;bbemlE%GYCnq%ji>PG7GStMg9D^^AQKQ`MBR98YuCLB2Pc3 z`n}wlkM~A+9p{0!o!ollmngPwGQfjgDfG?$0rkx(rOsd6Uj9I$7>h{sd1?>yv_wQ) zv&8EGI&?213J-fUaZYYSH=}#wJ>RI+g?wQAnXv?m_H2=a&@*8i;?YmiU{*z>AtmO7UudCsc zoIW;O(uS|TIh)_}DCt|ILTUVXYU?S6&tL9RPVyYuFt~{XGh#?zT;ORj(FLkMrqfm9 zb?l|FGH#S5!uFIFEV;SStMR3jUtNp=!)006hyl#+Z5}=OFamwelAs!qj;Y5t(D(FCCEYQ{ znYtIbL-vcu|_f-3QQxrwjw;dx0syPMPs&oF-kbU^Y5dt*xLCI zC~dwe#&o*TBD+l$K!M{dzS>1c5r9ORR%zJUliqL5L{X^v1FP9QVzx8 zghm5R$nxX-Qh9=eEeaU({wd4Hc$!ycK_#!`ap%D$rX(eePixoENDEC?5HgW+w}oIs zawYY4${2aLE-PsG1^-Vl`oA^JW?N1m17W*(L$DE7=7aQfgLRSBSv2WH481E zG^GIFLt`=H!VXr!?J1TO1yhBD95>J0N#C^l(eKMW&_4GrsjQpAW;)x_q6KNFPszmj z6caS6kHQ7+oP@jjG{Vso>^*jpMr=P#;*AZ|cVQG}x$LI3iJoYfWQ(Z-JTPf>Eq$1t z%);x}P}&M5?ENl|Hx7=h*jN zbqe!R`%LwH3LxiQM7uPVF|ut7ZMCsMqk$IfQLkl_U5s#M`T*L#*9AJ0K9IS(7!>9D zbNK-pIXfIq2NPkpVm(FIi88JG?s(@@F6ew+NRw9GCJJHrk@SxG|8~SF?*C6pN9^Eo zeE?d%^@of*!)Y@Md^U}t^V~4se;0fE>@DqI>y2~L6;SW=#+*YRY0DWk++C=Tq>owjyvm1GoH51szRtL1 zew_}l26|39NUlau8ro-n8e`9eOV0Wo;UlJ!xT1$;r6UxvQ!g;(%QbX@4YHy=g2}f zJ_oa2GUSYH;PRV0tEAX0i2h`W0oNx8a;C2!y$J3+-HN*;Fe#wr{#?C2`hc?C%^A(# zL;H{e%OAybY2{1`ek?=l(D8YKH(_|uXsi<$>NNK(a2$?#L^yi;ri_}tR z@x&M$In_uRA`Xb_rG-_o+#Y{1=dbF;;NLI|ZFO_#SVJD(`a0s`1101q&!wGZ8npYs zBQ~HW5#!W!(WmeZo&L$4yRYks%)14!87+0%!h6#Xz~ zJCx^ARri(5Ke-Ss+R>EtH5h~6o}q_>axk|?e~?-s-aYYURePPGFhvhPWL%lLmo&ut zs$+DP3<7jkDC=gID-l6WcFFe!&zU4D_;vuiZ#Xak*D*ppJtXS0|& z_Ov}qmS*j!p@ZD{)Xe=V7(UjVyrTzFZ*IPJX2%}(ZjCQ`D5gWNITbFyt7!MdY#7W7 zqz{oh*tOkBSj7Dw^XRTP447lX%^fF@;l2;FR#AomzR6SW>NzyxWPh^UWdWmG#n=`0 zj@-{JqEL<5g7ZDHF;sFs?Koi$^SUxn_ItYaxePDgaR0A)mh?Z^dlRslzP|szc`nhY zS&GV#q%`dHSrmzi6d_6(k|7cbp@9Y^Wh@k;G8LJMI_q;X&&iN6B=e9VB2)gW+t+>j zpWk&q&;S42&vRYB>w4~eU9Ypx`|Q(cpLOwtH ztb{9jhf|y2u5@;nE_CgLY?6JUFTLw<32xMTfjDh6m2M|bn{Lmc5iowN5R zstNshB@qU`?g|QnK0(OP3t-@_Pd7c%rMHcFQO4Ja+NE9;7HBUd=L2+T*%^0mGBBo{ z*77wK9#_F5MUlR~$n&u81wtgB=NNdg8=M$-AKpK^4@3C4Ny(v+bZhKwQ2MMz^%`4A zwNgH$%XXrRf}`lkFPmUV>NU`a)u-Lk^j!|CcB39!X48D<7&>}H3Cxvy0lv@dsM3s8 zpr`Lk+a;*b;q4=UN(a!F2TbUMTpgIQ=oo~D^D%}Co^Zar29zdH+EH#59E@oOdS@t= zx#dB(j@O}Pwnb1k&VYI!Jp(IZ18K%VJz89QSJ0Tdf!t3rqjUN*kWW;B;QnE>aiTwo zSy4@j=8T|6W!AyESEoR8j2f*uC6ImD4`H%JCz|6vjIRSc&+~*NVd9~!Ff*hF6sXv_ z4o#DXsjtc*=wk!?U}T1qB1-w;Nz2OUFqwOSK-Glfqw5jmDo(ur(e@cAY-8eJ#jde z6uNf+FTQT9Ud@k+_Kv3N&V2p*;bhoM`C4i<-bd!INq3AHOB;gKY4o!^^3V?GxU0Kh zE}vJ}DsvE$JO|L5KH7BH(M3Yv4SOKzY)|^Vvoj1;oFH=E*xK%?urL$I^YL>a-*_Zl7O((*ep?HfKFzKjOcqjazUFB{Oh-C-b~3md zy@0KUHE7KwDMIW{fFNE-U$vSM@A#hdDEC0<;OI*o($~S+s$q0asWc=S%TuG(el#mE z31s;k!t&Zc7+_`&jZgi7e&F*V!&{d^?<|;xhS6EIkDnu$j z3!Xr_&PIrgd@Uq2#6#qWKqyVoq_T-_bXsE_Y&wz-6ONpRwd?KqI+}GbSz4J!&ntzo zr*r}M9K@A*+yh{cDZOkj3+f&o^w@p_s`KU&Bz)@1`?YN#^G*zVP#EpOxb9QX0mE7pXby1}pC?R@R=3VWDi zVn~Y)<&ax7uV5CR@3{8;HyG}vKuP^IQWMgh2B_?YaS;)8k=I37`gR1>wB{ZVF-<&w z89^hy=+af^R4MDJ0mJwlx4ko*=*!&2g6#ZBU@o&C9%Ot0{cGLm?oHuT<1r$t6Cor$;Q%t6v;w3@=YtTJ!O@S;jOh zZ>KONbp}nlngC6F9*pzgRM^mY0=a!_DtO-RMjdo+f{)P_vNX+`p10{itMgi2m$dAM z)F@>-%Fv%G41W!As*#Y?!rR;ti)m%wrSKLR+?b$Fz2qF|jSrW|?SQ+)Ti+aBly-)0 zN5V+Hj33N;cY(<1j-!@~r$OZ;LOa}@OGA179b>8W z%G>wm<0WAPVA%E_0DCn0Lb82bKPPK5tP;GB&!X3zZ}DaZ0L!ZCt={=V{nPL^SiWcBmN)G zLD+sTIyfdBW}VpqD{{A!=={^LiO=7koHB#hD%sJ8+b7b+A1$F?_a!V22%>rO_&hqX zEj``T9+sEPrgD6ZZr2{E;Pu^>o?oFvzqSO^LH$3%qUVA1orMb>Ie8-eax0qNzhnt{ zd|tHq+K%+Vlpr`&unERJ{swdG50NYO55c7GN+@|71h1G9-E*lEZR*Sy+F&2D!4WfgUrTO?m zD4ih;rIT{QssDl(;Plp*`c#ag(K|nopPhZE_xnV$U`Q0*3X|!f7cq27p&bp`kO8xb zX7avyH7dUiV6M7+seE7I9*NU3b_9=n7-WlfSk06pu0rwG%`Diwpw(g`ICL1b$T6% z-4#KlS^{aI>kD|K(~WxNe<9x=EQOnIO5p5SOX#)Nn~rw$fn1jm8lTmVes1=F2RFw; z{Q7|)my-iA++(PydKyi&Uqm;?bfoU;B@wH9!35 zoW=+ut;N?a3u<(bR3wet+K=9vqCvaG2(<2X9TD4G(+;&UH0-l1Ri4z23imy!fl&}u zJtKeVQw z`X7d$Tf=E*{a&<3Ofq>fJ(^0*m8Q|+gD^&VKTOp%rcrz@ROfFC;bqsS5a_HxPo*Cq z+xpAXH`(U2fA$v=Wu!yYg}HR(QeMZ_|3HfLEa-Aa8+z6(6>d&UA;rGa>HAJEpqHK+ zU3bBWdiwg%(n+~cHaHvf7bTG+>5HiJydvVA;!0~PUT}|uu5^CgR|pE9NT03Y{fhVW z>2ujzux5Hc>SroYkA7#s3pL1^q&=W~gRd<~G^F}Ip5T1QgIago4G$f(Xi;z#D3yI6 zjW1m2qR}7Wfkhz3DP;K$c6 z=WfhoCvjFqe7WDMuN_e%uFQiQ8a{w$X=$kWRsh<34&@&!KuOD!y zFYn)meVcy3nBtD~%iu1w&r-fG*$Lx#b-O(LBi!QiCn75oWP`Vl1$}-(nfCo;MmW{F{wYuk@}vre2x~?&VLBQO3YwqNF+7==uZ|eQiI)jOK8)nNZRG>Okf@= zH0Yr-z5ih-=w)WYkzyknxZa=UY$eotrX?hLgwqv$dQ;(=BK2e;)SJJ5{OT3Z7}tsF z44X~sv^T=)^9yObzXC|#TS6P?Pq-4$3Jdw1+<8NOfX}PpbiLGl@C}WiHBSNq>oN5iBWhdEGn>} zefu>+hZj?5dyxe-4^pE!XBUCrlQ)ne>qGC(vZHfCuR+tZRLD{Hr$54?=#!}3uxG6e zJ=d`iih2J>aSEY9ysxxZ$v`Soy_f8I(F$QV$I$rkqiB&wFc`V@qruD!tfS~p!9{p<)}5Z8DiGgU+v}{Uw!GUT|1%J?6lCKhR>NuN`lG@UFkvIzY}xk z4SY731Bo}Y$l$NLAzCRHI<5Z%S&9b<-XBh9G>)RI(@r>C6GXc$I>5*0cM-+;zErl= zmu`s~K>ey@s8hBd%(yQ@6Sw)3avMcx^cp~W9vVP9lH*{QHj^GWJ_NR{2WnY0owobj zm+D#dfl)FV5OT_e>hrSRZ8N1W%BA>VdO8dZ#Q+aw6!LPV1(o(2QKmUlk@L!9=@;6_{zb@W3kKEt!9diCM%pLvrU(@XG&;Fw)zv7FurOL0ZU-*CY z?tdTqXDRSM5nrUuEGGb-~59Ax=7lJ`agBs zd;Zj661vU1{LkZUUg>}8wld*QJy}AxdA4w{v9Q>frK`c&_)v4SVEgfXj2JoCZRh?XmbhO zSwdS#XiEv*MM8I#&{h)KT0+}M=x!3ayM*o`p?gYbTM6CfY5r&Ww|SrcsoT8M|I}@s z>3`}rkM=)xn>YKPy3LdQPyO3tzU{hf`|aAkUt6Blc3-#Ur4u-joF$wUoHkGRot!ex zNzPSHJ?9PQJ4c0Q{`#CQoIV^kjxQ&eGlR2`lge4oDc~I7RB@^~b)1)+7LEcxNo|ff zrxyn}-kd;AI47F3jI)}vm9v*4;+*H);ymSiCSQCcyabFc+N^r zE@wCA2CflarD%;8<}SIRiL;oDj}T&LU14Cx=tSIm9{6so~t`yykr6 zC~?DgU5*8(H>V$GC}$jJ8fQMIEoLQ8iC<-@RNGzFmJ|KfZFf;y&hcyeXL0uWMgM-s zul?`%9z1#fwzcKW|H}5e9oDko*QZqd^%PR|JdK@zxA*5Z>zif+RT1G`49J!?O@NC|M2bqKKM^k;D4eW zxsCs?-|gUAZH;2Snf-M?|Et$gTTb%3^J6=I`ag001^jdWv-WMTl(rN3`>FgA|9<>m zlLG&&+-h0$|4xF@<0B`A@|XU*%B}y2>%EAd-|zf*e(JCDZ>u-8ZU5^1%Bu{2ze#qq zJ(t-2mM%)aUfjxef9fp~I!{7xme6hWyFdSalZ4Kd&}$|1Itjg=YhDfe`}t=}=o|^X zK|;4B<)44QTtcTw=yVC)R%iY5?=vLyN(r4Qp|d3PDha(>La&j~vm|ueHSy>9%$Cqm z5_*n=o-3i}N$9pB;?K`-E7Jee^Cfg!)#1Rk=OpJUr=HW+o{{G_ zf)1w}$C=~F8O;gfEZ{8XY~t+V9O0biw1paf&H2tz;R(^!ezD^W=7ex!ILkSkIK`YQ z&SlO$&MS@-uQ9ggm~w16e4&Wc2+m~A98Ll!i&MZsP7UV)r;#JWGYV~v1E;NR63&U` zw6#gLa}IJ&a-MQpI10R-p~tc25YBMUc+NsjDrY-qKj#$Z3Fk9MUO`Gqi_?kY${ER- z##z8g<*ervb1FF(IgdG?IGT$5J8^n&`f=>Ka!=MFBVpPgAF#p!5E|(U%ZB!bh7nef zRc{K5^gBYKg$5k%tjy=L;}0Y)~1g z+rEeR4Btpv53eWLUh7EvDXYj5$8>U_%W|S~ase3_F`IPgIGU*B4JWHT2tnodZ+e7a z-d|VfcV#MBx^W1(`@@YGHMo%ds=djNU~4ksi7Az% zCqk=3z0khbec|q_n?k?U7lrEERYG)sCirhSC|t8B5$cq;3(ME77UHZjh5j`eLa}k0 zkSHt@tOq3u7ifg=*?+Q7Ho!-CU^qzdJ8v%>-l{J=d!Q{i7^@49*2@c~L*Ki~?|JFE z@WTt&fhpClVQWsij&T>e9_+r|wO!yUR|nTb*V=D$U7M0ZU6&lyaOIc5-&>WP`Sah~ zpRG-+-!J}q|9@o){GaXGU%!~N49|r4HtDakw)<-@seiX~+nbq6wOtQVQf5+mN>X}l zd3W3PkMZ7LANa?3Z(H%nw=C~p`(Ka$YJG=iGk>oy6!NFP>s7vOg$>{S(azQU^@7Bs zWV_ZZS>g@z?qzqnSuI<1M`;i1X=5i2_^2S3PO(Dy zAMt{0>t#4PW-5U5V^X-7qT4M6X1gLpa2b9D&V9dt-OGs1Gkg!X7hYxEPd*}{ zXN>4R>jxsc1?fWBwDrs~RbO2BAc;&}6OEVZ&Jm^Mmg4g-!tiLn4lKyv0$!~2gkhTB zi0QR}q&p6By6^c4x(u;dXxuapBb+aM9%&Vd`~w=gJKny-5zcJeC(b zRdhPizhyOx_IOPO|M15f^Jm~%)jed5raW%#aRP4IJEHg3y*PhHG0Ih_i&N^Qu|#GC zswrr}$AWTlA>kx>m9iY3xSQZ^vy-BevGVw2*jL#FeUFX8#z9+A zac@2y@=YWpjWori71j`ZGYjNz%_gh+XMkO{rg(Spe(W7L1cx6#3FBrbVQJ3;BB!%u zYL1BAGIS?6p3by zvDnqAWg^PuXXuK07uP3&X7747BslsNC#AU^MhqUZ5ewovH`YNHZp`|e~v zBPeNqcO*1_--9BD))rM@=H^Uiwf4>K6P6bR!c>{im(nO~|C&=S=Hw63SP+0rLkbs*xXqm!RlXEIgtlt1vzH~vajmq%Qy&np1LZHNv!ratF!v5~D+-ti#*X3AC;AE2$tzI9b_F>~;&^OpCo-!4ch<-42=woGig=Y9)8SJCVZpmw zZ2C=2{QTiF8%OQoUX}qAe!52Fo^2CU*ac=ddOA9vTEKlNf?4MvHL>s0kL;1UthlIP zF|O&HfeKGd#XISC(ejsRd_7KAe9_{GS1cmn>7Ezxy4Hkt%v2B?eh*|DO5Vf$gXf6R z=Hsw;sEW{Cwi8x1UB~>R&O&9vXqL49JM)Z42D|W)La%quu)3lnJZe0Q3s;rkyW+9L z#=0B2gBEL8st1l^gIMh`E0WjO0(uomq2r-yAu^(t*qnY#>b9BS`Jfx5+r?BNdVvZ% z={bii*iuX841LXv9N#gapARv<+5=)5PUGaX)ofksF|c-6j}~tunR(biWa9HIQ#cB) zMdL`FNk0rye*%~B1iUnF6uus528)?|jb=zSzH_Y*o%!iZ-+l2DFW$BV$-(L9IPfTu zA5+iNqNOnCa!-7+-w@ZS48=7IRbly8Z=u3l8@p+57wPr9PExy1$8U0Dm_b+{(I~k^ zmDA>`!dtxwXnLrbog7!jin@Ek(WD+EvQq~f{`L%MH7gdazGcD&L>%VV8(*^>vmKXu z%fajOd>y*~08(M2NZQd4s3<#$J{+lmwoBD8q+1}|)7*&d(zBW35FdQ?Y(G<|Q4{+g zm6qd~h64LWG2rzHC2wyKni!3HD1MApM=o~MN?IxafZECvAdX9d?9A}*% zMhlDCh3Tb2a^Atp!B>mf(9X`J%h^0uclxU^cmGJ}D2^A`xb+ZwMjmEz^{QfD-v;Pc zGXORpv=zH$>r?CcyG;3~51PL*B@^toU{G!_S@dWov>wU<+vg6ra!UrD-?$zfUbjNu zIceXH+r^6GFX4_hTV%YCm+ zHZYPm?=ZPF-4IS+YamfC784I%ZdPo$#Rj@(!gh}zEriA!rO9zse>BJWU*u0P0?3T1uHddF>z`N6gnP6!R0%e_ze@zmAPWSiArLH{b|g@ zCl#~@KLBrqZam(-AF7m3!t7C##Qs+6N#B=uL8f&G8|v&Q?!Pn`Jt7yfr>6rEW-y$7 z)CQi;sABbp$K!h$MZtWVDSUP`z+TDIASOeOZjaxDmM+3HLSr(pcIQ@jI=HJi%Gw{IyuHO~aaC;LfD}~KN+7G|2;#%L*I}$j zI>gF$XCr0%(wTWobZqo{yj-A6AGj8ShT|CaRBg4WQhEdHnN>paZ1!V%d~dkhA->{h zZ&hfoX#|VYuCem1=Y$)VO2Bhc7_Mk379yWUVBV@uSej^y-EI{#$Hg8{SpS`sy&J() zG*X4J12RcK)=Ac{#0_s`mWb4!$1~}tN@M_!2del@+g171W_tVFDmW}gR+65@{Z+i- znvON8A7(0c_~M0cs`j$7!x6AJun+8!-HDgJ7_y9s$vFIeFpRw#2~GC1v7hD&u$)rL zmiJtZ$G1OYDspdOr;x)w4HyTNnHMp)>I7DFH^RkU!8qi-2bOMEq_g`oVy1bR3|`Qm zt#n$23Aa4Rt{huvoH&&YGPVXot@A9s{QxMb>VdBfD%kZnCpcDkmyGUVPxhp25Pc6& zfax=Lh)x_h%0@3xrk&^M!7bUoV#g1%;;Eg2A#9Hdz1(~rtG{)n!NXUt1DDzjp@d z%bl_K`BQc)(iGK27vBz!`b4*M&gy&3$D&w#BPLK5cB+PFkc3+V!#MG?J31;AtK>wh$=4N z@hMUZ#7z2L0{fWp5W7^K5yqdI1p~W}#|w&kiLY4_in(=B>BR^LHY^g&$w~pO{U6zf zVRn$Mo<^oOPiA@7R>6%!Zo*Wz1@M0MX5roLtL%XC2c~RkNahUO#FT|F;mL{<>{Qqp-%sRI9ASFmz_vq;X0$3EPi23kp< zg~DUYMAj!Y>6qzfP)I~b%Ic1hUCObG_6%q|Z$f6g)V-Bx{<7dO+eAq}=>&%;IIJm#~_^?cztfD}7PmTgN9=Eny(URKQpJCOv z9zi>gsgN-58p-A3Qy&NTv!=RTq)>d&IbCTP=-1E0YY9daR^1hK@s*_`=PSae`FyGE zuryMjodH(GcW|1|UUqYF1`IOk2eU8e2r-)v;l=c&=v6Zjjoo&NRMX`9aMyS;e)qeC{c~zq zFU_6s;L&L85uFRW)0Xmh5EF6p?qi?o#nV>7e)Q zf>=d-?xr*e>#NS56rIJSVa@pbXBRSTa94Es+zY&h+6o_c`>)DEd{h~!zUZ;q<&fG~Rgbc%h*S+OWOI+b zhr>;8$irZSnA=JyAFBe#0~g`+3wmPNJQcBiMi12Ss1ast9ZlDmZWZPc19A3AJv{g2 zD?GJ4E%Y8(K}PdfR{n2fAnI)+c|T|~%6C;4m)kvHL8|pEq}_OwaX&}A>Y5OG8bEVP z4&GQ-N_?4@c#K9Am=8OI4ed)RXH_YXj0wXFE9DwEmb4CgzX}3H zg(9-LmkSNsWe0wag=DScPS$;jG&tAFira;L!Ved2W5btKXkyyTo*f?}UNin08X7GE zQ^#WT{;?8k-WgCaxd(5D--Dm}0&O=ulPui2g*bj5OIpG=Lr7=?Ysz=V$A<&Sk5@y; zpz(c4RX=~2D%%HNv|bb0eykPc&vhdn3ghs@(EyCgUc#nTzaq!qK4->$>&P_qt|&K1 zS4dtxkVd^T5Dz%T`vW%Hu=)*6?48CU@!9#Y7*VwY?QGQ0NvObBogAX|wwR2%e+HQ0 zQjzw-dF*Sg7Tx8og7!lkuy@&72$_|P>mR%)X?Zwc0ns1URj#~QP&s6fH#aKiyNm%YIO69@E67yFQ_(({9Lrpe-J`ArN*w z{47i?jslMf32few*F?c<4c=)EWOrSR#9yC|hxe0x$?P8b;t#IXH|EH6%3kH7;R8vCe>G=@GB>IyU1_phGB*DV9$#0x8DUN{TOtj)wT%sYuwEl;p7 zPjuLGUp;zpmSPF zmP7jhU+rfdqcU)?&phn@UJkBcH6F0o0orPL_^4%N(=^W;Ne%KI!{jxtu! z=RE7~j;tILOVb-E_1A7kHPlP%FHr>pG?`GW8XRQ`~O;jblO`@?%qm-#vcE`^f zi^=BKlgMm;57z_5ovB6n2W-qc42#xbh1H~9GKhxprdfBV|B3(1P8Ui6C>o zOS54sy3+csqhX1b6&dAafxTU{aa-@3xa`JARGzpD-4xHTtA&vmb}Adc>{>1gd3uN3 z9WWRAr;dcu&OW&Qvoaovvw{aYQ-B;eF4Vp(BQxHYlMyyo$=QU>?9$;PxVKRQ7S5cE zO3zd2qzjkH?(Lh|BvX6*lsg}F&OC~cG9W%Ql zshInnEh=~cW6QSVgFG?0e6*5XtzN}$eHD@KPlrR{=dln~Rw2li-X^bfE~C6z3*2$b z7s@w|2ea?a>|For?5yemm>xX?hqog1DF|h1&d>12jLAfA@h)N03`^W~CkSC;D7&mS zo81q3L#&%Sx$0Et!vGg`kiF4goSkPY-j}RR_t=ex{nh@Uvt<&!n==iH=e8qD-iQR% z3q3IR$sP=Mkfj&p5MF;Dhj-oXku8~_Y|NF1tmdmWGSw51%H#imsUit_qc3v%vW=W7 zUxof9XUXN*@p$slBUtXIi@{?fuzJgEyuDf$zVbF!uB$FSJr>90x7n~)6PiTZf_I6I zX!w(t*WZd{mgX`0SSivwdL33rOsRZT7=p=x0k~^XIh%TA2(D8UGf3NtK1Z&wk!m7z z-`k7_+#Zs#9n{1d)=U<^opu}JN6d#=j_X<{SL}oWdOHv;b{Z|{H$2w!88omM+KDH(~W}C?XdmpC#C>+1K z-)8X_c>L4yEo{=Eox!EWgWR$6yA_uzrnRvN>>7(;B6u zpTj#_Q(P2cENBtZClGVhBGM*Cnw;WH>7?FMHO)lkRiELqa) zG1HG52;-hAp=b9|*tfbJmYwhck|Y=o->QdAo~*5)v1N)8=N3REc;T^dHKvt zw~pj|T?0?fb;jr^0(&D@3f;OHz`*zGaBN6C-B`U)9DYFqcApH!5zl_YlGSM}XQ(lp z(s@Nzc4dKHGw&ybFuVgG5E{A0=c74L~gb~?t5K! zmAz%j>}*0|eI*ctP_jxY6qd@NQ*hXEXzmNy>l(43k2^!wcz}^8j zA-(1^8|K2==ARQE)z% ziGhuu$>qM9Y?IqX`#gF!q^dbvv6n-%4Mu*Dl)B*0!=mwU9Pm&M1 zSJ<_sFR+7c8qTg9jQ6^p5|)<_#>KatTsNF>gJ<05xah$JHe_odbJey+HJvqV;aw?G z-?5#zyUudqq+$b1%=Z<$bdsk|Cig(cQHzS7Il&6=IAOG5Gasw?F4{hW$22zY4gJSP zvk%uV!)o61S~H{;)uO+_hJiQXW#c}4l%frL#R>R$`~jl7T8WH!rBC*)nopv3rn+n) zM_}x%k9ZvAv1Lyt4EA^~axy+iKIZt4)V|s@R#S?#{M>-y7n{j3?txn7GM#OhtwZKH zUuK>?^NDYOCCswlOi~Qyjc26~wOj0qn2=!lRpWar?D-FzuQfeB75ntgjZ5$;UoH zN>L&tn!I$qIcltUS?{B)EYJz6Kgo)_ba{&=PnA)-|4Wt<=z@2`G(@>uY{BeCB=&CE z&r)@Vl9Vbfc5?h@5|CtpPBxD!x7;%%BjOtw*c6b^EED|V|H$>pwlji9mJ03pz8%$m z+K3Aa3wRs51mgUpX=7)CmfzQa+t%Y?@Ij6~8nqlp>D7{Mvs;)-R5DI{um^jICz0m6 ziD2gU1HNw)vDM*IG0UYr-aO!okIp56mTEQ>H=QNv{>i8+KLIA{F2%fUp5(3A1cHiq z`E_9n9*cj-0{3?oTR7&yf-Ndc>-aj7d#8~+mA*_S`**=>cZWgRutD$$15h#OzU!SS zeIWGE9THOWOwhZ)eJimGy`H$2$7UFg->nS5ph1o#7f#0V`UL7IX9Vh(YQb9l8i^@i zihF-1v9lLfqJO0pv7c`Tr(QLZ(6V=s_hK{KnRZmvaC0|%F{6OVwBL>I%nOQXkT?;u@RU zNf-S6HwZ2+zGRbc0v>WpV_W*o!B2~Hp-@*72af9k^{1whJeDiudnnTmD|d*#?Nk-- zv+PQvIy&+gnPH+6Be%l2>q-p8H&S+_L931BwP-$ilCAS8{ ztnTN)HHNqGI%h*eR;@_JWUzRB;bKf3*PDBHzGHWkm%@xsmtfNzA80sO>tZ{+gZRqs zg%}g@ovbb_MA>DFvGZ6xNa;JDwEP@M9`qc*{*_k8-~E1n#-b#bGSF% zUSySr28linizic(TbNU$4V^VvMm%p<3wfd~3wrPbkc~}5V4F#-V@pG2m(LSj-7A8i z*vP)rg+cc&?S(fb(_N3Q9U>^alOaR&!r@tvh&ZHVkeQYf;CyL6IIU;P3I`P78sA#9 zvO7&aytRZn1rzEt(Lj93ERQ(I#fs;3OA_rJvI$bG7_PHc6VlUo{I=k(;tqYvfb?Du zn?9^XgU>na?PhbFsjMa5agveu2TqDEzC4Lz)UCkeP&F*5uVOPwOh`QUDvvsphE2vr z*iS15wDkAEOp|+fE(qDy`IRht?h2BAv5X{@28cTJ&?ZII(rl8QGi+lzuuQcZ9FR32 z7uru0T6<1|B)_39o5BXLHK&o3hVF+X{+n+tod;e)1z_f0h*L_^${TInr^eMmtV8~A<5*phebMs~;ouipkM{TK*|tNyVEDs#q}Y5pHoQA61UtLqL#4BV;Rh=W+^!7LQ@hb+ zlfIHgA6&(cJ==@xJK3YB^lpsT=kZFrZAO=zXW%y?8D~1=v8Lezh|57~aqjL|2*fmS z*H|vHU;P;x^4GxrTWMHQDJ90M=Xh+{cesC#G~AZi&4ey0Fy*s0(>?Z;m9~OtwL>bj zhVLY%iKdv?Vv5xnXUUkGO9ZLJd3-#t170+ogK|D4sMsP+?tkoqb;ULqG;%d0K6Hco z>dJ6+x)BaoHia}DxGpG}d1Evfkv{94=ws(c;5NG%cLp?r-clbr&de90@>j88Dak0` zxQ1;q9!S45=iJ7vl6maOskFEz7oF}fJD5`dfW7mT(iXL}X!LGxX3VYUj;Jgx3n7wNzz7daR_ktH- zz>r&_;ag4c$C?kU=G0ALO4&>tV6I7Knz}P(|5lN8KmlR6i^n ziTfP(5;qmviQ6fUhWsQi@rSKH@$z+j^jGy_CB2panJNpOMT5!SF%QV&kFq!+u@L%= z&S%ZpPw_^KWKTnVU2KD)(0lXZ6(L^4e;cb@%ZSY zJjg9b#$gKU;N`s=MBVm2D?DZo?dQ~ynVWly1HRlQ&R*_f?Q2)q+3AgA3=Iy{V`4Bcl{VM6wyMTBrY-1WY8Qg6@Gq)-2pu5uq zY_(}PdcV_k9K0#RHH+%;@r|EkxVIjz84>9kchbW}rl*&1a`h&1X@e{#nrMT~DF?7j z?hi|NKjoBz#^PQ!N?<>FFMO^tCk4u*L_a6zz_f%w2%eiH8m9UNrNw?|$35#!rcHnq zNj>0l&=!dC-GJ6w{!rjOh}8S-VYPP|6m7ggI?O*R@_(%*mV3ALs7U7k2=>dtY=AF@ zRD4B2rhq)zGXRtYSIEAaF6dWatD0M`UXvB7KJ zSNqhR&bHLX^zp9nL-rEt92^RZ((|#TB#a!KI0f{A=8L{8JOEW4UlZBE6Nr!M3_RM! z9{cvRN86+hVsU$4@M>=-_AL|R@c=XM>pF#Xut~=s{a51Kw1;HaekC@4?i+HmsI^kg zX(!YgU2}OhasnzoIRFS;>edb>`b-hW=zJ#u8`Q=6&U?}Lvzbts$LG)0 z?t^!{4?*f@3WR*#fF18%hpVF};bE^*JgzxZDBjK6`hH`vWcqzk?KwGkry9r7%$|@j zwjrPqsEb0a9v!q`D-K#K4eLbn;aIz4%(4Et@cD8Diz+-zw6YgK?}Thtv-u#K1s#!n zTF)Z9F0m}>5|Zqjz)o%6MGkd!z}Z(dA!PS6VqX89^yHIpW;Y--cVeucNNa5 zRAGu}6Z}lhg6r3-KyP3SIi|jhRj*Nk0QX{$O=!T)4m$K)<_h@1YfZV9Iik?(t3^!- z=6Jt<4O-3U3rmhK1gSyxtoTSMQq=%E5V6@ zD0)2M60?d(WdTi>G34M%=$*2W=r7>)nq}R{g#hFUmQK9~%oHIGFbb;IMi*=$;- ztawAcAg+Dz3KH78qWYx-I``{F;lVR!ad~JqcG&odJ@!}**`@1X{V-!3zfgF6?+OEv~;~&a@qa*v}<8#Bt{Wax`!s zrd$gKxoMW77Vi#sr&qv50=Ukl{Vj%P8wBH)5b6}a6`N9|^HdiB9e zSFNL^WTe+|e9z~~czf(b6IDQUYd7`*iooIZDBOiwXnxEQX4Nbd6>F%%W0hVYU0I4# z;#Pspl1n6F)MsIs&REbgSjWA4l~A;2EJk7wUi^O;IuC~$|0s+XO@%U&9VH+DF1RZ(14c$gYQ~GW#ocBvk;eH9C%XBNq{^18w6%E?1jNpk4CbCC5{tCy=+2r{z zn142Yz&my3ye}$<=T}#Y8XHsjO5;G@|6db5nfsL=l zn=EE^E_jZ4hCe{+hxQ+hSNw{(y{9@3@iR~y{`FFR@u?j@Uz`KA7vjO;K_WcXp2bb; zMxbk=FPXRY6V)=?Q^m7NdTWvn>Vcgxq5n-9B@~ILI%%S3)c|~dXAS1;iW0noJFwL_ z1JE7eAXuu&A$ag8_8)l?3#a`Nil+4x;+Dpt!`gbR4*f2D&c5h1yponU<)}y$8_^LY`IqW(DwJ$dgJpL*Xb2q)M}e84 z3b&U<&{^B5{I*{@eq4}>x_uf^^Wttw2u>9pM|WrcLn;chj>)j9-#7Le^H5y9G!(Gin9CU!_xyA=;b>B!~fQjl1>QSojH@ruEg=O2UeJA>?JOm!s5b5<0y4# zCfqV(i1dhHbv2P4+NI*4jI%tXRHkT}yMqredO;;!7Qm2u)9Av%^Vr(&7fwCRV5#~C z)<(CAGs{21wc|I~C&(WxQua}ARVA6peHm8`&w!A$TD%x`j(-@H(T=0`)V6#imu;#6 zlSj`fJv)K(Ycu3YxCdo$h>zsum>f~BZ9%Zh$ zH&u;(uG3?Ux6`CE(~NhtUjn}!`^aWoHsGsw)D(8ER*I(?2iWs&C-_*i1WOku;;tlL zT5Ej;-c%I8p`B}Z-6(5xRQku;o?U|zWzU2!yLX~mXct?-+6U?a3cz*JX1RP?HD!*| z^>(^udN1f?* z^E!+_)r0eIsqmb+C!p)ig`(q)G2)z@p17~a9+tbh)6a^*iWIF|7{2W`)`>4rXgEL) zlAhRjunW#nbQZfbo1x7(1$&1MWWA8yvcPU@x!q|avhZ5VPgYD3Gjd{inExG{v6UJM zxp$dpS1R(az#(*cLKjZn<-`dw5_hp9pZ#YfLinBCyr-zeHYN5BzYEq;Of%@ss-26u zwe}287F6Ls@2l}$*H+A;WzX9J`|B!{B zjo4o907)JDD+6SdX->Mw`dUk&OCNj4Q#uBUX6f8RsdMeXR@t%w?*+fBhatrTV?E`=un{beO3ZfpX-#D$5gdB7z{lwC*@ z@@7Osv&#uOl%|aRI;qLJ*<8WqN=f@^1=*}Hg8t8qNqzqg-kcF7dVUX*n7;M6c=Jts z65LMqE5StZUGkEI&pU)3e;!aqvYjG7Fqjj*8=_^8o@kn`!(VH^&>8!9Y3F;6mprvV z(M(@xzpN|IcRhoqZ=S(Lo!u(e= zpl-G@?iq4|*WQ$4%U^3*JMApaFg^;#mkyzI`gRJ<^uobX-_CJ)gLv)xYw_N+fpqfT zTidC9)n%VYIIs{EEslB~hRNTq@Wqt|ShdVV>i<5&CI&MoF6W2*Nm(^dOLwDX+Qtg4 zM;^2*^E_QC%Mtr_>%-*o8G|lo3(;n2WMk2b=ScJH#n`T#XJp2&@@ybQSrwD=3?W$B z%Q&ttgT-G~@KU1+(az9MzH_#;Lv;2M>yt0A($W@E+tyo_w0^X#_TmAwn;gJD%QCTN zv4tWIlw{MspQ7IxpIMRVi<12y;c@~ty{`tkG{f0;}m}PX9{a2Byi^kQolw( z2$vr#;_`E0ko4^s$k&eM;$|(pAGLt^k%mIT$_lHVb}u}9xRfGAf7DZ5#@frLQ22Fc zHeIPnd}1!gy2ZkcoC|EHJP^4uNIb4yF4}5L#)@s>)U82Iw&(P4{9ZfjK5j|8Ej@$0 z>y=RdYFAn7^~JTT_v48_2w_Clhj7uK!0$XVT#@$@1s#rE)x zvWrHODfRtc7_vbVIu@$)ijIY}LCD1HYJEKEl*uN0VtI>7HSXR&N!Gr-K(FeyV`;%g zZjMNxoU4a%^wuUWjB2Ettr;{YbOBng@Br;+y5g!W6X=e0S0UP0SJHuUD6d6geP$yFt-0fYEfpLA2@BO3AnS%c55OhsHlUV5eE1`BQE`PD>F5%2|eM zl6-Jn(g#uQo{15WeewGXa|-Y5iuoyVtiDQxum31U%`x9#f7vJ;FgF=Jf12|{lNB`g z)j{5VvnSYPZ)5Y!e5%*)jFow2s9f7i7PKZ=eB`IXSDKtLsMV4$pS=r48cta4)8Dp} zYb4q|l~{78JrMXHg{zjXriA84wm$7p*mWOZ&;5?@xjcnWu4t7j{q?2&wMMMdP>43C z-edUm^LS*b3LjGbh?9Txff7w6{%5YN*j4f$giT$Im#sR;J}rESGmafWN9)6QyR{P@ zNQjb6g6*hbeT)u88>8~CSh7NJ9kq<*eHuNJ((b;LKCsPmi}@A*KK?=xG?f`w@B{svSv zp3~1BJU$yYv z-=8*TM2M4OC-A{1i`dg<9uJo8_(x|gLSb#Ku){k63-yNbOXoUa=?E__y?hWZj-G_O zOb1Im{;;`pmY1R}&MaLDJcyD(X9^T}2$4xE@xEk6?aPS8R=nl0ScYo-J!HvX04W zoD`o5(@$Q32sc}venSn_Uw#MWxg zUcyOVFBe{x%||%36|P)f2@juF(anORaHnmh(6Q!@uxUfO(ATPpW*NBgOy@y3$mkN2 zu?%NcH^G4^exOmES*SJjFtkn8!(7czLe(Z;*dKHQ96S0_b8dTvF}HZq-KC11i=^LJ zpRTgF)|2>Tgdf@{HKSRL#QAQ?!XWoL@ng{v4C((LzMFkr{_~a@nO+${??*Z)RtFDI zlujBb3k&F_*il=}fp(+$*NS_jd3h=cC6clGpFyww2vk{(0nR;T{GpW8E+}@ z@DCn#cOu4GA>MiI#9Iq>Flk{|Mbo?}Ha<)^zpX&nwXd8eEv&$)-K}iazh6Z))-|YS zT*(7+J$d^6HgM5aS8O=h3Y8|dI6%-BMsyEj#SdMMewaq3S~;M<$dOl%mQ&Zb0_c8i zKj{_r;-#aH(q@xsXsG=uIUF7Gr{MVMm#}~P2Ab^K2w)gXFC(46Y)BGRhWJ3Wy&c7=r}5SG zL!dV9vpCwe00Lh2=b_WTL*K_)@XWnNn5FkcXc=eCldTHjkLF5Hn%7q`FV5KZzRiuEX4s)wK5HR5Y_*C^0#E*)y(*w&bmV$lngKzN<oSngGM+n}O ztMhj6i#)B}B;4ulD9s4PXy6wi)^})v2EB0{QsD@`aj|&3Tul*Dbb^g!O;R89adEkN zgpht|Cu^=u;WZ|2;DLWsOrfOF)0LB zCi=iU?X%#1ZMv|TUqOrYKt<*hL#g+>4jqmhql%rq#!5J1SIHICW$P*Uf@Tg_*Yvx_lUZ_e= zBQvltA&^5mgtBs%R7#%o5|n(hv3Ui-!zNR_bl;20+Cn%Xem}qZd>$g+*Fg6f_vtTX zu*0x6=;JN!9?Kl~_Q;-KvdkElTb_e{Q#561_CdU}sELpM$iaqFl8(CJ1@5uh zjrlLEMY(kqN_R^>lj%+!cAdqY!MeDue=?Q_x#7i`&Uk9NKhAyGLH76AX$U>k6S}?` z5A8$uL+DFmp8TZ?`}EpN3nom&@|At?=d~o*{wGA-*qj5)=_l?OUPJaJhak4gYOc!A zpiMubxnt5EDsCPI+R9yJ{(&hNlA$jfkv2n-kg2Tj%Gj#Nx#=$3CwGGr6OV z3xBckw=oJ)OJZe?jN*9p6X-kdH@si=1oZthNUdryKl{{){L*IA>GCp6)U1Nk^gnV# zi^Z(8zCz4Bxj<~G%PdqmVk#6R@8AX5CgOtWz4*b#Wuk}YK^)tEGCz;?2LHt(t@*D% zrO-*(J=Y(P&dGtwtw(8Vq&Dh2a%9<@!!+;GX^vGHhfge&(7)t1**~>Zd>{6e*Y;a3 z8*%p&?sG4tGq)0XP5xefx;a!jcgy*J{UKVQ+>;C3&f=d*Z4|5B2%kLD}|`GU!$v$9;S1fyYH~;EqPDo1QELb$-A* z?SAl*;&52XHW8xOx`}-O^8E&tbUfGeOlh#u4z(`JgnnS%Z*7d+trAo;vHX9#q-VbCO`dL8Lh&kv13qD%>~9OBjFTh;U=#RPu7rbLlgQ^U0;OC{qg1`7K;+w}ld~M}g^1HSD}W6E~^NrrZAg`L)M# zY&7tcN8XzTi^gxIMSV}v;s+;i`fv?g;xbk2IbhnA};O~P7MeA>0kXS81IygL&uHBZqHmWe3l6>8B)f=-g??F z`4Blw@|9n&FT_g4JMp})HLf4&j5F3}QMt!M!M-sYnwEE=~Y@UkWGi9ia}xVlx@Ao!i<{1xLUU_2T#^k1U;C6B}*e@6AvAv787&+(iFjC z=QO~Fl_$BvP4W{AI>8R_92Fxs?3J$%>Wrh(r$fu|mvHvZNVM3drtt0Gh(B9C!$TJ* zeE8zLFlostF;DF^IO?6XP2c9r$^Dkd-9NsDqOa%B`1(q&#CWlLuTaWsZIMrK`3A?^ z?}an^)i5_`I;7opCDQyrbq{M{ax@DA>ZQ)|-cuA4DpeGaSC0L-)2>Zz4+2R z9^Ee-#N-!+U{j|7Ci>EhC+#NGmncwP+KwNtGsF>_CEjDkHh!)Y#hXtbfi9`X$Y|kE zyjXkFRyOE1J=|`N`&=FHe!Z*w?4DeBW4ys;K@SBTce?>Egty|s*`|ERY&WgZ@#4$v zddof@FpzEkw1@4_tznDGjX1+Mm;al51r4n|WpC~cLd$oivf0;E6l;Z@!Y@B}u|@X= z?J7ye^IqR@W6EyZlH`0mG?F;@Yzl_eF5){DQE2f?l|KgN@acPXXt!DwCVWnlbj8zj zwbycR-v$srvnNMR>WgD%9cEwOE|_6!Nb`D`^YO)}`CiXns9~WiR%dIAJ~K_kivtH> za;Kj1%3s0!>KcezUB>cH?W>nYIObhxRQFq%L5M)_v9nn7&|vtv zYEGqh6gF+8vLAbDjaHk^G##mTQAygdVCyy%Ddb9T83DjF8Py zZZDhi&k3JgH(^ahnRL%+q2qD(T&Xo!vEfX-teuyc!rh%tsy6u~g^nE^8FaAo; z8;;Z2&GxL)>k!6XF2vQ3fFG(yQ}HPi8^0l=xw7r8d|L1&?DAp?L)ZoC?HWg$7NubA z&JM!X(imzTxe3Y-$|1bC1aj>@7G~!klsY^&D(dyLWMzB%$xN62Mdj;>a5h3Fvl$wK zYJG~(MYW1N9v0x%m-=*CcZe`+f}tXHNEpUtOva#!SkRao1%t*Nz;M+vTeV*fwEeM~ z?5?J&p~%Ing;5!g=#F}-I5c`ErbLgz#+?`G`ls&9 z+l;vT$0wqhojpIZIwPhwW#JsRanR-(%0Z`{LE&=&Je+i}dT|zfO)-X<3&zkXiBCI_ z)r_9~=EF1Nb~5KxU(vtnM9(qdyzq5w+EPp}btkPqoDMC1JLquE#j=#BGdy zpN++FU1{s)y=ZvZkWaq&3R{-^qL}1Vw#?1p$NoOx`n=k9<-FP4Fl7i{>uJDaTcU;K z4Oy&wz>DX_+!Mz2nZoY9)N$^aX8HCIYsH_8KYT1Y8hULqV)u}bPbB2n|&Y+CJ^!pqACQkQ>2 z*>cqad_7_@4|MAd_q4iTOr{RBI^Lv!uU|laY9j5w6hR}`E$7P(?a?>Jl+5k@sZ7sA zu{QmFE%gsz!X(A*@I<|>Fv7HTod&IdiGADb}?&3-@Q37e&9FA9+L~Y-#hbt zpV#oTaTs*-)T9d&q#d6Zqq^UIAz;7+-1azv68gu(m&nyrF?Bf&R@B)3DLswDHR^>0 z+mSabx5xSIjzPleGH5kjEoP*x#gwhy@V(#;&br(|R@eJ07N~TVxg{G&c@z&(J@^mo zh-K81tw*P+cj@S(x9oL)6x?3akFPF&hjSi@_{w)MUkh;Je%_b)RgbB%+?u2O-@Bt& zbnqvA98!(jPuTF%S_KFzKS4)4i$<&avVZ3eBDu{chuomT;?M4Q_Ppf%I(Hwf+Mnh( z3qNDVg9^b=`3%|>r;Babw}keMDY)^F6?=wEK$XO6uvPa6_0x$EK5d*Pns>IM=$tko zcvcH8pSM8PH#UJU{h2OvcwNM5OYU-d^I!%VzhynU zMHGp9HF7!sz7sl+bH>6w@31IhwcyZqi==OKmGX1#d1JH_J`mb*L(^~^<9S~=qZ$Fd z-*>?)gYJTH+eVTu`V{p5IPgV(C~g|ejt`@e`p@U|ZFgXk%>w$h?I%r}rDPL&aXN*2 zXW^X}%jx!?4j7TMh${>nc&UGpP^}pYR_pZ8qumDDzv@5n*YH>LGdr9g2Y8d`pFrCV z`wJ*FsXu>lCK#|Y1ytUz6DO9)X{~lgKA|=P7tc?Fw;!>g&4l-c*Y}>SmyK^fef~ z_9QG&9Zg!>CgO=h8rZ$imE({1rS;dUsV3hDBeN&q+&A??W}^doYL*G9PKQAMUI)53 zzoV$sa0%8;{Rlpfq`a1CZ{#mZhTv1pBy^wNNG7UNpuXg-IA47?9NHx9{XQk@g2M(d;lqTvDmBb-|SE;mKmo1vdv3kQXQ0bXZoj09jMSC6b^`D(EAqCia znm@dnV+Z#$Kf{gjVNjUzo(8SG3Qua)+5PcuxaG3}F9%L$MNR-$%ynhI*PSrZQw8rX zG=yhknxR9{G>A?7L7n!^fnBxgxWx4vEZi1@${XjildBc|eW%2i=QhG_-Lagp(H*}{ z87677Yan-&GN&x8qtG+)F!bdx?$WUmZdhI*Wy@&Ouuu&!SOvXu={wt`ECI(ndc128tufRmu5(_tRoGYR3QFU{GesC_Ti#C z>v`VgjlE3!%3RPY!KLi)v z{ehoJ-u&fBAIzC_%C@Phg?2Rz12{2)X9u0+6lkE_KcgjIZ6Hp2vXcxV{|YANYiwuK z71)eAphHHHh^ccvisz;CyH3iNxSqa$wkh*W-GSV+ z)g8Sbr$Nr5}W-VNZB9OposOx&iB6v^S=d1to$WV@vz0$@kQ8LU<54rn=X-M*9=DpUe`f@KU30e=TA8X)>*wwb#;*PK)ccPH7G!LFWQ{z#a;!)#w zD$i9Fu*P93cRjZr%*Uw7^M4P*srzbZl)MGjcD2B}^8mN1&k)oW>=W#Vlc*n~lK$ zzgFygX)09i`vaSL4@couJ3Q`PL-iex(72GXl1}hU$Q(9|7f=2Gy?@N$W62Zgyji4> z<8fM4$4|nHUr|u1d5+TLPhhF95?gFs0V6-xQjfFtxOM$T$Z;;F`Puo9)6^A@d27H4 zZzrxYJ4p-M3?WB#4jarQtXjLq_RU5!7?9;Bw2mIcuUew`zhm?0`<9WgtZFN7ty1D+ zD}#~d43+=+(Z8WHD)lqr@lIjn zWT-?pR@tKYu&Xp{ejZi)I0wadPl5aEKr&u%({{}K<5c^$JDYa06kcpS0gESU@Y3=9 zd2km=&%GIfioZJOGHiut>{LT%*PFpB?KO1yLYQsL!bE!bXb$R?i9*<|aPD^76O0nP z(6Hz`O{UBJa1U}e6MG!5Hq zLUSD`b$}r>u8qR;d;f{U6mNyOPEHsi%@1}iyQTYwFIH&u7d|}eA%2+94=21;=N)V2 z$X9!;!?K5x-{hYP9o_Sq7VWbY?Eem@^ww#lzRI1A|0ak!sUxs4rH4>45mootPQ9o2kxQ{I#+x3)o_?x0{=yhuGp&U7W-kLXgPU-|V;KAI`v@Cm z9HoE?Yc9Vx8mCuH6WVPKhWNgE;uqf~Xt~pwQ%u8Qj!_3J+;j&f=<9OUtDd;_k1~Aw zcUrJ1?+V2s2_RTq1(RYuEE#w}m_O_iS4QdaxX8gUve{I0STvR|RX5TRDLiNLnp=X7 zX#zj4xrA;P--#a7gALT$@p6^b@O8aAnymEZtp3Ao{U6oZE;lH%)h<$D9Rp)t7n>+O zSFQ5&38l1c;7@8_vl}N)2@z-h9xlF(P(zoUf8}fY)>5#oEo?T@0}anCI{nfSt%Htn zr&AH|_Sh46I)&vvG4t`7$q-mzn#@}sq=KsQOQ>8l5-+4I$Av3aidVn_YA$S-a&@DpcZF%sm+{~1TpBuRDsLT7NJqaK@g?sJNHoD|0h(HZ}q zTn0-Y?GuK048)Gg4}@M@35p!2!1Zk?e4TxO)?TZC@`2%WDeo72aN5YqF5}qSY9`&d zRWCkxnuk+Qro+v@p}6c-FWNMvJIkNh!iov^z+vhTnsvKBr0+EbWhvX%PfrbxN%Pyg z^V(A0izZ|*&%;5MdN}WV0&Z`o#QA$VazbexbU;^yZ}=4E;S`a0}JN@!Zaj@G)Ja;cI@=J>TDO#mNyH zbu&?=i#^!idI9=p`tdd0G*~(_AIgU;gGslg-M}Ynw11+;cW1ew%yKPgNnXmT!EJ@A zKeTwr#00n>m4#mudcp^r;cWTxgW&#e3}#L^0aFX+g3MlIuoD=r_aK)}F08-26Z*aAfLl=s6Vhc|;ZC^A^(yola0D);4uV>4zlK(`GB_f80q-S5xDfRVO9(w>@4VH{SQhQXJQ|Pd;kxFB->l zd0dqhyJ-c{UbTa$)1+pj`fViyY(Fj>nE6IXWE=c^q@0z$t)*QCM`=Qu4-bY{;J+ZA zM`T&hq8@rw5_Jc{k`v`4G!vomY!~bsBk_-W=g};4U%K-$2OdTa6=Li!3-QCX`iLJh23d$Du$m{kdviWF+ zkNwAikN!TI_Uni6r^{#xeyUBr?(-;UcOq4HIm-i=PeSc3CE`TG1Ze!)3fc43aP*8+ z^d5GT@<%1$Ubole{U?$9t!obefGUSYwdekMG{KekXQ zzu5Ly_%=@R*lz>DBX~nfJ6>v;2>w0ZP(OW5Fn&G`Ppu!y&CgGZ4R>sKp`8jp5EZ0X zk_NYyx=;^;c^JEI4i>!qCoHQO#T$GpiLze93(Mcan$%JZaw2lrA?#AgOZ;NL(?xGJHHF%M=9$j(?vnraUQ6+b83{N4n|2fUE0AAA9=rBA_n)Gj&| zF&DbjwFw;98^=5=q`Fn(x$c}d*1wE_O%~diY%Jpm=kAML;;Lz0%z6xyGVhuP-KFwP zv6$@=h@OT~RAe$ttQug8p7q z9gx$lMJHg{SPOQkoR9etS>##N1NLp0D0)@Q!Yio{$tp6COr4E6H^$G_eBv;C?AQ%^ zOt9v;L#hQ$n@kuG5{4~}k-J{Li+avMXl&HN{SMo>JWMEMe=_ zDhg7I;{lx|FZ-AWcr4;7^`G=onCR$7zyIXWghgY7(`7@2)dzdv^`G_PDyLDx>;VQi zIqEZ*&e!DOY3gFG>OlE|@pko^E{s$>aFS^@CVvRc~?J$J*Ge59BEx zg~G?N=9o{DscMH2g?*mGt>)aJZ3%AOCct0SVdI6t_prSqj6q*%hDGe!k1#myHr>? z#GcE>&*$~4f}yXb1L~zU3pG~r;dZ_Tp8e^{|F$9hvQM|Y?UgI+8ghtkE{PTjCfW1s z1WkI|eFs0E_z?PrPrx}VGlkrr&XTXP4FZ0daoOQT*tm=r)I0{I zHDjSTpbwepj1ylQo)H~p6_LTTd*qXIhjtD>3wjk7$ir+Sd>k-N(x<)UTdmaal%YB* zJs6FHW_d^*4>yIuT0Qt-_(!Vm(~FO7&%kMDMx9g*@sPP1POMS^cfUf)_&b=(0ycx` zAQcP`JI>3Wml38qV8H6{5Nn_emm~Jt4vKSzmbrtdW^6ycJ>P@hEXx9|Ff}&FI)zZs zM5me_lhgMnJXI@Flk3aT?za*SQc{RR+oA=hekpLm)&O@z z#lyK#4K$_Ahx8kDv13?I7;mzaU2q0m%N<1y@+`V-Kg%YzqjU~0x`;X3KM7+#U4TFN zn`oQa8R+V+!C`ixv{X@quIudhsHAU&{nWr81y!UIs?4LMth?C~%Uj-JM^DpbXz{m# zmJVvq%KuFPd3ZRDJwK2#4!6mb#y3-SrnIwfT!2>(JCMrjV1BUD66apI4AWh%L0fMP z(P`HXl#WYMtQcYIy-sH1t!9Y7VsF9ak|HwCTZB+(joE5z`QY74UWB~3ovBrAc?qV^z}beiY}}o#NRc*W_6})p)0-J*U0CjDw%`5ggTb3QpDo zSYzy9UZPnqdc?O<_A)*G=(C6h1R9AA+DG81feF30=#6RTnqc;cH^RlzmAv;+M`3-S zHa@q{5RyWcpvtZ*STiC++V7r|ziMJy(qSl_-K))Z8$>!A*b3uT7zut_dC)T;#S2O1$SusnE3O_Ly#vN2*H?lFZ`|bzMo+>gOlzYY)-; z$QEJp=z5_w){ft7G-b!W&Umt{jH(uAk@K~IXnhCJB2b%s2M1t7Nt4)dwzg+}QWV(^oRWvOL&K;~ zyb5LvyDiLCi-anZQ>^gT#iWQB%7|`;H9D5GbyU2NGD?GT4b^$h*(;Q{!~_=|-Gf%v zu4ps!F1W4Q#kF^T$-RtvutiNc`HT&O7d}&P^ML83eE+tj)lI}b?U&N%r88i!RT7UZ z*hca#64zPii3^1x5FKcSZ;xckFS{z?!I&hfT%1p_EmOIBNFaU~<4dE%Z_ox3pCcA~AQFE`C21N`0)- zDcWDkCq6Hr+oQkaG%i5g5vhcug0etLm<82q*TjO>ZM^PhAMRNfgXen2(4QUdJg2Hk z42#gk0ry7W!_9r*iB>Kpu1kiJMe$@}ZAb@P9*h1jMo9U=MYuT4n)dPpis;dW!@nlM zwM{v~yaVH)i~Bvfzt3_2+hz19G#Nvse2udD&hWnb8CpAQJ$K(4Oa?2S5bqRewoiNb z{9+T9|2rganJ#f*l2>2LFIo)zyI$-Sr^*HGheMyT&3wA-xZF5DPc-t}107bFqVC?U zG{ieq3~2J?rfy?GGa-FSck)GH`Rw<}&t-3A}Zzrv&gs_1rlHX0=QVDsFo zv?Fsq4{G^sn;>Oz%~1bCRiD=KPvuy$+|)(tDA2?1T|y}-^C5(LnSuSF8}Ku$FAsl} ziN1oAWgYcU{wwrEq1KntIP+f#xzxpT={aX9AH4z=&R8wJT6G7iT<=juMl_^YyUN>D z4Ir`ZD3qLyA>I67)T%#=y_$^B{`v*@;&-2>Y?HiET8V) zyRmy!7>+(4A+KFk1Nwy_G^6+iq|LG7A_o;Jd^|(wG5Q2l{_v(R-wuG?jWAxZ=RGyY zzEefrgzX<`&(cu!>XEa$ghm#}A{p(=p=s#le^}@Sm_hDgtC} zmSVq0J$c-bc-w(J#^7t~p%{#7v7nZ*q|-g&--l#q@;)V=S-**bo*km|`}KrpH)e2T z$Km*OcqwJLO`!`n>|v36KKPdRmGqV#)bOh-epvO6wg&$d`s_a-e0L0_7rM@5HRvc_ zeRo87(xQYrq#5D$;9&R?q>n0R&Vs(rc(|hOh|cNfB+pTT5Z$(by=^zaryZ%NyuyH8 z{>4JC@JXB`^u#+m{V6^`3x}+%g_|Gm3&ZR5F|zMNadFcDTz7LH_$!A9b82Sr+%E(l zyn}JuoQ^o-fS25SnJuhsvBabE)DhP8!dD-<@cZ8p!X^7eK~s>?zjIYE__76V+2p{L zH}`U~AnmUx86vLlC9gp(^4gGKUbAs4XAS=(^lE2G3)<(>N*(EM({qI(9Rgw2pD%)~ zk0TZo=i}$$I(TrPn_xHKF7=w02xkV=LfMZal=oAe<3W=r&a4*SUN{Gb_2Q`^>pMjo z$>GPucT|};f=f19!qnA_UnM_7yg@a*SsFv4W4BV}q z#M9LSXk+C|Tk~~FxWefks9d`S$`1|+T^-6OEJ6;00&C#HpO=(=JYComw@DbQ6(Cg3 z%(rcMaukvz&*jei+fZz>jt5w*rTYVO@!$4dXeViMi=~{EUd7uWQ>(pTf2ju!x|xhW zmn4ZheXD6ry&Ghuc%q8sC7yNPg0l~GME8L!#9PaiVa>EaX#H_o2>iPh-rZTlW8?zv zjNK`$+jju&#=jQ4944a5#plpl7Q=^cmW$>i{%`L*z@#X)JwCfj4wAD#!xAN<61Qub z90eo`D6=d`kR(Ykz<>z}0s;!CIHD*B3Ic-2&N?cB0xGBk13IE2f&mjI#CLvGT`R7a z>wEXg`)=6zzW>&APuIy+b?S7_>=ka6ZZ~B#9#keeVA6nS<6Em{Jd*HSbo-TQ8S@jn zr?k}<~!;{=e(92of(_% z&i{UIdfwIf)pOCtcMejI&hO|guU?&|dSiw9J}kH)HJp1(L_$;b`S zDwpnxc97p`-#lo!yF-5TxBS$i>hq~P(tED1?XJA|YSm-S1JUa@ZiqI0bfbGXapkt1 z-v=_fEzeM`r*3e!4tP$D^X`e>_i80|?fhibeq>X3#lVso3k!D6*pm{Ax-)vIO6@AT zo!t^@(U^kKjo<#9KINSSO)hGk5dG%sL=~Q~Hs!0nMcoH#H*oi+-K~DAv9{@b;alC} zOWJ1S`LJ?yhSMjb${nrKx14z?`n1%vW*?}hwhim9IxMIdT~T9gM!{zva<`;T%Q(6t z9Br7Rty=BL_cfQVh<;k}ay4zkUiZS$RZ@1R-lSIlINrUpc_sIuVMWzvJ8NfbYPLA# zb9pvw_QQQMPK*m7y>{w7s_(8B z+}+^=?#PNKqgS>_$Vix6G^6N>+|kmL$GiD@r@5~z81MeNrjvTON!{qL45QDwZA@{bJCk?(!i|yI)rQIQ{3MW1`=Ga(BwqWy3Olc&fRoJ-e7n{9sl3 zTMsQyukd7Dm22Zu84nbbb;WTqnpJ-~oi`9XBWJPTV>={mXu78K2f~?&g1U zc1A4U1@b#^^1mQL`Q7BN>ZcF-Y;4Ag$8T4)a=jnj)Oc0&>7#eMg|1)iZf(>hWyhHr z@_TMuQkKbYyS-iQ#q=#t)>O+TwRYF6o#!t4wuc(?$u{-v@@neG?iWT2U;J>i*o%jo zG;F-Yol^4~w_Dz8qfeyu$>{L76RmRQRrR>q;HDpaJ^hg>Z#RGKVlU(CH!`AKt4)s9 zd$CY@p0qCRJ;e*9SI9Rc`c?HuQYuz>)xGJ$rtZPsx2cog?Mz?Xr9#Fn{q{r;mH9P# z*|uhCQ==*wl|KJCJ!9btcj$sA+-Gl0ik4b_!2R*Q@1x%)ER9~aC6DUdS)~uSCy+6` z`I_`og(qaZ@_ZzEPn##@e`n5WrwQ&m5ARH`v2j^?jiW!gw-y0<;2ROO`u+yi;!K4GV%jGJeT%UJSeAmxL@Z88?`?;lN(di~v%Z^%em7gl|9 zeU)C|pt1f9-DiY4x^*++RlDzIn1cIB8wV zvHe|C-GRl_;bvX7wQJS2v;I$`{=a!P|8Nx)Yt9kv#$Jn{ZpY$twj()}b^I7#T{)g+0ol9gnZ~b2~%X#bnQd!R1>+S!S z&Z(yxkso0Xp9pURXcLDo7%);fp1+U|G%%zg=hTz@lu50y5QhWP~kEPlI{HP_p5@MnCpKmX6NK-Tzm zeD#|6^P1ImaO#9q-VYxwl^?05Njr9e)H7||HnA7}FWVkny0%V`sh-s< z7}D6%(^n?Qy3fnj|7|;1pOt#Q-zG=}PG9L8KEd~#?LXNs(Ai}&Nj~S&B2F-q-zE72 zm;CDAP{=7~`8+WxQB%z0(@RQBTvE>PfTVT1TshO1pjXBSeJ<(9t9Z@q z`uDHP91o-a{&j}hzyDv>rv!-v8#lI%*@ANx6<37+T?In*(UHXhxmTj%N#IXISoAEsOH+xv9u$agq6?~VUPx8a}8 z=r**xNlwm(SN71YbEvOwy`M&!;}<5H<6=)4eyy%E{OX_4ZMbgG(7B|uZX?w`(QW+F zFLdiQJ*nGRo1b(WYMG$vg|109^!GM0^jo*kZQ$0{rfpWk@VO(m?iZffRkx8cqjc*W z9b?Xav{JV*cdz+;#zAxZTzx~oR8Y5}dqTR6Ot{G$e}A+2eCLz84d>~q`NcjSX4(OZ zbsNd)+x3>i`Z(O{L)|)!Pn+Xzt&JRIA2577KVjrp@RV-7r7!8$8NbW4C#x8KLxv|} zckj#!-3FGP)@@{B4t-rbC6{i)V+xz&(pTy>)@y}neLjIX$INv<{-9g${u-Koe6MMp zRZr;SSnyiyzrff*nos!F1E!tzu5LqblsEcZT~D`O>!3M4a)UXZHr2Fu&Nt^vKBe2( z?rpk_cYDcvKJry_9F1vy@#n+(IK1Nx^Led_M&2u~({13A#k!63xLdbQ)WmD-#H;4> z-A0e_(V9lDRTt^jS$&Caz4U&%jm-VP*l*|@!>@G-L+{zLx((Md@fjXy#)b3wTju-= zAL!OwR@Ts)SWCCzD;t=0Pb=NVK1?|kxx($8*xqsc)<~o1;#Tqoz{9*%s zHT)|XyT+bwtFH^c7&rY(e5%{n=;NmSq^Pk|8#BH`6H|=++IQD&tnh8R4Xv1?Tkm6~ z?H7M}sBU9J(=@;MTaMv#-E`f?SKe%n7w$9d-FbEY@MWb;+xSA=hQ4g7+xQkUuX*bR zo6mnS{)=yT%kY_hM7QCbMKnF{#}{-PJN=3|{@^{s$Hqy(=;PGtXz0B^T(^OT4PCFx z9p?NMOLQA+yxJVM`AWx&SM+kt&-yd+`Bhra@Rj~J^T$_U>RtMLJ(XChU8PlG8*|g1f>o)AqJ5KTHnyy!=%kSmx@wDMv*vt>%iDh)Zcx0UE@B1~d zW~M$4T`^y`f&9x&+uO{ap_RrTp)36P&bMcLw3$ajcip0|kG=ksZX+X1Jjbhjr;nX? zj9=ni%zPgzy~X&Y(P87K(w!OyIr)ldKJg38{O|d36pC~)=eKv&t+Ql=ZoU4WnmBo~sG*;{ zSGU$*-Y{do_`$h`-|WY9>s5PNw}G3F=r&%;tjB@5r_FVj2Mqm$9J-BtUqQE#9~NtV znS2}g^ZPfIb-(bfGj!`k&A7Akr*rGahHk=mL&slVBduqe;{*5T)_ZlSZsUh{nD+I6 z(dRWYzl2AuGsii<)NO3)ce-^lN*ex;nK*Smt*(z_kv2x25hHZ#R5$a3T~ET-E;jw_ zJn>j(W6xz9jUWB_C7geR;rI1E!_QyW1NAP}{R6KrF!aNZ=r+=Hy=kA1>ekK^fxFB+ zk(vM8@Ppy=)KE<~KJ0PbhOHjADAT{4S(gH{`|D$G=Xl+QLuNgUbTQ+=b4*_0~>?^HMKFLMK4Kwb}htm7tf z{cg=C?$6gYFAS_Js_BLOyfe^#pgwkXn(-N4daFK;9lp(6*DOP~cHRv2G3#@n)2*6M z=;l?X^~Xh|Sy|0D(6_4L@6X%us}Jhq&`y(YgkK-3>3U7gdf;4W{2Ckdg+3qOc~ZB5 zTMBCZ;-wnu{^6b*%;#%2n|6zd-^i)L=DKbEI_~EU@x5kW;9O+NNAIJT18pw0KmM=DHk8UHCPny=xvtqmb zb*pE2qt~m~=r%TGnr=gVpV#YqXl`NC&(HI0ejM=gChzO+M(^*87 z<{Lg%LAQaGW?l_F*gzkL=9%#vpU};G?vDq@8E*K#aX`0@KaY7mI_T@1?@YcL7(7}Z z$Lnr2{qH@Z+syGF|41K4ikbD?+i2FKP^ga9FYfOj!v4A&_V-I+e}7{4VWIcF*K)+_ znfy0iq`9UWtKQS_^~Z>hn(DHRig-kDKf4cu4p2uFeJZd25eYJ~N-%{dD-S z>F12xqpyohe_glXi;O?*`WwE_=ojd1#(TJW53QfgcVi=$m^d9)PU{iy_Y2O*7tQf> z<433WN9H)6$@?p~^PVFzyuY)}y+`%<=7J zA7twZ;rx%Ae$SWDpU0oRM7Mz<%}l>_O1Dngj^?;XC*68w`sg;iV~B18iF0%tU$ek` zUg|z`ep5~D9~(FEQpfeN*K(eTpM{U%+DJmyZY$!fhSC!9>37! zHFo@k>pZ1D_ZFM^%o(-X#KZGX7(V_y97_99A3M`eo7Ugg+PvT9+ws1}zac+PL-|i> z`mrTH>o(%&5tdJU-w<%8ePMDDbmV&7#*dEB_Ot7Z-S349n*CNF+|0=N?O@aT`3NYX0G9 zH6zCiQ>XBTn|*FzTT2rUb=v9Hj+aQ?7Mfn5>rJ|~^F++wpW8en^g~_UFYvCv&ek>K zI38H8&xh|Y>#g_F27MffM-89KCZCV_uK}r_^5k~-rmDT z-j!>0>jhubZKS{<^Z6%-b!+$Uq5DnV9_atMj*Hk}vwj6mH`3PyW^6b8{dFexW*(#G zeWu3pv~ zj=yK}lR(O|=6I6HpJP4DxR3ArK%e(I6)jtyVXXb@|UDWR5 z<8xj!_BnQerWcrBNVg$>UlyA0=hYRAU&EE|G@s8fa@zf0W*zO7Rll>J6EJyRtkeN> zzHWZq-|_3Wkv4w4rGql(H<>&=RHCCf_V=~nhZpJNaIa^zesO;v7^+uV>lwaiif$wR z{1Wo_p^>Nk{M%m_;(mS;iCv`SiT~I`w_$(Xitn0ep-j|st`Qy|{zEab-d4Kq^G3NM=3A%MIc62@%o77(O&8!n{-*4pb>-K@9 zRLw7QUs0`-?jK)zy>7$)J}UB{Ul;ZB>+s_5%;&4BYkS&#L(I;@g^WF%gJvBM6f}9a zH}icXUpGH58D{czr%M^*7k_>U95(r8_?Ih<|F)X>(3|X!lUvL>8yfYDrWcxE^7qIP zljnF(nEW;Nj=7%@J3d*{iQKqGx3Q7Wo8w1J8z^~LABV^OV%qirJr2TUi<|NA%d5to zRiD!Kv-NT>PolP;SJl)-WBz{I*6lsN9&OhT>t9{q(A`BeDp z4Vqp&XzI@ae?K42S6a)Hxo-I5C;Yz2-y_9JYX8Kq^K=_HIzYFvPG;VW^)~m7!hW4K z@YFc-xqtt`^Y0@C{QM&B&vT(UZ)~@*koFN zf9{Pj^F+)!rs-KfWsb9Nh8R2SJEYrq4wJXo{X@vVk7f7uUJnx&-WId3cKkY!*VoTm zCT-F73JSe*Mbx={U{Jc#e&1ZtUF6jKk0! zetkLR27Nw!-4{A8omQ2M-TZty(5sQr%fIguTe029wb;y)&X2#}ABE?oYPp;eWk;9!JM`zyzTfA4PQhG;zP~<(-)Wc6>CZReZ~N$DYY(T~Bz+w5?~_JWoBKdv|Nc*A zp6jm<@qQ+*?Y!X(HT8L$=i2<)8EO1u=c!08Q}4-)OIv4q_Z_XjS2fA#>*qVRZf);3 z#QpnCwhr$sHS1R37C+7(^6N*wJ;VO}qVT%5nr>*P$%pNJFVg=ZeLk??)L)#bzZ(1b z^JA>Exi4VrmP-(d2zuz&y5?yKW|eJGS*^4ib}lShQxb<%v|siyTdU8avC{yb^- zm!X|zKan{fl>0&Z#nw5T)K=O~F@L{k^9Nghjl68;XB+?V?k2A6eX_8B|1Z?mU*G*W zuyvWtdDyRG+w~yU@dnK|;_o-?em&yfFAo2>O@D6psrG)Fci@rVk9U8*iM(OrG2qt) zY<=9;VQd~@u3Q#PqOMM*3ImFn+5(nT_Yx9ZNhhJ(x+PbS9Cw9KG_u)hSI%V@=CuZ{Ekh|5yvA?d{ z^*Zj?C7nBKYd<=6U3}*P<5zp%_Z8#s_?^$`^Y(tH-DicLKd#S*{rleDyp=}p-ez3K zn(qFc{(;v`zUEbX$*d24UDe7P_MacfTn}$B`}c@nUyB#<^Y(|{eiuY9e;M0zAiA!)YW3g%|1Wo@Ao{v?qlzd z#_j&C2Qi8Os2dGrR|#+!vryLO&#ZN1Mv zw`B8<$Q!1vXX}=+&&+z}l=)uU$>yJ-Pt5$`>`2vec-`*MZRqHH-G-N$`;M`4ON^Yc zul2aI_W^AFYxDV#f1lOfr?Bglz5f$m@QPX2{dpzxc?kc$x#K^75%=TRKF46kh0RB8 zKH&M!)5QGejBFj-@#nKpbMw4~eg4Nj4-$T+iME5ik6`mRd%wfhne6>|n@8Ju!?DjX z-R$4T^4G^MrZI`f5$L108W4KyQoBxH&eWZ_L z{fcUTM7CGeZLGY%j^8{=AA5C7T_U{JzyEjK)JtpxT z_P&~Z&duJ>u=`=Bqigi5-&(ivX?{J%4V&|Recaw(vCj|1A75kaYxP}V{9@-#`~00( zY^JfVe?P-M2VkE&v-j--N#E$}!oL4)y)1OqoBDiY?^n9@y7$v^g)cOIaoVlW$IgRm zbQ_rD*RzU!ua9FV^5}6Lism!zq8hplukT=5zusf>GCQ8^eWqaGXfu&`z_9k_3U#Gna@-D`#yW0H}L6B-7oIXFLvKx*PqONtAAhK@t;R?{O7Uk^NjX+ z4qFem^PSxfhBulxwRKv@zh7+cv)JbdV}8BR^Y16weR5_T+2?WW^P7IX$<~MM^Ca<| zcN@DWKA_u}Uw5(3A4RsB`z!W&ozRv=`Z{lcsWWBX7xUxEx$J90FWDdeH=5`AoLfv@ z7=N>#_GA2OGq2mandAF0meSMo_xafS=Jt6}Z|yA8-+#U*vS+tG4md~5`FDTPZ8&1$ zGu)x1j>|xLRo&YAP67XZTg;!&?DJ=yzkd!mX8yJFaKx{J+51iLBjdH4c7CwWZFv53 zQ+hr7zkiRca3CQdznhlF{>GalNBkbtxxYWwjShhTUk%Ek-|0W|e|!5c^zK~We~Op* z+d{t7*C=N{wcmd_OHun#cAIU1Yzt&tAlm}j7Ra{1e~1N|iyn;xJq4`3=-rMG!*drtpf#J z1-%3{1f2zW1&#nd#RQcE?F5$#_)S}AHWl;{R2IOK-+XN=AZKYWs3}0E>H@9_3FyOh zjsP0a^8`tP4uT5=B?a_DSM)>AZh~fl%LLHsBtT|p@LRsviQj;|Q~(dI@dSK^{jev$ zoePiV0(5`|bdeSQtpwPJbMVD3K>@U|K>-1Du$knbfi41k!dKd%(MUkwmICC*XV65h zD+KifZ3K=0xvL7051rAmh5#SXAO6rq2KdmA>+wOVAW?w)Tn~Nt<`jek$Wm8OUSMs8 zU9c-M!MlVYO+cLB18jq=(7{IN84{pJilDv#9k2)T(7%Tuw*Xth8@te#SV6XAL16*9 z{;(5I>Z`;->oKd#3Hj-Z@?SS1#F36PQgEd@#-I9XjX+ z9b_VQppQL?G2#hb90C5v&mlo`0oOnq9q}bRx(f0L=!+dL5fJnEiRpO4|I`>n80V)+}aJf9Rc5+n$)6EsT+ zh^0Wa*EDySjAkLba8rysV;FTgh; z0dgT{nt>YLPm%!nt*?nW^u#{UfEThP3X%oLjSmV5kQ4u5A70Q! zr!+x#0kk=;BR~#pfgb4CO8^~5KoqThL;DZ7uKw*cDl^54>v& zhy&=skL!@Vuz;B0T4+PBrJ#%e8$$Ax zusQrg0%+myL;-Q)2)G^@@nM1h8PFSfI7iH3`}zWGV`bx7Phe@bkoU-f9k37nKrdv$ zKG1>=es%=NMC{{x^uzvK&oMUSI(Q;K{jr@Rz_zUf*rSKwashT2B!D;e;f0)B6B1xc z<|XK2Q(_DGum!Y{7drg}%>-PRCa5QXpS2%0^8|bbjs61qR1zQ)@rBQ!g&v-uvH-af z1?ZP5$RWU&woj70M_=Nen1Da>p%*cInE+k623w&U@ojZ)FYnO{-q;vBHE{EkH-?1W)L|qmrPA z0Ga5EuGlb_0K26Lplz?m2Iz}E$Z2g$|9*mU0^~ubzJiJZ`_txKTiN3WI#U7VJAlb9b~ckvgQ!0@V`_5t$YG(;R%Qx`g0Ba zDhaR|@kJ~kQ%F!ykVAmYh>H|K1A!;tI^^bBUdV-Ai2+A|&h{AE*dH0t4L;Z%TF}LA z$b?U@lOw>6$eSoYR$>6XD+?GqAp!ce5YQhw=+Cv-xvik30DjQLmiPqv*b7}M2(UZg zI&`=~fZmKF{Dkg|CwMak`U^S>;D-*-z%TSi2k0jYxDL6H1G&(nqyU;+kKFAA&_y@; z6K|~q*bSce2Dv=}zAE9rM{ll!F1jE;erhbZRFGc~5+I`zFs5n>3JES2(7%qLoS?n{ znL`42LJOMc0)2diJ)w(?*d;-L4pjwQpDZXVfEV;^OnCA>BtZ5w0eWJ0=p!4p=qi8~ z@xkX61Y85}qJr`QYzX};1;hk##GHe_=#Rd%@NXqR&Ljb`!u80AeXuohR1|mucwvV^ z0%G15!m2#^syh#lyb5_A;cYv^GwXb%!_ z-s+CcJpuNHAJ-xqFC`!rstMqOjP%1U&?kn8i$no>!K(2r}OflrZv7MbA5b;!;6WC1Y)U-%^n&;k9>9a_AnFZM)!c+?XV7SIo0per&| z72q>uL^f=iU*HMg&9(Tux&Yn#2}%kW6X^P9_s56GHN5QaWG0Ce$n$@=KmPO>Is^ic z!i@fFzJ2pA)t>AA_#&gjA7l+p`28tsud@{W{RP=a*%rvQK(+<4Es$-2YzzF`TfqKb znymj9;J4qy|Hu9>O+K-V{l7Fh<^B2izcl2K=lZ`M!)`DM&oJLZ8+!<}><)9?AO(PaI96nnGyIjfZHU3v9=$S7xy z{`l9_uy)c3aE+r`G+S%LC5dS#!=wm`N8vMrEpfouz8 zTj1Z>0^Qd)P!}()q%L0lox3c5MYU+dE){FpMm1|tQ}tZ5)@}3Th`v>0)R8%d;Z&jZ^*iN-wJXdX-v)YZcTc&>fby{#;&P{56@0w~t z_(8SzvNI}S)y3+_%GT;gil=&?-l$$LyI;NbLtN!*|ETJ6OC`Cr5)OWLSst}$uZ6HdA>?fqHxJ6IuDd(^Y;l+-=$*4THdQHgEp-8#3ZhHt;E1|?ji_Mfh% z-U<#1*12JqnpNik<;;3b^?A02T3M$;@Tnq&RIcmt2B(j$s7CK>psud4Uv9UAgC89n zp~5#+3s!mWbv1Efadl6l+M3q$x0hBA-t@5h%gcJ?VIvrk}7^@e|7sk&DGq- zd(`HhhgIdA)q*4LKCaZWU#Z96o~>GaxJy0q=nQx4%)wgkpI`V$EiLt!dM@w%TGkU@ z5mlh@V3p(7{J}|M%c?@-FH%>QUZalZ+pEH5YpGIOE>bn8R##7V%TOcl&!v!kRIg9m zBO@MF#pXY%W|b?T`t5y4eOJDbyK2l~ZL2PoHmSp(ysoZIouL}uRw}q+$#?E+{fn!; zIZr9#hyHgSdc`f$%u_WU|IS^}DBxa`P)EJB=m$0Mqxq_N=^|=X%eYE=aGol6@;&v| zf#a&%meX#|e!<}KZg0BR|BzGN`{aP&w?`gO^+r}xn-}EO_9$H^m%3qTy1SxMso-@F zO;G!C%~y?o`9!5I&ljwJ_jj(18{+My8RgZ^XP$EB9Bih#lz32mdX1+-cXkX;xac;O z(loc)9&pwEuQ#jROP8qzCu*y^R=($se(Xmz;~8m}AEwPvHI5b6HpG`7KRe64 zp!a?1z^q)s`5l+4Nqg^f?;rT0yCHp|QhBcpzH#|RcX^Km>XPb5l{4@HbzjDOwP)u5 z)ihT%E&COl`>VC>aw_+Q)9%oV0;7KdZS)sz zwen0zYR)Hp)z^`cs^+JogRj*4)y;cg zj!M4pL$}kDP1T(rCIpYqdqgn?h+*uy`x4=CWzJxlst_EP7F1uCNKkd>A5~M*o_7bo zTT&G~);9R*h-vE3qyehTBa76*wT1P3fIX1wlYDbkzq<>lhI6k}qu)KIb~km^^0Euv z8`lTb3rFQpsh96iI#2tI8L~+_KA(v|iWF%N2Zj@*0JY z(HEL$nxv?8TidJ4k{$K#;Nq&yXWP`MCULc`(QK8_xlVA%%D(DYxl(H3+=untz!<^C z$O4V&4~Nuui|44#AC6KP17DWg$l>6i`|AaVmh7W?j-3{qb4@PQY4MjXb0hN-@vUtk z*FAT%TCi;Lr*6OL^Hs;H3F?jNRaNS?&)gGhK6WeLn-=UEnxX1nv04oY+@t3+#uc#% zkMDN9?S6h@uUod!A@y)!Ps_3~QXn{1HBk3mKU;PDV2gTh(`zo{nRvpd-;{nuRX_Nq z+oVD%)vm-&?U#n{J>wp|ZKirZXPe-SFHTpXscqd3;|{2AmaS6Eb48!|-G7(p_`ZPA^40zPhfK_2YMo zs1-e4bD0kr56FQIU{my6m6WTv8W}C4)*p(yZ};t^I$m{YFgAX-+v&(r_w-NqtGsXb zP_JxVrgEQNt6n;AgPq@qK^irMrwpP14R#KB@ zeCamZ{Ed5J^?T~FL@&s^LR{nXH6K2tGKP#*lZx(0X*O!AJEHMxTBb&WW~Fqx;1yN; z5yy=msT5q%E{|Jk|4f&4jXB}Emh)7j>ly~bJ;v$vspOW~Dmd#4H%G(gmFs-#mYG>x zy*lf8_wq5t)b-oHaan_y3z^Rtlf+uClFPOHrAyqb&gB2n?cC{M_4wqZ;LO{ecbo0c z6f^ORGE-|3)rYBH<6`Y;$*k1y_|)_%2CpJQ%i9N|N3IcaCV;QICNq?|l< zO>k!01l4Fuf*QELtNQu&$5KvxHcUO=x}JOD_*KFCC+$i}S${$?FEg((4(}N`Lrr{p zn!0Y&4wd{#O;vqwb2a;UN4?VSWi@lYqXs{Bx90gwk9~^Vfc1>ImHCBn2Y>tlok{ms zP&aIdsm%)$gFC9+<3{s@)RUWXt4ku)Q|{>BUG=)ORm$fn6;<~wHv}JUFj&i8CE7qm zXT9J)pWH&RPO@Gw-yZ*bz5DTXh1LA|_p6y3Q`NGX1yqk|gEhalTfTDN{I!;)cc(So7N&L2@8+0M=&KCe{<;SIgA<7x%rPOWYdO$Egpy zg@T{mR8+m$qmBA1=?%BRie&ZEJJ-66-}%KQFC`yi{vz&?TgEBDoIp5q5=E-(T{mNpIzM4=fq2 z>2}KTg&Mo7aqy;6n^e2qPrBE2$#8FqRtSX?JMn}X`otsd@+k{d z>ikFD@4kw={TjaSMrJ|0kE|Wc7sLSmv;0=9uNa(O{w+1)`wgn=;GEj7^=I@`W$Ug|7Z$A%jNZD_ zJ^9P~ZtB(5)XSe=B{@N3or{v^kteZ+vM#djGnR=jVxx)p1|F-{ysb7T?$cxA-Oqkh zGltf7U%Fvnkerjej`f`N3Hx#VjxSzzKYwqRyZ_|t?u}pM3NC3rRDCgexO%XC{$Q_i zlNGr+`3X4yYd7l&a}je0wusExs1`PPO)a~Bu^Lq(m-?>de)ZsY-PDuI^J@C!Wz6Hu z$BaW_5x)3t?%=s@(!rj=lnMu3_v=k=H}3&A8e8OUb*8JFr%DD#Romc_hmsqSe~>q@ z9`VKs61_wHTqRtgpg zbPtjXk;9OmFc&gkF+K;ZyxE;RYKPmcW2)xU{<#Ukxd)$hcQiZUmYOrqW&g!qiTsS* zg7t)Xj=8DrgoW;zncryNHEG^j_YZ7b=I-jzGsvC|J;}+*hqMi49b#M~H+>(Q-BZ0- zrLyY%epwZdH&)$ej8$bTZdB}J$X|#VaslR4#xQYAEMhbG?%P~FIIP)Xb>_x>?p3SS zxmyw^sQlrN)zT?bRPQ%uxcd*6)qL3#k!zE$l4FuzkzcUZv)-~cvQDwSFn2RoFwPh= z#69tXork2Z)AY(rs-jYp^T=N1E_LmSeX7ny?NzNCE9*Ti`wwzraz*kfatYRR)=<_T zV(9Kp<#kNGJ9?(-dAw-wuA}?h8TbF-?!NIZO`m+4JeXV)yD{#FSK?&K)~nP9cU4uN zwl1!I*z~sgyx~EWuV5f}x?E|s_}iX}JuUkmawpaf=5pq}OC>MwvwW&**{7;%@mNu{ z`mrLxO?y65rQSCCQTDU!N7*})%aaF^doh1A|1ozkR*4sDhkTvhP_O>7Sj#x8@=!JX zt)1?ZE9pJ9DY+532)O|3tX^N3pT$gYRHs{%E9tD`&ixQka6Q`hxORK>>jR(D=jP~CWQ zEft@iS6#h*tlsaj|AHRt67vADhd$76G_s&t@p$R_#NA!%OyXLP}`@~3zprN?oRr7 ztr|DwX;nBmx9*d^yovg(^4spOv5M;Ty9=vRqhjj%{&%Rux!zUxOk1U>qmZAIr;=Z? z)-!)F9*IqSgRS84)yfN0=dyRGOJ?1w$`vaXe5UdE3j8#zd*jhn$KFxI>lPU zJk2~o%oE4hrO!5#+&%W~3YF6GIbAEF z20}f6Jtg}Q@?Y{m@+syX?6_~~X%)Tj1x@>j1?yCeGZzHQjSeX4Q`8aI=d&LmuO|m4 zw`1L6?qu#`{vo#TN5wtU-C0u}QEMxPwVkMMQLmzQLOp^#JbNPcH0)K#kI8AtOUX~j z4Ori?9cvx)4P%$_M(pBmY=F%0x;@tsHRjNKRb$K-YTA40>e=KXI_^IzzFiG$+$^~M zrbVjkr#I?)8MPqlA?#~d!&!4#&saO`8o-!lY~uH4N3B$&)3>Tc^C|_8?rEsL&)wgp zmO{;g{XY9@WMQw!K83uT{0jXKcC7EVeR#5aNv+G(Bd>p?sF$%HW-rJdf;|CwA!|PC zIcqU%AaepVH!Yc~CS1@w_{_q_Zv6YJRsQR@>v|ovEb1-n2ifP5W0RMWPq7Bjm+{D$ zAWpt$E47ymkGV%*DWf)|RS6FMwrG&L9d#_OrT)TRo4qc3M)oP}5BQASlN^m)gE^nM zig^Q@y9*q(eont&&gqqP4Ud`_wHazF)Ir!cvv(wCCx0bxWPN6>WA4Yl#4t8)KlBY9 zd(Z7Tq$(}Wq3fa4*QhD6XJW6y9)i3Rxd8JG@lW3ck;SU@gn>b7eAMKqCsE&__Q2ki zy%K%MS;;ra-N>C-?^%PGe;MP9RpJ((qaS_7R^Fhlso7OMJ!`UheEqd5|IJUUl1*zV z>Za5_si{%lKp*xBj3fFSo7_YVUeHnX%eTq>dBo#x&s@ps?tS05)F7#KQIBDN&OVv_ zBzrmXU~(&R5Y}?|GeUyE#SEUEXiL+H(0Mw|%pFb*+^;sjhWO?U6bkH9Tru)K;ib zP%B_>$3BOgmVAg@hV_uSlX(t#8S9KI;+gm(X0S7#;e&@-JmVJ6ebgOz@oH6aPnF=B z%4WW%#%ybb%Y#xmdIZ*0CW*J;y%-zhfj5E&nOemqIeqB&qo&2cco&q&h&QnLE z7DpY7dKBlV1<;a%vEDQ1GH)_o84tuFzL@__j^OaF8}!^m?VEZWIU9Ko>n3X->j(2S zV~BVn4(M0l%8l;e{+(4|e-(YdfVw$6sR>e#qozasgqj5V4)zeN7mO>$0dd*=ic z5BF=RKT)rte!-rPT$Y@aT!r-)+RUxQV3(?o>iav~SK%HB^?K^w)KjUAQs<=ZL+yrI z2(=3OvxgzaWn423u_^i^N59k`Rn01i`rZZgIqE0umDo3skCO+I!?BLC{xF{~uCNKV zD6{{h;+_dLMe0q|b=Z@$7iM1wfA$9C=j4;*f~;YzLCn|0P_+6tF88~*cf$P->iE>n zso7FzrQSzvfxRI4C^;9o4Rby7F!L(&6=R)o$~eGB(EaYw8rlZj)8f7g_avwxQwydB zO8tX9JNsAmZRCgKW#kOZ)yy4?d&U*IaBq!!Q{0>2UIR6B>e1A5sY!B;u0Kg_hWG4= z*$c9-VSmEDfP9)9l-!DSj`e_fg*k$;i%+<3#=R5n9Z;jEUPPUP+5-D59g~uilh?2g z;Rkg6_J=R@y+7_jaW98^FWh^eUQfN0T94gNGp_JGa?+3cCfsMBrc7Ol^VDOgtx#8B zZ_oaU^W>G}Q{)lMt;{!!QN{&v`e)x)vd=^P>c9Sh?<);^_-{Uc_wV(6rE@($wq$Mg z^JD+c_y4nH&$d9e1+p!WZGmhH{390NyW)ILy^NrOfbXMM5%B%-8UntP&UeuHu6j!W z-y!cP;Je~{cl;^=-)Ao&;QR3WMua4%0ZHEUcg|Z#OP|()9s<69PCve@9un|9c)r8V z_nVM|@9Og%cSlf3fDF*(JL`Oxo!`&sD&YJ30|h+i&i9u1%@1TKC#Wpo`|ULabp(9( zzOA6Wfc|`!zqIwc753-Aqcp&K-8TwovQ5DQkm2J+s<8M0j_AkL62BtSOgfhPLeSVLEAg`dx+ zWp%};Ndjy|jMEp}a6LSULu?FB<_PTESpZ++j8~!{kKh6UG3E%M0blIUTtHlO74#C& zkFgOF&=>z&TRQTdzRUyI5&p~to`8P%fxgIVZAdISf|>$s#`WljAK`7~M}`gpe1ZJL z3H0y>Hh>;Bzz@{~#05520#DFF&`N+EE*CJ~LIUC;pMdx(CBWvD1lSu{ut5_6@<0z7 zj4|wC5)2X~ z39utER7g-pfIN%^{CajgK)0~~nk@v3gMNaL0D0(NLVzw+1=g47gFTq1kO{r;CA#4Y zmga)>B1a$=Tg>NeXecB5;3ebZ%;)VaPrS(NgdCxd3FF*(E3axqqXdwgiun9h~ zazGQ`pi_SVeulP{jd6({=uUqt3pCJ!{`eQU=#Q_kE&Z`?Q32zWcx66CK5UJ>kQ4fh zJsZ2$R>Z93#T)=1WP?7mi34;lC9w04jRoj223!H-5!pKkx(Tel$ZK^b#zKN50kWfi zE&(xzeTxXJ9>hvHfg?Z%#t!jmkNjn`V&Xkl72}7Y*Ac*Ezcf@R)6%MuP5j( zAclC+kJzRk^1`pG0AHMKKRbVTa=x{|>W@E=1$!b7^w1w2nB#~$;slw_rjJdKsigpY zph=t%&$$G71n7@W)djT#%>`V`b=G!_2V_VSBngNE`oX)Tpu7OyH3jH{4bjCDK*!pm zkG#((Kvu?hB>{G?CV+n<0Wpc3_zU}4y+iUI{>Xeb{}OV{xF)WU0lPvEU)B-e8*5i& z=_2SV=qb2LFi607vob*keUObYf-cxZ36P7}L?7ZC`K}NQ5LkQQi;4nAkSwtFr>~7^ zbfaH?L4qJrKrGrAapb+_kNmCxAHtUy2npy%3}6p<+P>JczM!E1nLR-Z0dnIm(Hs9j55M9U^e8HTE`Gt!nT9~0Rn7#l^`TQm%M`f0{lq~<41Vl z4`Kp(j34Mh=j`}PkYj9-TL2wmu#ljH06&!ypcB5vXEg*i_V78e4lnFy`C-2ng0uC3 zH-51C5S#cJzw{HFJvQN8MgU)81e;wfXe6*a@KaELkF9^KU!aS9(24m8eToQ*3(x_Y z$k;(ZU+Y)wOkZTdF2o$ZWqv>=`e9!ib2j#o4gH%4tpA{m&27A3cWC27WJ?pk1KTqu zusb}kGh-1sp^Ki->@0v5{jpa_z>B{44c}ukV$TuO5;PE4{}2b3RvUS5Z6S4WAs1=uzq4sZGrGra`#w_`F<-s4aFQBqJ!;0TBp?8Z334=Dm`CulJ)kmX7Nwm=r^k9_jJ zsDRjo9&+IeM?fq>*ZKk3px;W+NdV1$0vpHq3y>Av=u2D> zXI4JqB3VHHCIaNcX2?RE!T)SqS(?zoFIE=xCsr;H5GU3SHdgQrabj%%4^Mz?i9Pg3 z?z4RafAoeYx?7v4$@?S$F$&KTf=dOB1y%<;cVS;o01s#p+m=p9-a`i+@Lf4UO+g(2 zam_r4%$621+JV7e~dcy}E{RM*r=mU>}fKhg&~BahV;e_C0vDf%J{_G1jA zPZt6DKnvcyuvB6aLtom_Y_;S{dvd z#uy0+ux~Cw83FyQ-|-PP!S}5M*wWeqTVTgj0d~OG@OA_>1o)X4B7W>Rv$UYYxa}zD zCBUA%kb!YU>=YEB4|XM1s|o4}prHhwfd0e@@rZ8LPd(*5vJs!ihkl6yXfqzHt&q8@ zz{=WC-V=|+BKku2Y?{cLAi(zd1jtD&LXQ|iPW)L(P(^^u*Z{iN65rZ5gHH=V2LXKW zZ+8K*_ZQ$}WW^5fNETSTVoys4IkAC_OLRp>Y)O1r8inOO@}VzwL>}S*Uf4fXfIb%p zN(!v})#W{WYYWf?9f&1Q0B`(=jgT99*u9_N&z>I}BG>TxC!QZ$wF^c6Lx1nuo33@s z{A2A!>}9{QEs$-2Yzt&tAlm}j7WmJ!fc?Gey^~FnbMkuA{`Pyj|NQS=lMkPd-@7L7 zwDLD8YUJ)){I8Lp{Kx)YH@WTkke}Q>L9%=#E-m0?fB(4jL8C`ZpOw5<|6ccRd92N6 z{g1Po|8fgt{k?9+2Cvuw6G!IA?AdtFUh1!S+3^>C)9{J-{QlYbUof1V#l)(&8RH{QR_UPaCE zA9+!a|L>AzGyGSuK-T%FzAscaf1R>2j!hkxI!cDV z%n!p;N2l5Hd*<=Laj6rAkNGWcx8uk5&)S;tov5G3|NnmDxMC*mK+zA|U>_yk_! z#+AJ6b%o>RUHvTbnydcv|NhzdjvxEI{r>YUlC9y{7C6`VuJ6zL_S)-*j2kmBZOG^e zLrgv}ar}^h!zA?>Ic1<8@`Fa2A#cas|89II>3gp5IGJVKp8U{ID(uTq*cW8jXO@GN z?DwtXAI|^$?_c-(UjM#F31UI5sjn`_ysY*~ayOdyrNzFq{C{NgS)M@K7M**vk#jw} z_oC4sWd8rJ#6*2r9>1cOl<@6DeN851bM(*d&)7TJ`Xu-5ZQZcOaU-S8M*{)JF#MCh zcHOXfT-c{`u5NbS*!zj$cdqOH+s>bleKKP>^Zz>f=01P%`rUbtSDqs}A9gA#bZtE{ z>pb|+j<>Vxk;TrF{5*s7M?aepnUG){&ofrMZ2Q0dzl497m)@LVC~Y^5|M#D@Q%<2{ z=Ovz1Iv;-KxuO3*J1x1{nCAT1iRZ)4hniDKiD&8b^14cdp_n{o+-EMY98NQf6O!g(Rj_}SAfr7yz)H!`LK(l?ee#u z)98D~7%C>$@cOg5(cT};^^U$e`_F%y1>J5-~FOc*w1a3C-)b@bpd zqXx=iG(i`R%*x|wO24f?S?d}l{qbxo0fzBE zyZ)SA*Qg{U&&PPKEOgB}lq2)GtI|JP3mQGL{HM}01a|-`2YX_ literal 0 HcmV?d00001 From ee6ca6251edbeab8b6a069cbddd46a2b1d4d9c43 Mon Sep 17 00:00:00 2001 From: miguelrosell Date: Sat, 28 Feb 2026 00:43:37 +0100 Subject: [PATCH 6/9] fix: update versions logic to importlib and sync all test snapshots --- bin/run_causality.py | 5 ----- bin/run_diffmap.py | 5 ----- bin/run_topology.py | 8 -------- modules/local/causality/main.nf | 2 +- modules/local/diffmap/main.nf | 2 +- modules/local/phate/main.nf | 4 ++-- modules/local/soupx/tests/main.nf.test.snap | 20 ++++++++++++++----- modules/local/topology/main.nf | 2 +- .../manifold_learning/tests/main.nf.test.snap | 19 ++++++++++++++++++ 9 files changed, 39 insertions(+), 28 deletions(-) create mode 100644 subworkflows/local/manifold_learning/tests/main.nf.test.snap diff --git a/bin/run_causality.py b/bin/run_causality.py index 5b1d8a12..cc7b5916 100755 --- a/bin/run_causality.py +++ b/bin/run_causality.py @@ -47,10 +47,5 @@ def main(): print(f"Saving results to {args.output}...") adata.write_h5ad(args.output) - # Versions - with open("versions.yml", "w") as f: - f.write('process:\n') - f.write(f' scanpy: "{sc.__version__}"\n') - if __name__ == "__main__": main() diff --git a/bin/run_diffmap.py b/bin/run_diffmap.py index c16a6d7c..0fbae45f 100755 --- a/bin/run_diffmap.py +++ b/bin/run_diffmap.py @@ -52,11 +52,6 @@ def main(): # 6. Generate versions.yml import scanpy as s_lib - with open("versions.yml", "w") as f: - f.write('process:\n') - f.write(f' scanpy: "{s_lib.__version__}"\n') - f.write(f' numpy: "{np.__version__}"\n') - print("Diffmap computation completed successfully.") if __name__ == "__main__": diff --git a/bin/run_topology.py b/bin/run_topology.py index 165e4d5e..9807beb2 100755 --- a/bin/run_topology.py +++ b/bin/run_topology.py @@ -76,14 +76,6 @@ def main(): except Exception as e: sys.exit(f"Error saving h5ad file: {e}") - # 7. Generate versions - import ripser as r - with open("versions.yml", "w") as f: - f.write(f'process:\n') - f.write(f' ripser: "{r.__version__}"\n') - f.write(f' scanpy: "{sc.__version__}"\n') - f.write(f' numpy: "{np.__version__}"\n') - print("TDA computation completed successfully.") if __name__ == "__main__": diff --git a/modules/local/causality/main.nf b/modules/local/causality/main.nf index eb7b7761..d13a79fb 100644 --- a/modules/local/causality/main.nf +++ b/modules/local/causality/main.nf @@ -32,7 +32,7 @@ process CAUSALITY { cat <<-END_VERSIONS > versions.yml "${task.process}": - scanpy: \$(python -c "import scanpy; print(scanpy.__version__)") + scanpy: \$(python -c "import importlib.metadata; print(importlib.metadata.version('scanpy'))") END_VERSIONS """ } diff --git a/modules/local/diffmap/main.nf b/modules/local/diffmap/main.nf index b3072dc7..22103570 100644 --- a/modules/local/diffmap/main.nf +++ b/modules/local/diffmap/main.nf @@ -34,7 +34,7 @@ process DIFFMAP { cat <<-END_VERSIONS > versions.yml "${task.process}": - scanpy: \$(python -c "import scanpy; print(scanpy.__version__)") + scanpy: \$(python -c "import importlib.metadata; print(importlib.metadata.version('scanpy'))") END_VERSIONS """ } diff --git a/modules/local/phate/main.nf b/modules/local/phate/main.nf index db270481..a5e49658 100644 --- a/modules/local/phate/main.nf +++ b/modules/local/phate/main.nf @@ -33,8 +33,8 @@ process PHATE { cat <<-END_VERSIONS > versions.yml "${task.process}": - phate: \$(python -c "import phate; print(phate.__version__)") - scanpy: \$(python -c "import scanpy; print(scanpy.__version__)") + phate: \$(python -c "import importlib.metadata; print(importlib.metadata.version('phate'))") + scanpy: \$(python -c "import importlib.metadata; print(importlib.metadata.version('scanpy'))") END_VERSIONS """ } diff --git a/modules/local/soupx/tests/main.nf.test.snap b/modules/local/soupx/tests/main.nf.test.snap index 9c2ae052..c7465df9 100644 --- a/modules/local/soupx/tests/main.nf.test.snap +++ b/modules/local/soupx/tests/main.nf.test.snap @@ -3,20 +3,30 @@ "content": [ { "0": [ - + [ + { + "id": "test" + }, + "test_soupX.h5ad:md5,d41d8cd98f00b204e9800998ecf8427e" + ] ], "1": [ - + "versions.yml:md5,d41d8cd98f00b204e9800998ecf8427e" ], "h5ad": [ - + [ + { + "id": "test" + }, + "test_soupX.h5ad:md5,d41d8cd98f00b204e9800998ecf8427e" + ] ], "versions": [ - + "versions.yml:md5,d41d8cd98f00b204e9800998ecf8427e" ] } ], - "timestamp": "2026-02-27T18:19:30.615804799", + "timestamp": "2026-02-28T00:14:22.594548408", "meta": { "nf-test": "0.9.4", "nextflow": "25.10.4" diff --git a/modules/local/topology/main.nf b/modules/local/topology/main.nf index bef7be2f..c19befde 100644 --- a/modules/local/topology/main.nf +++ b/modules/local/topology/main.nf @@ -32,7 +32,7 @@ process TOPOLOGY { cat <<-END_VERSIONS > versions.yml "${task.process}": - ripser: \$(python -c "import ripser; print(ripser.__version__)") + ripser: \$(python -c "import importlib.metadata; print(importlib.metadata.version('ripser'))") END_VERSIONS """ } diff --git a/subworkflows/local/manifold_learning/tests/main.nf.test.snap b/subworkflows/local/manifold_learning/tests/main.nf.test.snap new file mode 100644 index 00000000..8901a1a8 --- /dev/null +++ b/subworkflows/local/manifold_learning/tests/main.nf.test.snap @@ -0,0 +1,19 @@ +{ + "Should run successfully on test dataset": { + "content": [ + [ + "versions.yml:md5,2739ae12e44ad2c9e6a1ccf1e10a7e53", + "versions.yml:md5,7ff083f7fa9bed2b0e51b5e8e0dfcca6", + "versions.yml:md5,7ff083f7fa9bed2b0e51b5e8e0dfcca6", + "versions.yml:md5,8959b9f7eac3b55c99ffe18910dd4a44", + "versions.yml:md5,de9d8e4ffb9637560e57ca638f3a7a6e", + "versions.yml:md5,de9d8e4ffb9637560e57ca638f3a7a6e" + ] + ], + "timestamp": "2026-02-28T00:42:47.298251606", + "meta": { + "nf-test": "0.9.4", + "nextflow": "25.10.4" + } + } +} \ No newline at end of file From 3775784505b5685994431e311ae2ef035b508bc8 Mon Sep 17 00:00:00 2001 From: miguelrosell Date: Sat, 28 Feb 2026 00:56:25 +0100 Subject: [PATCH 7/9] style: fix trailing whitespaces from pre-commit --- modules/local/causality/main.nf | 2 +- modules/local/diffmap/main.nf | 2 +- modules/local/phate/main.nf | 2 +- modules/local/topology/main.nf | 2 +- subworkflows/local/manifold_learning/main.nf | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/local/causality/main.nf b/modules/local/causality/main.nf index d13a79fb..db7fd39e 100644 --- a/modules/local/causality/main.nf +++ b/modules/local/causality/main.nf @@ -19,7 +19,7 @@ process CAUSALITY { script: def args = task.ext.args ?: '' def prefix = task.ext.prefix ?: "${meta.id}" - + """ mkdir -p ./tmp export MPLCONFIGDIR="./tmp" diff --git a/modules/local/diffmap/main.nf b/modules/local/diffmap/main.nf index 22103570..f22f5be0 100644 --- a/modules/local/diffmap/main.nf +++ b/modules/local/diffmap/main.nf @@ -19,7 +19,7 @@ process DIFFMAP { script: def args = task.ext.args ?: '' def prefix = task.ext.prefix ?: "${meta.id}" - + """ mkdir -p ./tmp export MPLCONFIGDIR="./tmp" diff --git a/modules/local/phate/main.nf b/modules/local/phate/main.nf index a5e49658..7a178f31 100644 --- a/modules/local/phate/main.nf +++ b/modules/local/phate/main.nf @@ -19,7 +19,7 @@ process PHATE { script: def args = task.ext.args ?: '' def prefix = task.ext.prefix ?: "${meta.id}" - + """ mkdir -p ./tmp export MPLCONFIGDIR="./tmp" diff --git a/modules/local/topology/main.nf b/modules/local/topology/main.nf index c19befde..50b7ff33 100644 --- a/modules/local/topology/main.nf +++ b/modules/local/topology/main.nf @@ -19,7 +19,7 @@ process TOPOLOGY { script: def args = task.ext.args ?: '' def prefix = task.ext.prefix ?: "${meta.id}" - + """ mkdir -p ./tmp export MPLCONFIGDIR="./tmp" diff --git a/subworkflows/local/manifold_learning/main.nf b/subworkflows/local/manifold_learning/main.nf index ec04cd79..f5b96477 100644 --- a/subworkflows/local/manifold_learning/main.nf +++ b/subworkflows/local/manifold_learning/main.nf @@ -18,7 +18,7 @@ workflow MANIFOLD_LEARNING { // ------------------------------------------------ // 1. GEOMETRY STEP (PHATE / DIFFMAP) // ------------------------------------------------ - + ch_geometry_out = Channel.empty() // Run PHATE if requested @@ -37,7 +37,7 @@ workflow MANIFOLD_LEARNING { // If no geometry method ran (user error?), pass input through (fallback) // But ideally, we continue with the output of geometry - + // ------------------------------------------------ // 2. TOPOLOGY & CAUSALITY STEPS // ------------------------------------------------ @@ -47,7 +47,7 @@ workflow MANIFOLD_LEARNING { // Run Topology TOPOLOGY ( ch_geometry_out ) ch_versions = ch_versions.mix( TOPOLOGY.out.versions ) - + // Run Causality (Using Topology output to chain the information) CAUSALITY ( TOPOLOGY.out.h5ad ) ch_versions = ch_versions.mix( CAUSALITY.out.versions ) From b18080734a1b9a9863b90ef74736d544e88b816d Mon Sep 17 00:00:00 2001 From: miguelrosell Date: Sat, 28 Feb 2026 01:21:13 +0100 Subject: [PATCH 8/9] chore: update linting workflow to match nf-core v3.5.2 --- .github/workflows/linting.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 30e66026..7a527a34 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -11,7 +11,7 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Set up Python 3.14 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6 @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out pipeline code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Install Nextflow uses: nf-core/setup-nextflow@v2 @@ -71,7 +71,7 @@ jobs: - name: Upload linting log file artifact if: ${{ always() }} - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 with: name: linting-logs path: | From e67cee34138e68b17bf83db2fb4dda22c7af9331 Mon Sep 17 00:00:00 2001 From: miguelrosell Date: Sat, 28 Feb 2026 13:48:34 +0100 Subject: [PATCH 9/9] fix: temporary disconnect manifold learning from main workflow to prevent channel errors --- workflows/scdownstream.nf | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/workflows/scdownstream.nf b/workflows/scdownstream.nf index 8bc2d76c..232f8ec6 100644 --- a/workflows/scdownstream.nf +++ b/workflows/scdownstream.nf @@ -19,8 +19,6 @@ include { paramsSummaryMap } from 'plugin/nf-schema' include { paramsSummaryMultiqc } from '../subworkflows/nf-core/utils_nfcore_pipeline' include { softwareVersionsToYAML } from '../subworkflows/nf-core/utils_nfcore_pipeline' include { methodsDescriptionText } from '../subworkflows/local/utils_nfcore_scdownstream_pipeline' - -// Added by Miguel include { MANIFOLD_LEARNING } from '../subworkflows/local/manifold_learning' /* @@ -283,11 +281,11 @@ workflow SCDOWNSTREAM { ch_versions = ch_versions.mix(FINALIZE.out.versions) // - MANIFOLD_LEARNING ( - FINALIZE.out.h5ad, // we use the h5ad from the previous process - params.manifold_methods // 'phate,diffmap' (defined in nextflow.config) - ) - ch_versions = ch_versions.mix(MANIFOLD_LEARNING.out.versions) + // MANIFOLD_LEARNING ( + // FINALIZE.out.h5ad, // we use the h5ad from the previous process + // params.manifold_methods // 'phate,diffmap' (defined in nextflow.config) + // ) + // ch_versions = ch_versions.mix(MANIFOLD_LEARNING.out.versions) } //